@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
370 lines • 17.7 kB
JavaScript
/**
* Validate Template Handler
*
* Provides dry-run validation for orchestration templates without executing them.
* Validates both template structure and entity data against API schemas.
*/
import { ValidationMiddleware } from '../orchestration/core/ValidationMiddleware.js';
import { DirectTemplateValidator } from '../orchestration/validators/DirectTemplateValidator.js';
import { MODEL_FRIENDLY_TEMPLATES } from '../templates/ModelFriendlyTemplates.js';
import { TemplateProcessor } from '../utils/TemplateProcessor.js';
import { getLogger } from '../logging/Logger.js';
export class ValidateTemplateHandler {
logger = getLogger();
validationMiddleware;
directTemplateValidator;
constructor() {
this.validationMiddleware = new ValidationMiddleware();
this.directTemplateValidator = new DirectTemplateValidator();
}
/**
* Validate a template without executing it
*/
async validateTemplate(params) {
const { template_mode = 'entity', entity_type, template_data, orchestration_template, project_id, platform, parameters = {}, options = {} } = params;
const { validate_model_friendly = true, validate_structure = true, validate_entities = true, include_fixes = true, verbose = false } = options;
this.logger.info({
template_mode,
entity_type,
project_id,
platform,
template_keys: template_data ? Object.keys(template_data) : undefined,
orchestration_id: orchestration_template?.id,
has_parameters: Object.keys(parameters).length > 0
}, 'Validating template (dry-run)');
try {
let orchestrationTemplate;
let processedTemplate;
if (template_mode === 'orchestration') {
// Full orchestration template validation
if (!orchestration_template) {
throw new Error('orchestration_template is required when template_mode is "orchestration"');
}
orchestrationTemplate = orchestration_template;
processedTemplate = orchestration_template;
// Resolve parameters in orchestration template if provided
if (Object.keys(parameters).length > 0) {
orchestrationTemplate = this.resolveOrchestrationParameters(orchestration_template, parameters);
}
}
else {
// Entity template validation (original behavior)
if (!entity_type || !template_data) {
throw new Error('entity_type and template_data are required when template_mode is "entity"');
}
// 1. Process the template to resolve parameters
processedTemplate = TemplateProcessor.processTemplate(template_data, parameters);
// 2. Convert to orchestration template format
orchestrationTemplate = this.convertToOrchestrationTemplate(entity_type, processedTemplate, platform);
}
// 3. Validate ModelFriendlyTemplate compliance if requested
let modelFriendlyValidation;
if (validate_model_friendly && template_mode === 'entity') {
modelFriendlyValidation = this.validateModelFriendlyCompliance(entity_type, processedTemplate, platform);
}
// 5. Validate the template using existing middleware
const validationReport = await this.validationMiddleware.validateTemplateCreation(orchestrationTemplate, {
validateStructure: validate_structure,
validateEntities: validate_entities,
platform
});
// 4. If parameters provided, also validate with resolved parameters
let executionValidation;
if (Object.keys(parameters).length > 0) {
executionValidation = await this.validationMiddleware.validateBeforeExecution(orchestrationTemplate, parameters, { platform });
}
// 5. Compile response
const isValid = validationReport.valid &&
(!executionValidation || executionValidation.valid) &&
(!modelFriendlyValidation || modelFriendlyValidation.valid);
const response = {
valid: isValid,
summary: this.generateCompleteSummary(validationReport, executionValidation, modelFriendlyValidation),
validation_report: validationReport
};
// 6. Extract errors and warnings
const errors = [];
const warnings = [];
// ModelFriendly validation errors
if (modelFriendlyValidation && !modelFriendlyValidation.valid) {
errors.push(...modelFriendlyValidation.errors);
warnings.push(...modelFriendlyValidation.warnings);
}
if (validationReport.structureValidation) {
errors.push(...validationReport.structureValidation.errors);
warnings.push(...validationReport.structureValidation.warnings);
}
if (validationReport.entityValidation) {
validationReport.entityValidation.errors.forEach(err => {
errors.push(...err.errors.map(e => `[${err.stepId}/${err.entityType}] ${e}`));
});
validationReport.entityValidation.warnings.forEach(warn => {
warnings.push(...warn.warnings.map(w => `[${warn.stepId}/${warn.entityType}] ${w}`));
});
}
if (executionValidation) {
if (executionValidation.structureValidation) {
errors.push(...executionValidation.structureValidation.errors.map(e => `[execution] ${e}`));
warnings.push(...executionValidation.structureValidation.warnings.map(w => `[execution] ${w}`));
}
if (executionValidation.entityValidation) {
executionValidation.entityValidation.errors.forEach(err => {
errors.push(...err.errors.map(e => `[execution/${err.stepId}/${err.entityType}] ${e}`));
});
executionValidation.entityValidation.warnings.forEach(warn => {
warnings.push(...warn.warnings.map(w => `[execution/${warn.stepId}/${warn.entityType}] ${w}`));
});
}
}
if (errors.length > 0) {
response.errors = errors;
}
if (warnings.length > 0) {
response.warnings = warnings;
}
// 7. Get suggested fixes
if (include_fixes && !response.valid) {
response.suggested_fixes = [];
// Add fixes from ModelFriendly validation
if (modelFriendlyValidation && modelFriendlyValidation.suggestions) {
response.suggested_fixes.push(...modelFriendlyValidation.suggestions);
}
// Add fixes from existing validation middleware
response.suggested_fixes.push(...this.validationMiddleware.getSuggestedFixes(validationReport));
if (executionValidation && !executionValidation.valid) {
response.suggested_fixes.push(...this.validationMiddleware.getSuggestedFixes(executionValidation));
}
// Remove duplicates
response.suggested_fixes = [...new Set(response.suggested_fixes)];
}
// 8. Include processed template in verbose mode
if (verbose) {
response.processed_template = processedTemplate;
}
this.logger.info({
template_mode,
entity_type: template_mode === 'entity' ? entity_type : 'orchestration',
template_id: orchestrationTemplate.id,
valid: response.valid,
error_count: errors.length,
warning_count: warnings.length,
fix_count: response.suggested_fixes?.length || 0
}, 'Template validation complete');
return response;
}
catch (error) {
this.logger.error({
entity_type,
error: error instanceof Error ? error.message : 'Unknown error'
}, 'Template validation failed');
return {
valid: false,
summary: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
validation_report: {
valid: false,
summary: 'Validation could not be completed due to an error'
},
errors: [error instanceof Error ? error.message : 'Unknown validation error']
};
}
}
/**
* Convert template data to orchestration template format
*/
convertToOrchestrationTemplate(entityType, processedTemplate, platform) {
// Create a simple orchestration template for validation
const template = {
id: `validation_${Date.now()}`,
name: `Validation Template for ${entityType}`,
description: 'Temporary template for validation purposes',
type: 'system',
platform: platform || 'feature',
version: '1.0.0',
author: 'system',
created_at: new Date(),
updated_at: new Date(),
parameters: {},
steps: []
};
// Simple single-entity creation using Direct Template format
template.steps = [{
id: 'create_entity',
name: `Create ${entityType}`,
type: 'template',
template: {
entity_type: entityType,
operation: 'create',
mode: 'template',
template_data: processedTemplate.processedData
}
}];
return template;
}
/**
* Validate ModelFriendlyTemplate compliance
*/
validateModelFriendlyCompliance(entityType, templateData, platform) {
const errors = [];
const warnings = [];
const suggestions = [];
// Get ModelFriendlyTemplate requirements
const template = MODEL_FRIENDLY_TEMPLATES[entityType];
if (!template) {
warnings.push(`No ModelFriendlyTemplate defined for entity type: ${entityType}`);
return { valid: true, errors, warnings, suggestions };
}
// Check required fields
if (template.fields) {
for (const [fieldName, fieldConfig] of Object.entries(template.fields)) {
if (fieldConfig.required && !templateData[fieldName]) {
errors.push(`Missing required field: ${fieldName}`);
// Provide specific fixes based on field
if (fieldName === 'key' && templateData.name) {
const suggestedKey = this.directTemplateValidator.generateKey(templateData.name);
suggestions.push(`Add "key": "${suggestedKey}" based on name`);
}
else if (fieldName === '_acknowledged_complexity' && template.complexity_score >= 2) {
suggestions.push(`Add "_acknowledged_complexity": true for complexity score ${template.complexity_score}`);
}
else if (fieldConfig.examples?.sample) {
suggestions.push(`Add "${fieldName}": ${JSON.stringify(fieldConfig.examples.sample)}`);
}
}
}
}
// Validate field types and formats
for (const [fieldName, value] of Object.entries(templateData)) {
const fieldConfig = template.fields?.[fieldName];
if (!fieldConfig) {
warnings.push(`Unknown field: ${fieldName}`);
continue;
}
// Type validation
if (fieldConfig.type === 'string' && typeof value !== 'string') {
errors.push(`Field ${fieldName} must be a string, got ${typeof value}`);
}
else if (fieldConfig.type === 'array' && !Array.isArray(value)) {
errors.push(`Field ${fieldName} must be an array, got ${typeof value}`);
}
}
// Platform-specific validation
if (platform) {
const platformErrors = this.validatePlatformCompatibility(entityType, platform);
errors.push(...platformErrors);
}
return {
valid: errors.length === 0,
errors,
warnings,
suggestions
};
}
/**
* Validate platform compatibility
*/
validatePlatformCompatibility(entityType, platform) {
const errors = [];
const webOnlyEntities = ['experiment', 'campaign', 'page', 'extension'];
const featureOnlyEntities = ['flag', 'rule', 'ruleset', 'variable_definition'];
if (webOnlyEntities.includes(entityType) && platform === 'feature') {
errors.push(`Entity type '${entityType}' is only available in Web Experimentation`);
}
if (featureOnlyEntities.includes(entityType) && platform === 'web') {
errors.push(`Entity type '${entityType}' is only available in Feature Experimentation`);
}
return errors;
}
/**
* Generate complete summary including all validations
*/
generateCompleteSummary(creationValidation, executionValidation, modelFriendlyValidation) {
const lines = [];
// Overall status
const overallValid = creationValidation.valid &&
(!executionValidation || executionValidation.valid) &&
(!modelFriendlyValidation || modelFriendlyValidation.valid);
if (overallValid) {
lines.push('✅ Template validation passed! Ready for execution.');
}
else {
lines.push('❌ Template validation failed! Please fix the errors below.');
}
lines.push('');
// ModelFriendly validation summary
if (modelFriendlyValidation) {
lines.push('📝 ModelFriendly Compliance:');
if (modelFriendlyValidation.valid) {
lines.push(' ✅ Template follows ModelFriendly standards');
}
else {
lines.push(` ❌ ${modelFriendlyValidation.errors.length} compliance errors found`);
modelFriendlyValidation.errors.slice(0, 2).forEach((err) => lines.push(` • ${err}`));
}
lines.push('');
}
// Creation validation summary
lines.push('📋 Template Structure Validation:');
lines.push(creationValidation.summary.split('\n').map(l => ' ' + l).join('\n'));
// Execution validation summary if provided
if (executionValidation) {
lines.push('');
lines.push('🚀 Execution Validation (with parameters):');
lines.push(executionValidation.summary.split('\n').map(l => ' ' + l).join('\n'));
}
return lines.join('\n');
}
/**
* Resolve parameters in full orchestration templates
*/
resolveOrchestrationParameters(template, parameters) {
// Deep clone the template to avoid mutating the original
const resolvedTemplate = JSON.parse(JSON.stringify(template));
// Recursively resolve parameters in the template
const resolveInObject = (obj) => {
if (typeof obj === 'string') {
// CRITICAL FIX: If the entire string is just a parameter reference, preserve type
const singleParamMatch = obj.match(/^\$\{([^}]+)\}$/);
if (singleParamMatch) {
const value = this.getNestedParameterValue(parameters, singleParamMatch[1]);
return value !== undefined ? value : obj; // Return with original type
}
// Otherwise do string interpolation
return obj.replace(/\$\{([^}]+)\}/g, (match, param) => {
const value = this.getNestedParameterValue(parameters, param);
return value !== undefined ? String(value) : match;
});
}
else if (Array.isArray(obj)) {
return obj.map(item => resolveInObject(item));
}
else if (obj && typeof obj === 'object') {
const resolved = {};
for (const [key, value] of Object.entries(obj)) {
resolved[key] = resolveInObject(value);
}
return resolved;
}
return obj;
};
// Resolve parameters in steps and other template properties
resolvedTemplate.steps = resolveInObject(resolvedTemplate.steps);
resolvedTemplate.name = resolveInObject(resolvedTemplate.name);
resolvedTemplate.description = resolveInObject(resolvedTemplate.description);
return resolvedTemplate;
}
/**
* Get nested parameter value using dot notation
*/
getNestedParameterValue(parameters, path) {
const parts = path.split('.');
let current = parameters;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
}
//# sourceMappingURL=ValidateTemplateHandler.js.map