UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

432 lines 16.7 kB
/** * Field Catalog - Universal Field Resolution System * * The Field Catalog is responsible for discovering and resolving field locations * across different data models. It maintains a registry of all known fields and * their physical locations (SQL columns, JSON paths, related tables, etc.). */ import { LRUCache } from 'lru-cache'; import { getLogger } from '../../logging/Logger.js'; import { ViewOnlyFieldResolver } from '../robust-parser/ViewOnlyFieldResolver.js'; const logger = getLogger(); /** * Field Catalog implementation */ export class FieldCatalog { config; catalog; discoveryCache; adapters; discoveryPromises; lastDiscovery; fieldResolver; constructor(config = {}) { this.config = config; this.catalog = new Map(); this.adapters = new Map(); this.discoveryPromises = new Map(); this.lastDiscovery = new Map(); // Initialize view-only field resolver this.fieldResolver = new ViewOnlyFieldResolver(); // Initialize LRU cache for discovered fields this.discoveryCache = new LRUCache({ max: config.cacheSize || 1000, ttl: config.cacheTTL || 3600000 // 1 hour default }); logger.info(`FieldCatalog initialized with cacheSize: ${config.cacheSize || 1000}, cacheTTL: ${config.cacheTTL || 3600000}, autoDiscovery: ${config.autoDiscovery !== false}`); } /** * Register a data model adapter */ registerAdapter(adapter) { this.adapters.set(adapter.name, adapter); logger.info(`Registered adapter: ${adapter.name}`); // Trigger initial discovery if auto-discovery is enabled if (this.config.autoDiscovery !== false) { this.discoverAllFields(adapter).catch(error => { logger.error(`Failed to discover fields for adapter ${adapter.name}: ${error}`); }); } } /** * Resolve a field to its physical location */ async resolveField(entity, field) { const key = `${entity}.${field}`; // Check catalog first if (this.catalog.has(key)) { logger.debug(`Field ${key} found in catalog`); return this.catalog.get(key); } // Check discovery cache const cached = this.discoveryCache.get(key); if (cached) { logger.debug(`Field ${key} found in cache`); return cached; } // Check if discovery is already in progress if (this.discoveryPromises.has(key)) { logger.debug(`Waiting for in-progress discovery of ${key}`); return this.discoveryPromises.get(key); } // CRITICAL FIX: Check ViewOnlyFieldResolver for known field mappings try { logger.debug(`FieldCatalog.resolveField: Checking ViewOnlyFieldResolver for ${key}`); const resolution = this.fieldResolver.resolve(field); logger.debug(`FieldCatalog.resolveField: ViewOnlyFieldResolver resolved ${key} to view: ${resolution.viewName}`); logger.debug(`Field ${key} resolved via ViewOnlyFieldResolver: ${resolution.viewName}.${resolution.columnName}`); // Convert ViewOnlyFieldResolver result to FieldLocation format const location = { entity: entity, physicalLocation: { type: resolution.isPreComputed ? 'computed' : 'column', path: resolution.columnName, // Views are pre-joined, so we just reference the column }, indexed: false, cacheable: true, estimatedCost: 1 // Views have minimal cost }; // Cache the resolved location this.discoveryCache.set(key, location); return location; } catch (error) { logger.debug(`FieldCatalog.resolveField: ViewOnlyFieldResolver failed for ${key}: ${error instanceof Error ? error.message : String(error)}`); logger.debug(`ViewOnlyFieldResolver failed for ${key}: ${error instanceof Error ? error.message : String(error)}`); // Continue to auto-discovery } // Auto-discover if enabled if (this.config.autoDiscovery !== false) { logger.debug(`Auto-discovering field ${key}`); const discoveryPromise = this.discoverField(entity, field); this.discoveryPromises.set(key, discoveryPromise); try { const location = await discoveryPromise; this.discoveryPromises.delete(key); return location; } catch (error) { this.discoveryPromises.delete(key); throw error; } } // Field not found throw new FieldResolutionError(field, entity, { availableFields: await this.getAvailableFields(entity) }); } /** * Get field information if it exists (non-throwing version) */ getFieldInfo(fieldPath) { // Handle entity.field notation if (this.catalog.has(fieldPath)) { return this.catalog.get(fieldPath) || null; } // Also check discovery cache if (this.discoveryCache.has(fieldPath)) { return this.discoveryCache.get(fieldPath) || null; } // CRITICAL FIX: Check FieldLocalityResolver for known field mappings try { // Parse entity.field format const parts = fieldPath.split('.'); if (parts.length === 2) { const [entity, field] = parts; // Create a basic query intent for field resolution const basicQueryIntent = { type: 'list', confidence: 0.8, reasoning: ['Field resolution fallback'] }; try { const resolution = this.fieldResolver.resolve(field); // Convert ViewOnlyFieldResolver result to FieldLocation format const location = { entity: entity, physicalLocation: { type: resolution.isPreComputed ? 'computed' : 'column', path: resolution.columnName, }, indexed: false, cacheable: true, estimatedCost: 1 // Views have minimal cost }; // Cache the resolved location for future use this.discoveryCache.set(fieldPath, location); return location; } catch (error) { // Field not in views, return null } } } catch (error) { // Continue to return null } return null; } /** * Manually register a field location */ registerField(entity, field, location) { const key = `${entity}.${field}`; this.catalog.set(key, location); this.discoveryCache.set(key, location); logger.debug(`Registered field ${key} at ${location.physicalLocation.type}:${location.physicalLocation.path}`); } /** * Discover all fields for an adapter */ async discoverAllFields(adapter) { logger.info(`Starting field discovery for adapter: ${adapter.name}`); try { // Discover entities const entities = await adapter.discoverEntities(); logger.info(`Discovered ${entities.length} entities`); // Discover fields for each entity for (const entity of entities) { await this.discoverEntityFields(adapter, entity); } // Discover relationships const relationships = await adapter.discoverRelationships(); this.processRelationships(relationships); logger.info(`Field discovery complete for adapter: ${adapter.name}`); } catch (error) { logger.error(`Field discovery failed for adapter ${adapter.name}: ${error}`); throw error; } } /** * Discover fields for a specific entity */ async discoverEntityFields(adapter, entity) { try { const fields = await adapter.discoverFields(entity.name); logger.debug(`Discovered ${fields.length} fields for entity ${entity.name}`); for (const field of fields) { const location = this.createFieldLocation(entity, field); this.registerField(entity.name, field.name, location); } } catch (error) { logger.error(`Failed to discover fields for entity ${entity.name}: ${error}`); } } /** * Create field location from field definition */ createFieldLocation(entity, field) { // Handle virtual fields with join requirement if (field.metadata?.isVirtual && field.metadata?.requiresJoin) { return { entity: entity.name, physicalLocation: { type: 'related', path: field.metadata.joinField || field.name, relationship: { from: { entity: entity.name, field: entity.name === 'flags' || entity.name === 'flag' ? 'key' : 'id' }, to: { entity: field.metadata.requiresJoin, field: entity.name === 'flags' || entity.name === 'flag' ? 'flag_key' : `${entity.name}_id` }, type: 'one-to-many', nullable: true }, requiresJoin: true }, indexed: false, cacheable: false, estimatedCost: 20 }; } // Handle JSON fields if (field.type === 'json' && field.jsonPath) { return { entity: entity.name, physicalLocation: { type: 'json_path', path: field.name, jsonPath: field.jsonPath, jsonExtractFunction: this.getJSONExtractFunction(entity.type) }, indexed: false, cacheable: true, estimatedCost: 5 }; } // Handle computed fields if (field.computed && field.computeExpression) { return { entity: entity.name, physicalLocation: { type: 'computed', path: field.computeExpression }, indexed: false, cacheable: false, estimatedCost: 10 }; } // Default to column return { entity: entity.name, physicalLocation: { type: 'column', path: field.name }, indexed: field.metadata?.indexed || false, cacheable: true, estimatedCost: 1 }; } /** * Process discovered relationships */ processRelationships(relationships) { for (const [entity, relations] of Object.entries(relationships)) { for (const relation of relations) { // Create synthetic field for relationship navigation const fieldName = `${relation.to.entity}_via_${relation.from.field}`; const location = { entity: entity, physicalLocation: { type: 'related', path: relation.to.field, relationship: relation, requiresJoin: true }, indexed: false, cacheable: false, estimatedCost: 20 }; this.registerField(entity, fieldName, location); } } } /** * Auto-discover a single field */ async discoverField(entity, field) { logger.debug(`Discovering field ${entity}.${field}`); // Try each adapter for (const [name, adapter] of this.adapters) { try { // Check if adapter knows about this entity const entities = await adapter.discoverEntities(); const entityDef = entities.find(e => e.name === entity); if (!entityDef) continue; // Try to discover the specific field const fields = await adapter.discoverFields(entity); const fieldDef = fields.find(f => f.name === field); if (fieldDef) { const location = this.createFieldLocation(entityDef, fieldDef); this.discoveryCache.set(`${entity}.${field}`, location); return location; } // Check if it's a relationship field const relationships = await adapter.discoverRelationships(); if (relationships[entity]) { for (const relation of relationships[entity]) { const syntheticName = `${relation.to.entity}_via_${relation.from.field}`; if (field === syntheticName || field === relation.to.entity) { const location = { entity: entity, physicalLocation: { type: 'related', path: relation.to.field, relationship: relation, requiresJoin: true }, indexed: false, cacheable: false, estimatedCost: 20 }; this.discoveryCache.set(`${entity}.${field}`, location); return location; } } } } catch (error) { logger.warn(`Adapter ${name} failed to discover field ${entity}.${field}: ${error}`); } } throw new FieldResolutionError(field, entity, { attemptedAdapters: Array.from(this.adapters.keys()) }); } /** * Get available fields for an entity */ async getAvailableFields(entity) { const fields = []; // From catalog for (const [key, _] of this.catalog) { if (key.startsWith(`${entity}.`)) { fields.push(key.substring(entity.length + 1)); } } // From cache - LRU v5 doesn't have entries(), use keys() Array.from(this.discoveryCache.keys()).forEach((key) => { if (key.startsWith(`${entity}.`)) { const field = key.substring(entity.length + 1); if (!fields.includes(field)) { fields.push(field); } } }); return fields; } /** * Get appropriate JSON extract function for entity type */ getJSONExtractFunction(entityType) { switch (entityType) { case 'table': return 'JSON_EXTRACT'; case 'collection': return 'JSON_EXTRACT'; default: return 'JSON_EXTRACT'; } } /** * Clear all cached data */ clearCache() { this.discoveryCache.clear(); this.lastDiscovery.clear(); logger.info('Field catalog cache cleared'); } /** * Get catalog statistics */ getStatistics() { return { catalogSize: this.catalog.size, cacheSize: this.discoveryCache.size, adapters: Array.from(this.adapters.keys()) }; } } // Re-export the custom error class export class FieldResolutionError extends Error { field; entity; details; constructor(field, entity, details) { super(`Cannot resolve field '${field}' in entity '${entity}'`); this.field = field; this.entity = entity; this.details = details; this.name = 'FieldResolutionError'; } } //# sourceMappingURL=FieldCatalog.js.map