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