@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
320 lines • 13.9 kB
JavaScript
/**
* Comprehensive Schema Validator
*
* This validator uses fields.generated.ts to validate entity data against
* actual API schemas, catching issues before they reach the API.
*/
import { FIELDS } from '../generated/fields.generated.js';
import { getLogger } from '../logging/Logger.js';
import { SchemaCache } from './SchemaCache.js';
export class ComprehensiveSchemaValidator {
logger = getLogger();
schemaCache;
constructor(schemaCache) {
// Use provided cache or create a new one
this.schemaCache = schemaCache || new SchemaCache({
ttl: 3600000, // 1 hour
maxSize: 50,
preloadAll: true
});
}
/**
* Validate entity data against API schema from fields.generated.ts
*/
validateEntity(entityType, data, options) {
const errors = [];
const warnings = [];
const autoFixed = {};
// Get schema from cache
const schema = this.schemaCache.get(entityType);
if (!schema) {
errors.push(`Unknown entity type: ${entityType}. Valid types: ${Object.keys(FIELDS).join(', ')}`);
return { valid: false, errors, warnings };
}
this.logger.debug({
entityType,
dataFields: Object.keys(data),
schemaFields: {
required: schema.required,
optional: schema.optional,
enums: Object.keys(schema.enums || {})
}
}, 'Validating entity against schema');
// Define server-generated fields that should not be required for CREATE operations
const serverGeneratedFields = new Set([
'id', 'urn', 'created_time', 'updated_time', 'url',
'created_by_user_email', 'updated_by_user_email',
'revision', 'version'
]);
// 1. Check required fields
const requiredFields = options?.operation === 'create'
? schema.required.filter(field => !serverGeneratedFields.has(field))
: schema.required;
for (const field of requiredFields) {
if (!(field in data) || data[field] === undefined || data[field] === null || data[field] === '') {
// Special handling for fields with defaults
if (schema.defaults && typeof schema.defaults === 'object' && field in schema.defaults) {
if (options?.autoFix) {
autoFixed[field] = schema.defaults[field];
warnings.push(`Missing required field '${field}' - auto-filled with default: ${JSON.stringify(schema.defaults[field])}`);
}
else {
errors.push(`Missing required field '${field}'. This field is required by the API.`);
}
}
else {
errors.push(`Missing required field '${field}'. This field is required by the API.`);
}
}
}
// 2. Check enum values
if (schema.enums) {
for (const [field, enumValues] of Object.entries(schema.enums)) {
if (field in data && data[field] !== undefined) {
const value = data[field];
if (!enumValues.includes(value)) {
// Special handling for common mistakes
const suggestion = this.getSuggestionForInvalidEnum(entityType, field, value, enumValues);
errors.push(`Invalid ${field}: '${value}'. ` +
`Valid values are: ${enumValues.join(', ')}. ` +
(suggestion ? `${suggestion}` : ''));
}
}
}
}
// 3. Check field types
if (schema.fieldTypes) {
for (const [field, expectedType] of Object.entries(schema.fieldTypes)) {
if (field in data && data[field] !== undefined) {
const value = data[field];
// Skip type validation for template parameters
if (typeof value === 'string' && /\$\{[^}]+\}/.test(value)) {
warnings.push(`Field '${field}' contains template parameter '${value}' - type validation will be performed at execution time`);
continue;
}
const actualType = Array.isArray(value) ? 'array' : typeof value;
// Allow some type flexibility
const typeMatches = this.checkTypeCompatibility(actualType, expectedType);
if (!typeMatches) {
errors.push(`Invalid type for '${field}': expected ${expectedType}, got ${actualType}. ` +
`Value: ${JSON.stringify(value)}`);
}
}
}
}
// 4. Check validation constraints
if (schema.validation) {
// Pattern validation
if (schema.validation.pattern) {
for (const [field, pattern] of Object.entries(schema.validation.pattern)) {
if (field in data && data[field] !== undefined) {
const value = data[field];
// Skip pattern validation for template parameters
if (typeof value === 'string' && /\$\{[^}]+\}/.test(value)) {
warnings.push(`Field '${field}' contains template parameter '${value}' - pattern validation will be performed at execution time`);
continue;
}
const regex = new RegExp(pattern);
if (!regex.test(String(value))) {
errors.push(`Field '${field}' doesn't match required pattern: ${pattern}`);
}
}
}
}
// Min/max validation
if (schema.validation.minimum) {
for (const [field, min] of Object.entries(schema.validation.minimum)) {
if (field in data && data[field] !== undefined && data[field] < min) {
errors.push(`Field '${field}' must be at least ${min}, got ${data[field]}`);
}
}
}
if (schema.validation.maximum) {
for (const [field, max] of Object.entries(schema.validation.maximum)) {
if (field in data && data[field] !== undefined && data[field] > max) {
errors.push(`Field '${field}' must be at most ${max}, got ${data[field]}`);
}
}
}
// String length validation
if (schema.validation.minLength) {
for (const [field, minLen] of Object.entries(schema.validation.minLength)) {
if (field in data && data[field] !== undefined) {
const length = String(data[field]).length;
const minLength = Number(minLen);
if (length < minLength) {
errors.push(`Field '${field}' must be at least ${minLength} characters, got ${length}`);
}
}
}
}
if (schema.validation.maxLength) {
for (const [field, maxLen] of Object.entries(schema.validation.maxLength)) {
if (field in data && data[field] !== undefined) {
const length = String(data[field]).length;
const maxLength = Number(maxLen);
if (length > maxLength) {
errors.push(`Field '${field}' must be at most ${maxLength} characters, got ${length}`);
}
}
}
}
}
// 5. Check for unknown fields
const knownFields = new Set([
...(schema.required || []),
...(schema.optional || [])
]);
for (const field of Object.keys(data)) {
if (!knownFields.has(field)) {
warnings.push(`Unknown field '${field}' - this field may be ignored by the API`);
}
}
// 6. Platform-specific validation
if (options?.platform) {
this.validatePlatformSpecific(entityType, data, options.platform, errors, warnings);
}
return {
valid: errors.length === 0,
errors,
warnings,
...(Object.keys(autoFixed).length > 0 && { autoFixed })
};
}
/**
* Get suggestion for common enum mistakes
*/
getSuggestionForInvalidEnum(entityType, field, value, validValues) {
// Event category specific suggestions
if (entityType === 'event' && field === 'category') {
if (value === 'conversion') {
return "Did you mean 'convert'? 'conversion' is not a valid category.";
}
if (value === 'click' || value === 'pageview') {
return `For '${value}' events, use category: 'other' and event_type: '${value}'.`;
}
}
// Project platform suggestions
if (entityType === 'project' && field === 'platform') {
if (value === 'feature_experimentation') {
return "Use 'custom' for Feature Experimentation projects, not 'feature_experimentation'.";
}
if (value === 'web_experimentation') {
return "Use 'web' for Web Experimentation projects, not 'web_experimentation'.";
}
}
// Generic case-insensitive match
const lowerValue = String(value).toLowerCase();
const caseInsensitiveMatch = validValues.find(v => String(v).toLowerCase() === lowerValue);
if (caseInsensitiveMatch && caseInsensitiveMatch !== value) {
return `Did you mean '${caseInsensitiveMatch}'? (case-sensitive)`;
}
return null;
}
/**
* Check if types are compatible (allowing some flexibility)
*/
checkTypeCompatibility(actual, expected) {
// Exact match
if (actual === expected)
return true;
// Allow number/integer interchangeability
if ((actual === 'number' && expected === 'integer') ||
(actual === 'integer' && expected === 'number')) {
return true;
}
// Allow 'any' type
if (expected === 'any')
return true;
// String can accept numbers (will be converted)
if (expected === 'string' && (actual === 'number' || actual === 'boolean')) {
return true;
}
return false;
}
/**
* Platform-specific validation rules
*/
validatePlatformSpecific(entityType, data, platform, errors, warnings) {
// Web-only entities
const webOnlyEntities = ['experiment', 'campaign', 'page', 'extension'];
if (webOnlyEntities.includes(entityType) && platform === 'feature') {
errors.push(`Entity type '${entityType}' is only available on Web Experimentation platform`);
}
// Feature-only entities
const featureOnlyEntities = ['flag', 'ruleset', 'rule', 'variable_definition'];
if (featureOnlyEntities.includes(entityType) && platform === 'web') {
errors.push(`Entity type '${entityType}' is only available on Feature Experimentation platform`);
}
// Experiment metrics scope validation
if (entityType === 'experiment' && data.metrics && Array.isArray(data.metrics)) {
data.metrics.forEach((metric, index) => {
if (metric.scope && metric.scope !== 'visitor') {
errors.push(`Experiment metric[${index}] has invalid scope '${metric.scope}'. ` +
`Experiments require scope: 'visitor'. Campaigns use scope: 'session'.`);
}
});
}
// Campaign metrics scope validation
if (entityType === 'campaign' && data.metrics && Array.isArray(data.metrics)) {
data.metrics.forEach((metric, index) => {
if (metric.scope && metric.scope !== 'session') {
warnings.push(`Campaign metric[${index}] has scope '${metric.scope}'. ` +
`Campaigns typically use scope: 'session' (not 'visitor').`);
}
});
}
}
/**
* Batch validate multiple entities
*/
validateEntities(entities, options) {
const results = new Map();
let allValid = true;
for (const entity of entities) {
const id = entity.id || `${entity.entityType}_${Date.now()}`;
const result = this.validateEntity(entity.entityType, entity.data, options);
results.set(id, result);
if (!result.valid) {
allValid = false;
}
}
return { allValid, results };
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.schemaCache.getStats();
}
/**
* Clear schema cache
*/
clearCache(entityType) {
if (entityType) {
this.schemaCache.clear(entityType);
}
else {
this.schemaCache.clearAll();
}
}
/**
* Refresh schema in cache
*/
refreshSchema(entityType) {
this.schemaCache.refresh(entityType);
}
/**
* Get cached entity types
*/
getCachedTypes() {
return this.schemaCache.getCachedTypes();
}
/**
* Clean up expired cache entries
*/
cleanupCache() {
return this.schemaCache.cleanup();
}
}
//# sourceMappingURL=ComprehensiveSchemaValidator.js.map