@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
539 lines • 22.4 kB
JavaScript
/**
* 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