@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
369 lines • 13.5 kB
JavaScript
/**
* Recursive Path Builder
* @description Builds comprehensive field paths by recursively analyzing database content
*
* Purpose: Discover nested JSON structures by analyzing actual data in the database
* to find all queryable paths, including deeply nested and dynamic structures.
*
* Key Features:
* - Analyzes actual JSON data from database
* - Discovers dynamic field names and array structures
* - Builds comprehensive path index
* - Handles complex nested objects
* - Merges paths from multiple records
* - Provides path statistics and usage
*
* @author Optimizely MCP Server
* @version 1.0.0
*/
import { getLogger } from '../logging/Logger.js';
export class RecursivePathBuilder {
logger = getLogger();
database;
maxDepth = 10;
maxExamples = 3;
constructor(database) {
this.database = database;
this.logger.info('RecursivePathBuilder initialized');
}
/**
* Analyze all records of an entity type to discover paths
*/
async analyzeEntity(entityType, limit = 100) {
this.logger.info(`Analyzing entity type: ${entityType} (limit: ${limit})`);
try {
// Get sample records from database
const records = await this.getSampleRecords(entityType, limit);
if (!records || records.length === 0) {
this.logger.warn(`No records found for entity type: ${entityType}`);
return {
entityType,
totalRecords: 0,
discoveredPaths: [],
complexityScore: 0,
recommendations: ['No data available for analysis']
};
}
// Build paths from all records
const pathMap = new Map();
for (const record of records) {
const recordPaths = this.buildPathsFromObject(record.data_json || {}, entityType);
// Merge paths into map
for (const path of recordPaths) {
const existing = pathMap.get(path.path);
if (existing) {
existing.occurrences++;
if (existing.examples.length < this.maxExamples && !existing.examples.includes(path.examples[0])) {
existing.examples.push(path.examples[0]);
}
existing.isNullable = existing.isNullable || path.isNullable;
}
else {
pathMap.set(path.path, path);
}
}
}
// Convert to array and calculate statistics
const discoveredPaths = Array.from(pathMap.values());
const complexityScore = this.calculateComplexityScore(discoveredPaths);
const recommendations = this.generateRecommendations(discoveredPaths, entityType);
this.logger.info(`Discovered ${discoveredPaths.length} unique paths for ${entityType}`);
return {
entityType,
totalRecords: records.length,
discoveredPaths,
complexityScore,
recommendations
};
}
catch (error) {
this.logger.error({ error, entityType }, 'Failed to analyze entity');
throw error;
}
}
/**
* Get sample records from database
*/
async getSampleRecords(entityType, limit) {
const tableName = this.getTableName(entityType);
try {
const sql = `
SELECT id, key, name, data_json
FROM ${tableName}
WHERE data_json IS NOT NULL
LIMIT ?
`;
const result = await this.database.query(sql, [limit]);
return result.rows || [];
}
catch (error) {
this.logger.error({ error, tableName }, 'Failed to get sample records');
return [];
}
}
/**
* Recursively build paths from an object
*/
buildPathsFromObject(obj, entityType, currentPath = '', depth = 0, parentPath) {
const paths = [];
if (depth > this.maxDepth) {
return paths;
}
// Handle null/undefined
if (obj === null || obj === undefined) {
if (currentPath) {
paths.push({
path: currentPath,
type: 'null',
occurrences: 1,
examples: [null],
isArray: false,
isNullable: true,
depth,
entityType,
parentPath
});
}
return paths;
}
// Handle arrays
if (Array.isArray(obj)) {
if (currentPath) {
paths.push({
path: currentPath,
type: 'array',
occurrences: 1,
examples: [obj.length],
isArray: true,
isNullable: false,
depth,
entityType,
parentPath
});
}
// Analyze array elements
if (obj.length > 0) {
// Sample first few elements
const sampleSize = Math.min(obj.length, 5);
for (let i = 0; i < sampleSize; i++) {
const elementPath = currentPath ? `${currentPath}[]` : '[]';
const elementPaths = this.buildPathsFromObject(obj[i], entityType, elementPath, depth + 1, currentPath);
paths.push(...elementPaths);
}
}
return paths;
}
// Handle objects
if (typeof obj === 'object') {
if (currentPath) {
paths.push({
path: currentPath,
type: 'object',
occurrences: 1,
examples: [Object.keys(obj).length],
isArray: false,
isNullable: false,
depth,
entityType,
parentPath,
childPaths: []
});
}
// Process each property
for (const [key, value] of Object.entries(obj)) {
const newPath = currentPath ? `${currentPath}.${key}` : key;
const childPaths = this.buildPathsFromObject(value, entityType, newPath, depth + 1, currentPath);
paths.push(...childPaths);
// Track child paths
const parentPathInfo = paths.find(p => p.path === currentPath);
if (parentPathInfo && parentPathInfo.childPaths) {
parentPathInfo.childPaths.push(newPath);
}
}
return paths;
}
// Handle primitive values
if (currentPath) {
const type = typeof obj;
paths.push({
path: currentPath,
type,
occurrences: 1,
examples: [obj],
isArray: false,
isNullable: false,
depth,
entityType,
parentPath
});
}
return paths;
}
/**
* Calculate complexity score for discovered paths
*/
calculateComplexityScore(paths) {
let score = 0;
// Factor in total number of paths
score += Math.min(paths.length / 10, 10);
// Factor in depth
const maxDepth = Math.max(...paths.map(p => p.depth), 0);
score += Math.min(maxDepth * 2, 10);
// Factor in array nesting
const arrayPaths = paths.filter(p => p.isArray).length;
score += Math.min(arrayPaths, 10);
// Factor in nullable fields
const nullablePaths = paths.filter(p => p.isNullable).length;
score += Math.min(nullablePaths / 5, 5);
return Math.round(score);
}
/**
* Generate recommendations based on discovered paths
*/
generateRecommendations(paths, entityType) {
const recommendations = [];
// Check for deeply nested structures
const deepPaths = paths.filter(p => p.depth > 5);
if (deepPaths.length > 0) {
recommendations.push(`Found ${deepPaths.length} deeply nested paths (depth > 5). Consider using JSONata for complex queries.`);
}
// Check for array-heavy structures
const arrayPaths = paths.filter(p => p.isArray);
if (arrayPaths.length > paths.length * 0.3) {
recommendations.push(`High array usage detected (${arrayPaths.length} arrays). Use array operators in queries.`);
}
// Check for nullable fields
const nullablePaths = paths.filter(p => p.isNullable);
if (nullablePaths.length > paths.length * 0.5) {
recommendations.push(`Many nullable fields (${nullablePaths.length}). Add null checks in queries.`);
}
// Suggest common query patterns
const commonPatterns = this.findCommonPatterns(paths);
if (commonPatterns.length > 0) {
recommendations.push(`Common patterns found: ${commonPatterns.join(', ')}`);
}
return recommendations;
}
/**
* Find common patterns in paths
*/
findCommonPatterns(paths) {
const patterns = [];
// Check for common prefixes
const prefixCounts = new Map();
for (const path of paths) {
const parts = path.path.split('.');
if (parts.length > 1) {
const prefix = parts[0];
prefixCounts.set(prefix, (prefixCounts.get(prefix) || 0) + 1);
}
}
// Find dominant prefixes
for (const [prefix, count] of prefixCounts) {
if (count > paths.length * 0.2) {
patterns.push(`${prefix}.*`);
}
}
return patterns;
}
/**
* Get table name for entity type
*/
getTableName(entityType) {
const tableMap = {
'flag': 'flags',
'flags': 'flags',
'experiment': 'experiments',
'experiments': 'experiments',
'variation': 'variations',
'variations': 'variations',
'event': 'events',
'events': 'events',
'audience': 'audiences',
'audiences': 'audiences',
'page': 'pages',
'pages': 'pages',
'project': 'projects',
'projects': 'projects',
'attribute': 'attributes',
'attributes': 'attributes',
'campaign': 'campaigns',
'campaigns': 'campaigns',
'extension': 'extensions',
'extensions': 'extensions',
'group': 'groups',
'groups': 'groups',
'webhook': 'webhooks',
'webhooks': 'webhooks'
};
return tableMap[entityType.toLowerCase()] || entityType + 's';
}
/**
* Merge paths from schema and database analysis
*/
async mergePaths(schemaPaths, databasePaths) {
const mergedMap = new Map();
// Add schema paths first
for (const schemaPath of schemaPaths) {
mergedMap.set(schemaPath.fullPath, {
...schemaPath,
source: 'schema',
inDatabase: false,
occurrences: 0
});
}
// Merge database paths
for (const dbPath of databasePaths) {
const existing = mergedMap.get(dbPath.path);
if (existing) {
existing.inDatabase = true;
existing.occurrences = dbPath.occurrences;
existing.examples = dbPath.examples;
existing.isNullable = existing.isNullable || dbPath.isNullable;
}
else {
mergedMap.set(dbPath.path, {
fullPath: dbPath.path,
sqlPath: this.buildSqlPath(dbPath.path),
jsonataPath: this.buildJsonataPath(dbPath.path),
type: dbPath.type,
nullable: dbPath.isNullable,
entityType: dbPath.entityType,
fieldName: dbPath.path.split('.').pop() || dbPath.path,
depth: dbPath.depth,
source: 'database',
inDatabase: true,
occurrences: dbPath.occurrences,
examples: dbPath.examples
});
}
}
return Array.from(mergedMap.values());
}
/**
* Build SQL path for JSON_EXTRACT
*/
buildSqlPath(path) {
let sqlPath = '$';
const parts = path.split('.');
for (const part of parts) {
if (part.endsWith('[]')) {
const fieldName = part.substring(0, part.length - 2);
sqlPath += `.${fieldName}[*]`;
}
else {
sqlPath += `.${part}`;
}
}
return sqlPath;
}
/**
* Build JSONata path
*/
buildJsonataPath(path) {
return path.replace(/\[\]/g, '');
}
}
// Export singleton instance
export const recursivePathBuilder = (database) => new RecursivePathBuilder(database);
//# sourceMappingURL=RecursivePathBuilder.js.map