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