UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

264 lines 10.3 kB
/** * JSON Path Handler for Intelligent Query Engine * * Handles complex JSON path resolution for nested object queries * Supports paths like $.environments.production.rules[0].variations */ import { getLogger } from '../../logging/Logger.js'; const logger = getLogger(); export class JSONPathHandler { pathCache = new Map(); /** * Parse a JSON path into segments * Supports: * - $.field.subfield - property access * - $.field[0] - array index * - $.field[*] - wildcard (all array elements) * - $.field..subfield - recursive descent * - $.field[?(@.price > 10)] - filter expressions */ parseJSONPath(path) { // Check cache if (this.pathCache.has(path)) { return this.pathCache.get(path); } try { // Remove leading $ if present const normalizedPath = path.startsWith('$') ? path.substring(1) : path; if (!normalizedPath) { return { isValid: true, segments: [], sqliteExpression: '$' }; } const segments = []; let remaining = normalizedPath; while (remaining.length > 0) { // Handle recursive descent (..) if (remaining.startsWith('..')) { segments.push({ type: 'recursive', value: null }); remaining = remaining.substring(2); continue; } // Handle property access (. or start) if (remaining.startsWith('.') || segments.length === 0) { if (remaining.startsWith('.')) { remaining = remaining.substring(1); } // Extract property name const match = remaining.match(/^([^.\[\]]+)/); if (match) { segments.push({ type: 'property', value: match[1] }); remaining = remaining.substring(match[1].length); continue; } } // Handle array access [index] or [*] or [?(filter)] if (remaining.startsWith('[')) { const bracketEnd = this.findMatchingBracket(remaining); if (bracketEnd === -1) { return { isValid: false, segments: [], error: `Unclosed bracket in path: ${path}` }; } const bracketContent = remaining.substring(1, bracketEnd); remaining = remaining.substring(bracketEnd + 1); // Wildcard if (bracketContent === '*') { segments.push({ type: 'wildcard', value: null }); continue; } // Filter expression if (bracketContent.startsWith('?(') && bracketContent.endsWith(')')) { segments.push({ type: 'filter', value: null, filter: bracketContent.substring(2, bracketContent.length - 1) }); continue; } // Array index const index = parseInt(bracketContent); if (!isNaN(index)) { segments.push({ type: 'index', value: index }); continue; } return { isValid: false, segments: [], error: `Invalid bracket content: [${bracketContent}]` }; } // If we get here, something went wrong return { isValid: false, segments: [], error: `Unable to parse remaining path: ${remaining}` }; } // Convert to SQLite expression const sqliteExpression = this.convertToSQLiteExpression(segments); const result = { isValid: true, segments, sqliteExpression }; // Cache the result this.pathCache.set(path, result); return result; } catch (error) { logger.error(`Error parsing JSON path: ${error instanceof Error ? error.message : String(error)}`); return { isValid: false, segments: [], error: error instanceof Error ? error.message : String(error) }; } } /** * Find the matching closing bracket */ findMatchingBracket(str) { let depth = 0; for (let i = 0; i < str.length; i++) { if (str[i] === '[') depth++; if (str[i] === ']') { depth--; if (depth === 0) return i; } } return -1; } /** * Convert parsed segments to SQLite JSON expression */ convertToSQLiteExpression(segments) { // Always return a valid JSON path, never a bare $ if (segments.length === 0) return '$'; let expression = '$'; for (const segment of segments) { switch (segment.type) { case 'property': expression += `.${segment.value}`; break; case 'index': expression += `[${segment.value}]`; break; case 'wildcard': // SQLite doesn't support wildcard, need to handle differently expression += '[*]'; break; case 'recursive': // SQLite doesn't support recursive descent directly expression += '..**'; break; case 'filter': // SQLite doesn't support filter expressions directly expression += `[?${segment.filter}]`; break; } } return expression; } /** * Generate SQLite JSON extraction SQL */ generateJSONExtractSQL(columnName, jsonPath, alias) { const parseResult = this.parseJSONPath(jsonPath); if (!parseResult.isValid) { throw new Error(`Invalid JSON path: ${parseResult.error}`); } // Check if path contains unsupported features const hasWildcard = parseResult.segments.some(s => s.type === 'wildcard'); const hasRecursive = parseResult.segments.some(s => s.type === 'recursive'); const hasFilter = parseResult.segments.some(s => s.type === 'filter'); if (hasWildcard || hasRecursive || hasFilter) { // For complex paths, we need special handling return this.generateComplexJSONSQL(columnName, parseResult, alias); } // Simple path - use JSON_EXTRACT const sqlPath = this.convertToSimpleSQLitePath(parseResult.segments); const extraction = `JSON_EXTRACT(${columnName}, '${sqlPath}')`; return alias ? `${extraction} AS ${alias}` : extraction; } /** * Convert segments to simple SQLite JSON path */ convertToSimpleSQLitePath(segments) { let path = '$'; for (const segment of segments) { if (segment.type === 'property') { path += `.${segment.value}`; } else if (segment.type === 'index') { path += `[${segment.value}]`; } } return path; } /** * Generate SQL for complex JSON paths (wildcards, filters, etc.) */ generateComplexJSONSQL(columnName, parseResult, alias) { // For wildcards, we might need to use JSON_EACH const hasWildcard = parseResult.segments.some(s => s.type === 'wildcard'); if (hasWildcard) { // Find the path up to the wildcard const pathToWildcard = []; for (const segment of parseResult.segments) { if (segment.type === 'wildcard') break; pathToWildcard.push(segment); } const basePath = this.convertToSimpleSQLitePath(pathToWildcard); // Use JSON_EACH to expand array // This is a simplified version - real implementation would be more complex const extraction = `( SELECT JSON_GROUP_ARRAY(value) FROM JSON_EACH(JSON_EXTRACT(${columnName}, '${basePath}')) )`; return alias ? `${extraction} AS ${alias}` : extraction; } // For other complex cases, throw error for now throw new Error('Complex JSON paths with recursive descent or filters not yet supported'); } /** * Check if a field path references JSON data */ isJSONPath(fieldPath) { // Check for JSON path indicators return fieldPath.includes('.') || fieldPath.includes('[') || fieldPath.startsWith('$'); } /** * Extract the base field name from a JSON path */ getBaseFieldName(jsonPath) { const parseResult = this.parseJSONPath(jsonPath); if (!parseResult.isValid || parseResult.segments.length === 0) { return jsonPath; } const firstSegment = parseResult.segments[0]; if (firstSegment.type === 'property') { return String(firstSegment.value); } return jsonPath; } /** * Validate if a JSON path can be used in SQLite */ canUseSQLiteJSONFunctions(jsonPath) { const parseResult = this.parseJSONPath(jsonPath); if (!parseResult.isValid) return false; // SQLite JSON functions don't support wildcards, recursive descent, or filters return !parseResult.segments.some(s => s.type === 'wildcard' || s.type === 'recursive' || s.type === 'filter'); } } //# sourceMappingURL=JSONPathHandler.js.map