UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

369 lines 13.5 kB
/** * 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