UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

539 lines 22.4 kB
/** * Template Validator * @description Validates orchestration templates for correctness and completeness * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from '../../logging/Logger.js'; export class TemplateValidator { logger = getLogger(); /** * Validate a complete orchestration template */ async validateTemplate(template) { const errors = []; const warnings = []; this.logger.info({ templateId: template.id, templateName: template.name }, 'Validating orchestration template'); // Validate metadata this.validateMetadata(template, errors); // Validate parameters definition this.validateParametersDefinition(template.parameters, errors, warnings); // Validate steps this.validateSteps(template.steps, errors, warnings); // Validate step dependencies this.validateStepDependencies(template.steps, errors); // Validate outputs if (template.outputs) { this.validateOutputs(template.outputs, template.steps, errors, warnings); } // Validate config if (template.config) { this.validateConfig(template.config, errors, warnings); } const valid = errors.length === 0; this.logger.info({ templateId: template.id, valid, errorCount: errors.length, warningCount: warnings.length }, 'Template validation complete'); return { valid, errors, warnings }; } /** * Validate template metadata */ validateMetadata(template, errors) { if (!template.id || typeof template.id !== 'string') { errors.push('Template must have a valid id'); } if (!template.name || typeof template.name !== 'string') { errors.push('Template must have a valid name'); } if (!template.version || !this.isValidVersion(template.version)) { errors.push('Template must have a valid semantic version (e.g., 1.0.0)'); } if (!template.type || !['user', 'system'].includes(template.type)) { errors.push('Template type must be either "user" or "system"'); } if (!template.platform || !['feature', 'web', 'both'].includes(template.platform)) { errors.push('Template platform must be "feature", "web", or "both"'); } } /** * Validate semantic version string */ isValidVersion(version) { const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; return semverRegex.test(version); } /** * Validate parameters definition */ validateParametersDefinition(parameters, errors, warnings) { if (!parameters || typeof parameters !== 'object') { errors.push('Template must define parameters object'); return; } for (const [name, definition] of Object.entries(parameters)) { this.validateParameterDefinition(name, definition, errors, warnings); } } /** * Validate individual parameter definition */ validateParameterDefinition(name, definition, errors, warnings) { const validTypes = ['string', 'number', 'boolean', 'array', 'object']; if (!definition.type || !validTypes.includes(definition.type)) { errors.push(`Parameter "${name}" must have a valid type: ${validTypes.join(', ')}`); } if (!definition.description) { warnings.push(`Parameter "${name}" should have a description`); } // Validate default value matches type if (definition.default !== undefined) { const defaultType = Array.isArray(definition.default) ? 'array' : typeof definition.default; if (defaultType !== definition.type && !(definition.type === 'object' && defaultType === 'object')) { errors.push(`Parameter "${name}" default value type (${defaultType}) doesn't match declared type (${definition.type})`); } } // Validate validation rules if (definition.validation) { this.validateParameterValidation(name, definition, errors); } } /** * Validate parameter validation rules */ validateParameterValidation(name, definition, errors) { const validation = definition.validation; if (validation.pattern && definition.type !== 'string') { errors.push(`Parameter "${name}" pattern validation only applies to string type`); } if ((validation.min !== undefined || validation.max !== undefined) && definition.type !== 'number') { errors.push(`Parameter "${name}" min/max validation only applies to number type`); } if (validation.enum && validation.enum.length === 0) { errors.push(`Parameter "${name}" enum validation must have at least one value`); } } /** * Validate steps */ validateSteps(steps, errors, warnings) { if (!Array.isArray(steps) || steps.length === 0) { errors.push('Template must have at least one step'); return; } const stepIds = new Set(); for (const step of steps) { // Check for duplicate IDs if (stepIds.has(step.id)) { errors.push(`Duplicate step ID: ${step.id}`); } stepIds.add(step.id); // Validate individual step this.validateStep(step, errors, warnings); } } /** * Validate individual step */ validateStep(step, errors, warnings) { if (!step.id || typeof step.id !== 'string') { errors.push('Step must have a valid id'); } if (!step.name || typeof step.name !== 'string') { errors.push(`Step ${step.id} must have a valid name`); } const validTypes = ['template', 'conditional', 'loop', 'plugin', 'wait', 'parallel']; if (!step.type || !validTypes.includes(step.type)) { errors.push(`Step ${step.id} must have a valid type: ${validTypes.join(', ')}`); } // Validate type-specific configuration switch (step.type) { case 'template': if (!step.template) { errors.push(`Step ${step.id} of type "template" must have template configuration`); } else { this.validateTemplateStep(step.id, step.template, errors); } break; case 'conditional': if (!step.condition) { errors.push(`Step ${step.id} of type "conditional" must have condition configuration`); } else { this.validateConditionalStep(step.id, step.condition, errors, warnings); } break; case 'loop': if (!step.loop) { errors.push(`Step ${step.id} of type "loop" must have loop configuration`); } else { this.validateLoopStep(step.id, step.loop, errors, warnings); } break; case 'plugin': if (!step.plugin) { errors.push(`Step ${step.id} of type "plugin" must have plugin configuration`); } else { this.validatePluginStep(step.id, step.plugin, errors, warnings); } break; case 'wait': if (!step.wait) { errors.push(`Step ${step.id} of type "wait" must have wait configuration`); } else { this.validateWaitStep(step.id, step.wait, errors); } break; case 'parallel': if (!step.parallel) { errors.push(`Step ${step.id} of type "parallel" must have parallel configuration`); } else { this.validateParallelStep(step.id, step.parallel, errors, warnings); } break; } // Validate retry configuration if (step.retry) { this.validateRetryConfig(step.id, step.retry, errors); } // Validate error handling if (step.on_error && !['fail', 'continue', 'rollback'].includes(step.on_error)) { errors.push(`Step ${step.id} on_error must be "fail", "continue", or "rollback"`); } } /** * Validate template step configuration */ validateTemplateStep(stepId, config, errors) { // Check for new Direct Template Architecture format if (config.entity_type) { // New format validation const validOperations = ['create', 'update', 'delete', 'adopt_or_create']; if (!config.operation || !validOperations.includes(config.operation)) { errors.push(`Step ${stepId}: template operation must be one of: ${validOperations.join(', ')}`); } // Must have either entity_data or template_data if (!config.entity_data && !config.template_data && !config.inputs) { errors.push(`Step ${stepId}: template step must have entity_data, template_data, or inputs`); } } else { // Legacy format validation (deprecated) if (!config.system_template_id) { errors.push(`Step ${stepId}: template step must have entity_type (new format) or system_template_id (deprecated)`); } const validOperations = ['create', 'update', 'delete', 'get']; if (!config.operation || !validOperations.includes(config.operation)) { errors.push(`Step ${stepId}: template operation must be one of: ${validOperations.join(', ')}`); } if (!config.inputs || typeof config.inputs !== 'object') { errors.push(`Step ${stepId}: template step must have inputs object`); } } } /** * Validate conditional step configuration */ validateConditionalStep(stepId, config, errors, warnings) { if (!config.if || typeof config.if !== 'string') { errors.push(`Step ${stepId}: conditional step must have "if" expression`); } if (!config.then || !Array.isArray(config.then) || config.then.length === 0) { errors.push(`Step ${stepId}: conditional step must have non-empty "then" steps`); } else { // Recursively validate nested steps this.validateSteps(config.then, errors, warnings); } if (config.else && Array.isArray(config.else)) { // Recursively validate else steps this.validateSteps(config.else, errors, warnings); } } /** * Validate loop step configuration */ validateLoopStep(stepId, config, errors, warnings) { if (!config.over || typeof config.over !== 'string') { errors.push(`Step ${stepId}: loop step must have "over" expression`); } if (!config.iterator || typeof config.iterator !== 'string') { errors.push(`Step ${stepId}: loop step must have iterator variable name`); } if (!config.steps || !Array.isArray(config.steps) || config.steps.length === 0) { errors.push(`Step ${stepId}: loop step must have non-empty steps array`); } else { // Recursively validate nested steps this.validateSteps(config.steps, errors, warnings); } if (config.max_iterations !== undefined && config.max_iterations < 1) { errors.push(`Step ${stepId}: loop max_iterations must be at least 1`); } } /** * Validate plugin step configuration */ validatePluginStep(stepId, config, errors, warnings) { if (!config.code || typeof config.code !== 'string') { errors.push(`Step ${stepId}: plugin step must have code`); } if (config.code && config.code.trim().length === 0) { errors.push(`Step ${stepId}: plugin code cannot be empty`); } // Basic syntax check if (config.code) { try { new Function(config.code); } catch (e) { warnings.push(`Step ${stepId}: plugin code may have syntax errors: ${e instanceof Error ? e.message : String(e)}`); } } } /** * Validate wait step configuration */ validateWaitStep(stepId, config, errors) { if (!config.duration && !config.until) { errors.push(`Step ${stepId}: wait step must have either duration or until condition`); } if (config.duration !== undefined && config.duration < 0) { errors.push(`Step ${stepId}: wait duration must be positive`); } if (config.max_wait !== undefined && config.max_wait < 0) { errors.push(`Step ${stepId}: wait max_wait must be positive`); } if (config.poll_interval !== undefined && config.poll_interval < 100) { errors.push(`Step ${stepId}: wait poll_interval must be at least 100ms`); } } /** * Validate parallel step configuration */ validateParallelStep(stepId, config, errors, warnings) { if (!config.steps || !Array.isArray(config.steps) || config.steps.length === 0) { errors.push(`Step ${stepId}: parallel step must have non-empty steps array`); } else { // Recursively validate nested steps this.validateSteps(config.steps, errors, warnings); } if (config.max_concurrency !== undefined && config.max_concurrency < 1) { errors.push(`Step ${stepId}: parallel max_concurrency must be at least 1`); } } /** * Validate retry configuration */ validateRetryConfig(stepId, retry, errors) { if (retry.max_attempts !== undefined && retry.max_attempts < 1) { errors.push(`Step ${stepId}: retry max_attempts must be at least 1`); } if (retry.delay !== undefined && retry.delay < 0) { errors.push(`Step ${stepId}: retry delay must be positive`); } if (retry.backoff && !['linear', 'exponential'].includes(retry.backoff)) { errors.push(`Step ${stepId}: retry backoff must be "linear" or "exponential"`); } } /** * Validate step dependencies */ validateStepDependencies(steps, errors) { const stepIds = new Set(steps.map(s => s.id)); for (const step of steps) { if (step.depends_on) { for (const depId of step.depends_on) { if (!stepIds.has(depId)) { errors.push(`Step ${step.id} depends on non-existent step: ${depId}`); } if (depId === step.id) { errors.push(`Step ${step.id} cannot depend on itself`); } } } } } /** * Validate outputs configuration */ validateOutputs(outputs, steps, errors, warnings) { for (const [key, output] of Object.entries(outputs)) { const outputDef = output; if (!outputDef.value || typeof outputDef.value !== 'string') { errors.push(`Output "${key}" must have a value expression`); } if (!outputDef.description) { warnings.push(`Output "${key}" should have a description`); } } } /** * Validate configuration */ validateConfig(config, errors, warnings) { if (config.max_execution_time !== undefined && config.max_execution_time < 1000) { warnings.push('max_execution_time less than 1 second may be too short'); } if (config.max_retries !== undefined && config.max_retries < 0) { errors.push('max_retries must be non-negative'); } } /** * Validate parameters against template definition */ async validateParameters(parameterDefs, providedParams) { const errors = []; const warnings = []; const missingRequired = []; const invalidTypes = []; // Check required parameters for (const [name, def] of Object.entries(parameterDefs)) { if (def.required && !(name in providedParams)) { missingRequired.push(name); errors.push(`Required parameter "${name}" is missing`); } } // Validate provided parameters for (const [name, value] of Object.entries(providedParams)) { const def = parameterDefs[name]; if (!def) { warnings.push(`Unknown parameter "${name}" provided`); continue; } // Type validation const actualType = Array.isArray(value) ? 'array' : typeof value; if (actualType !== def.type && !(def.type === 'object' && actualType === 'object')) { invalidTypes.push({ param: name, expected: def.type, received: actualType }); errors.push(`Parameter "${name}" expected type ${def.type} but got ${actualType}`); } // Validation rules if (def.validation) { this.validateParameterValue(name, value, def, errors); } } const valid = errors.length === 0; return { valid, errors, warnings, missingRequired, invalidTypes }; } /** * Validate parameter value against rules */ validateParameterValue(name, value, def, errors) { const validation = def.validation; if (validation.pattern && def.type === 'string') { const regex = new RegExp(validation.pattern); if (!regex.test(value)) { errors.push(`Parameter "${name}" does not match pattern: ${validation.pattern}`); } } if (validation.min !== undefined && def.type === 'number') { if (value < validation.min) { errors.push(`Parameter "${name}" must be at least ${validation.min}`); } } if (validation.max !== undefined && def.type === 'number') { if (value > validation.max) { errors.push(`Parameter "${name}" must be at most ${validation.max}`); } } if (validation.enum && !validation.enum.includes(value)) { errors.push(`Parameter "${name}" must be one of: ${validation.enum.join(', ')}`); } if (validation.custom) { try { const customValidator = new Function('value', validation.custom); const result = customValidator(value); if (result !== true) { errors.push(`Parameter "${name}" failed custom validation${result ? ': ' + result : ''}`); } } catch (e) { errors.push(`Parameter "${name}" custom validation error: ${e instanceof Error ? e.message : String(e)}`); } } } /** * Validate orchestration step for new Direct Template Architecture */ validateOrchestrationStep(step) { const errors = []; // Required fields if (!step.id) { errors.push('Step missing required field: id'); } if (!step.entity_type) { errors.push('Step missing required field: entity_type'); } if (!step.operation) { errors.push('Step missing required field: operation'); } // Must have either entity_data or template_data if (!step.entity_data && !step.template_data) { errors.push('Step must include either entity_data or template_data'); } // Validate entity_type is known const validEntityTypes = [ 'audience', 'attribute', 'campaign', 'environment', 'event', 'experiment', 'extension', 'feature', 'flag', 'group', 'page', 'project', 'rule', 'ruleset', 'variable', 'variable_definition', 'variation', 'webhook' ]; if (step.entity_type && !validEntityTypes.includes(step.entity_type)) { errors.push(`Invalid entity_type: ${step.entity_type}`); } // Validate operations const validOperations = ['create', 'update', 'delete', 'adopt_or_create']; if (step.operation && !validOperations.includes(step.operation)) { errors.push(`Invalid operation: ${step.operation}`); } // Platform-specific validation if (step.entity_type && step.template_data) { const platformErrors = this.validatePlatformCompatibility(step.entity_type, step.template_data); errors.push(...platformErrors); } return { valid: errors.length === 0, errors, warnings: [] }; } validatePlatformCompatibility(entityType, templateData) { const errors = []; // Web-only entities const webOnlyEntities = ['experiment', 'campaign', 'page', 'extension']; // Feature-only entities const featureOnlyEntities = ['flag', 'rule', 'ruleset', 'variable_definition']; // Check if trying to create web entity in feature project if (webOnlyEntities.includes(entityType) && templateData.platform === 'feature') { errors.push(`Entity type ${entityType} is not supported in Feature Experimentation`); } // Check if trying to create feature entity in web project if (featureOnlyEntities.includes(entityType) && templateData.platform === 'web') { errors.push(`Entity type ${entityType} is not supported in Web Experimentation`); } return errors; } } //# sourceMappingURL=TemplateValidator.js.map