UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

370 lines 17.7 kB
/** * 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