UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

358 lines 12.8 kB
/** * Fields Schema Resolver * @description Resolves field paths from FIELDS generated structure * * Purpose: Parse and traverse the FIELDS object to build queryable field paths * for the Dynamic JSON Query Engine. This adapts to the actual structure of * fields.generated.ts which contains field definitions rather than full schemas. * * Key Features: * - Loads and parses FIELDS from fields.generated.ts * - Builds queryable paths from field definitions * - Handles nested objects and arrays based on field types * - Generates dot-notation paths for all queryable fields * - Caches results for performance * * @author Optimizely MCP Server * @version 1.0.0 */ import { join } from 'path'; import { getLogger } from '../logging/Logger.js'; export class FieldsSchemaResolver { logger = getLogger(); pathCache = new Map(); fields = {}; fieldsPath; constructor(fieldsPath) { this.fieldsPath = fieldsPath || join(process.cwd(), 'dist', 'generated', 'fields.generated.js'); this.logger.info(`FieldsSchemaResolver initialized with path: ${this.fieldsPath}`); } /** * Initialize by loading the generated fields file */ async initialize() { try { this.logger.info('Loading fields.generated.js...'); // Import the generated fields file dynamically const fieldsModule = await import(this.fieldsPath); this.fields = fieldsModule.FIELDS || {}; this.logger.info(`Loaded ${Object.keys(this.fields).length} entity field definitions`); // Log some example entities for debugging const exampleKeys = Object.keys(this.fields).slice(0, 5); this.logger.debug(`Example entities: ${exampleKeys.join(', ')}`); } catch (error) { this.logger.error({ error }, 'Failed to initialize FieldsSchemaResolver'); throw error; } } /** * Get all queryable paths for an entity type */ async getQueryablePaths(entityType) { // Check cache first if (this.pathCache.has(entityType)) { return this.pathCache.get(entityType); } this.logger.debug(`Resolving paths for entity type: ${entityType}`); // Find the fields for this entity type const entityFields = this.fields[entityType]; if (!entityFields) { this.logger.warn(`No fields found for entity type: ${entityType}`); return []; } // Build paths from field definitions const paths = []; // Process all fields (required and optional) const allFields = [ ...(entityFields.required || []), ...(entityFields.optional || []) ]; for (const fieldName of allFields) { const fieldType = entityFields.fieldTypes?.[fieldName] || 'string'; const description = entityFields.fieldDescriptions?.[fieldName]; const enumValues = entityFields.enums?.[fieldName]; const example = entityFields.fieldExamples?.[fieldName]; const defaultValue = entityFields.defaults?.[fieldName]; // Create the resolved path const path = { fullPath: fieldName, sqlPath: `$.${fieldName}`, jsonataPath: fieldName, type: this.normalizeFieldType(fieldType), nullable: !entityFields.required?.includes(fieldName), description, enum: enumValues, example: example || defaultValue, entityType, fieldName, depth: 0 }; paths.push(path); // Handle complex types if (fieldType === 'object' || fieldType === 'any') { // Add common nested paths based on entity type and field name this.addCommonNestedPaths(paths, entityType, fieldName, 1); } else if (fieldType === 'array') { // Add array element path paths.push({ ...path, fullPath: `${fieldName}[]`, sqlPath: `$.${fieldName}[*]`, jsonataPath: fieldName, depth: 1 }); } } // Add known nested structures based on entity type this.addKnownNestedStructures(paths, entityType); // Cache the results this.pathCache.set(entityType, paths); this.logger.info(`Resolved ${paths.length} queryable paths for ${entityType}`); return paths; } /** * Normalize field type to standard types */ normalizeFieldType(fieldType) { switch (fieldType) { case 'integer': case 'number': return 'number'; case 'boolean': return 'boolean'; case 'array': return 'array'; case 'object': case 'any': return 'object'; default: return 'string'; } } /** * Add common nested paths based on entity and field */ addCommonNestedPaths(paths, entityType, fieldName, depth) { // Common patterns for nested objects const commonNestedFields = { // Flag-specific nested fields environments: ['production', 'staging', 'development'], variable_values: ['cdnVariationSettings', 'customCSS', 'customJS'], actions: ['type', 'selector', 'content'], // Experiment-specific nested fields url_targeting: ['edit_url', 'conditions', 'activation_mode'], metrics: ['event_id', 'aggregator', 'winning_direction'], traffic_allocation: ['variation_id', 'percentage'], // Common nested structures config: ['enabled', 'value', 'type'], settings: ['enabled', 'value', 'options'] }; const nestedFields = commonNestedFields[fieldName]; if (nestedFields) { for (const nestedField of nestedFields) { paths.push({ fullPath: `${fieldName}.${nestedField}`, sqlPath: `$.${fieldName}.${nestedField}`, jsonataPath: `${fieldName}.${nestedField}`, type: 'any', nullable: true, entityType, fieldName: nestedField, depth }); } } } /** * Add known nested structures based on entity type */ addKnownNestedStructures(paths, entityType) { // Add entity-specific known paths switch (entityType) { case 'flag': // Add flag-specific nested paths this.addFlagPaths(paths); break; case 'experiment': // Add experiment-specific nested paths this.addExperimentPaths(paths); break; case 'variation': // Add variation-specific nested paths this.addVariationPaths(paths); break; case 'audience': // Add audience-specific nested paths this.addAudiencePaths(paths); break; } } addFlagPaths(paths) { const flagPaths = [ 'environments.production.enabled', 'environments.staging.enabled', 'environments.development.enabled', 'variable_definitions[].key', 'variable_definitions[].type', 'variable_definitions[].default_value', 'variations[].key', 'variations[].name', 'variations[].variable_values', 'variations[].variable_values.cdnVariationSettings' ]; for (const path of flagPaths) { paths.push({ fullPath: path, sqlPath: this.buildSqlPath(path), jsonataPath: this.buildJsonataPath(path), type: 'any', nullable: true, entityType: 'flag', fieldName: path.split('.').pop() || path, depth: path.split('.').length - 1 }); } } addExperimentPaths(paths) { const experimentPaths = [ 'variations[].key', 'variations[].name', 'variations[].weight', 'variations[].actions', 'metrics[].event_id', 'metrics[].aggregator', 'audience_conditions.audiences', 'audience_conditions.audience_ids', 'url_targeting.edit_url', 'url_targeting.conditions' ]; for (const path of experimentPaths) { paths.push({ fullPath: path, sqlPath: this.buildSqlPath(path), jsonataPath: this.buildJsonataPath(path), type: 'any', nullable: true, entityType: 'experiment', fieldName: path.split('.').pop() || path, depth: path.split('.').length - 1 }); } } addVariationPaths(paths) { const variationPaths = [ 'variable_values.cdnVariationSettings', 'variable_values.customCSS', 'variable_values.customJS', 'actions[].type', 'actions[].selector', 'actions[].content' ]; for (const path of variationPaths) { paths.push({ fullPath: path, sqlPath: this.buildSqlPath(path), jsonataPath: this.buildJsonataPath(path), type: 'any', nullable: true, entityType: 'variation', fieldName: path.split('.').pop() || path, depth: path.split('.').length - 1 }); } } addAudiencePaths(paths) { const audiencePaths = [ 'conditions.and', 'conditions.or', 'conditions.not', 'conditions.audience_id', 'conditions.name', 'conditions.type', 'conditions.value', 'conditions.match_type' ]; for (const path of audiencePaths) { paths.push({ fullPath: path, sqlPath: this.buildSqlPath(path), jsonataPath: this.buildJsonataPath(path), type: 'any', nullable: true, entityType: 'audience', fieldName: path.split('.').pop() || path, depth: path.split('.').length - 1 }); } } /** * Build SQL path for JSON_EXTRACT */ buildSqlPath(path) { let sqlPath = '$'; const parts = path.split('.'); for (const part of parts) { if (part.endsWith('[]')) { // Array notation const fieldName = part.substring(0, part.length - 2); sqlPath += `.${fieldName}[*]`; } else { sqlPath += `.${part}`; } } return sqlPath; } /** * Build JSONata path */ buildJsonataPath(path) { // JSONata uses different syntax for arrays return path.replace(/\[\]/g, ''); } /** * Get field type information */ async getFieldType(entityType, fieldPath) { const paths = await this.getQueryablePaths(entityType); const resolved = paths.find(p => p.fullPath === fieldPath); if (!resolved) { return null; } return { path: resolved.fullPath, type: resolved.type, description: resolved.description, enum: resolved.enum, nullable: resolved.nullable, example: resolved.example }; } /** * Search for fields matching a pattern */ async searchFields(entityType, pattern) { const paths = await this.getQueryablePaths(entityType); const regex = new RegExp(pattern, 'i'); return paths.filter(path => regex.test(path.fullPath) || (path.description && regex.test(path.description))); } /** * Get all entity types with fields */ getAvailableEntityTypes() { return Object.keys(this.fields); } /** * Clear all caches */ clearCache() { this.pathCache.clear(); this.logger.info('Field path caches cleared'); } } // Export singleton instance export const fieldsSchemaResolver = new FieldsSchemaResolver(); //# sourceMappingURL=FieldsSchemaResolver.js.map