UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

280 lines 10.1 kB
/** * Schema Resolver * @description Resolves field paths from OpenAPI schema definitions * * Purpose: Parse and traverse OpenAPI schemas to build queryable field paths * for the Dynamic JSON Query Engine. This is the foundation that enables * querying ANY field in the Optimizely data model. * * Key Features: * - Loads and parses fields.generated.ts * - Recursively traverses schema definitions * - Handles references ($ref) and allOf/anyOf/oneOf * - 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'; import { schemaParser } from './SchemaParser.js'; export class SchemaResolver { logger = getLogger(); schemaCache = new Map(); pathCache = new Map(); schemas = {}; fieldsPath; constructor(fieldsPath) { this.fieldsPath = fieldsPath || join(process.cwd(), 'src', 'generated', 'fields.generated.ts'); this.logger.info(`SchemaResolver initialized with path: ${this.fieldsPath}`); } /** * Initialize by loading the generated fields file */ async initialize() { try { this.logger.info('Loading fields.generated.ts...'); // Use the SchemaParser to safely parse the TypeScript file this.schemas = schemaParser.loadSchemasFromFile(this.fieldsPath); this.logger.info(`Loaded ${Object.keys(this.schemas).length} schema definitions`); // Log some example schemas for debugging const exampleKeys = Object.keys(this.schemas).slice(0, 5); this.logger.debug(`Example schema keys: ${exampleKeys.join(', ')}`); } catch (error) { this.logger.error({ error }, 'Failed to initialize SchemaResolver'); 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 schema for this entity type const schemaKey = this.findSchemaKey(entityType); if (!schemaKey) { this.logger.warn(`No schema found for entity type: ${entityType}`); return []; } const schema = this.schemas[schemaKey]; if (!schema) { this.logger.warn(`Schema not found: ${schemaKey}`); return []; } // Recursively build paths const paths = []; await this.traverseSchema(schema, entityType, '', paths, new Set(), 0); // Cache the results this.pathCache.set(entityType, paths); this.logger.info(`Resolved ${paths.length} queryable paths for ${entityType}`); return paths; } /** * Find the schema key for an entity type */ findSchemaKey(entityType) { // Direct match if (this.schemas[entityType]) { return entityType; } // Try common variations const variations = [ entityType, `${entityType}Schema`, `${entityType}Response`, entityType.charAt(0).toUpperCase() + entityType.slice(1), `${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Schema` ]; for (const variation of variations) { if (this.schemas[variation]) { return variation; } } // Search for partial matches const keys = Object.keys(this.schemas); for (const key of keys) { if (key.toLowerCase().includes(entityType.toLowerCase())) { return key; } } return null; } /** * Recursively traverse a schema to build paths */ async traverseSchema(schema, entityType, currentPath, paths, visitedRefs, depth) { // Prevent infinite recursion if (depth > 10) { this.logger.warn(`Max depth reached at path: ${currentPath}`); return; } // Handle $ref if (schema.$ref) { const refPath = schema.$ref; if (visitedRefs.has(refPath)) { return; // Circular reference } visitedRefs.add(refPath); const resolvedSchema = this.resolveRef(refPath); if (resolvedSchema) { await this.traverseSchema(resolvedSchema, entityType, currentPath, paths, visitedRefs, depth); } visitedRefs.delete(refPath); return; } // Handle allOf/anyOf/oneOf if (schema.allOf) { for (const subSchema of schema.allOf) { await this.traverseSchema(subSchema, entityType, currentPath, paths, visitedRefs, depth); } return; } if (schema.anyOf || schema.oneOf) { const schemas = schema.anyOf || schema.oneOf; for (const subSchema of schemas) { await this.traverseSchema(subSchema, entityType, currentPath, paths, visitedRefs, depth); } return; } // Extract type information const type = schema.type || 'any'; // Add current path if it's not the root if (currentPath) { const fieldName = currentPath.split('.').pop() || currentPath; paths.push({ fullPath: currentPath, sqlPath: this.buildSqlPath(currentPath), jsonataPath: this.buildJsonataPath(currentPath), type, nullable: schema.nullable || false, description: schema.description, enum: schema.enum, example: schema.example, entityType, fieldName, depth }); } // Handle object properties if (schema.type === 'object' && schema.properties) { for (const [propName, propSchema] of Object.entries(schema.properties)) { const newPath = currentPath ? `${currentPath}.${propName}` : propName; await this.traverseSchema(propSchema, entityType, newPath, paths, visitedRefs, depth + 1); } } // Handle arrays if (schema.type === 'array' && schema.items) { const arrayPath = currentPath ? `${currentPath}[]` : '[]'; await this.traverseSchema(schema.items, entityType, arrayPath, paths, visitedRefs, depth + 1); } } /** * Resolve a $ref to its schema */ resolveRef(ref) { // Handle local references like #/components/schemas/Flag if (ref.startsWith('#/')) { const parts = ref.substring(2).split('/'); let current = { components: { schemas: this.schemas } }; for (const part of parts) { if (current && typeof current === 'object') { current = current[part]; } else { return null; } } return current; } // Handle external references (would need to load external files) this.logger.warn(`External reference not supported: ${ref}`); return null; } /** * Build SQL path for JSON_EXTRACT */ buildSqlPath(path) { // Convert dot notation to JSON path // e.g., "environments.production.enabled" -> "$.environments.production.enabled" // Handle arrays: "variations[].weight" -> "$.variations[*].weight" 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 // e.g., "variations[].weight" -> "variations.weight" 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 schemas */ getAvailableEntityTypes() { const entityTypes = new Set(); for (const key of Object.keys(this.schemas)) { // Extract entity type from schema key const match = key.match(/^(\w+?)(Schema|Response)?$/); if (match) { entityTypes.add(match[1].toLowerCase()); } } return Array.from(entityTypes); } /** * Clear all caches */ clearCache() { this.schemaCache.clear(); this.pathCache.clear(); this.logger.info('Schema caches cleared'); } } // Export singleton instance export const schemaResolver = new SchemaResolver(); //# sourceMappingURL=SchemaResolver.js.map