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