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