UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

288 lines 11.2 kB
/** * 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