UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

689 lines 29.3 kB
/** * JSONataProcessor - Handles complex JSON transformations using JSONata */ import jsonata from 'jsonata'; export class JSONataProcessor { expressions = new Map(); // Pre-compiled common patterns for performance COMMON_PATTERNS = { // Flag patterns flagsWithVariables: '$[variable_definitions != {}]', flagsWithSpecificVariable: (varName) => `$[variable_definitions.${varName} exists]`, flagVariationCount: '{ "flag_key": key, "variation_count": variations.$count() }', flagsWithCdnVariable: '$[variable_definitions.cdnVariationSettings exists]', // Enhanced Flag Environment patterns (Phase 2) flagsGroupedByEnvironment: `$group(environments.key)~>$keys()`, flagsEnabledInEnvironment: (envName) => `$[environments[key="${envName}"].enabled = true]`, flagEnvironmentComplexity: `{ "flag_key": key, "environments": environments.{ "name": key, "enabled": enabled, "variation_count": rollout_rules.$count(), "traffic_allocation": rollout_rules.percentage_included~>$sum() } }`, flagsWithMultipleEnvironments: '$[environments.$count() > 1]', flagsEnabledInProduction: '$[environments[key="production"].enabled = true]', flagsDisabledEverywhere: '$[environments[enabled=true].$count() = 0]', // Variation patterns variationsWithCustomValues: 'variations[variable_values != {}]', variationWeightDistribution: `variations.{ "key": key, "weight": weight / 100, "has_custom_code": actions[type = "custom_code"] exists }`, variationsByVariable: (varName) => `variations[variable_values.${varName} exists]`, // Ruleset patterns rulesetsWithComplexTargeting: '$[rules[audience_conditions.$count() > 1]]', ruleTrafficAllocation: 'rules.{ "rule_key": key, "traffic": percentage_included / 10000 }', rulesetsEnabled: '$[enabled = true]', // Experiment patterns experimentsWithMultipleAudiences: (limit) => `$[audience_ids.$count() > ${limit}]`, experimentPageComplexity: `{ "experiment": name, "page_count": page_ids.$count(), "has_url_targeting": url_targeting exists }`, experimentsByStatus: (status) => `$[status = "${status}"]`, // Audience patterns audienceComplexity: `{ "audience": name, "condition_count": conditions.($type() = "array" ? $count() : 1), "uses_custom_attributes": conditions.$string().$contains("custom_attribute") }`, audiencesWithCustomAttributes: '$[conditions.$string().$contains("custom_attribute")]', // Cross-entity patterns entitiesWithArchived: '$[archived = true]', entitiesByProject: (projectId) => `$[project_id = "${projectId}"]`, recentlyUpdated: (days) => { const date = new Date(); date.setDate(date.getDate() - days); return `$[updated_time > "${date.toISOString()}"]`; }, // Phase 2: Advanced cross-entity analysis patterns entityCountsByType: `$group($type)~>$values()~>{"entity_type": $[0], "count": $count()}`, entitiesWithComplexTargeting: `$[ $exists(audience_conditions) and ($type(audience_conditions) = "array" ? audience_conditions.$count() > 1 : true) or $exists(url_targeting) or $exists(page_ids) and page_ids.$count() > 1 ]`, flagExperimentMatrix: `{ "flag_key": key, "has_experiments": experiments.$count() > 0, "experiment_types": experiments.type~>$distinct(), "environments_with_experiments": environments[experiments.$count() > 0].key }`, audienceUsageAnalysis: `{ "audience_id": id, "audience_name": name, "used_in_experiments": experiments.$count(), "used_in_flags": flags.$count(), "condition_complexity": $type(conditions) = "array" ? conditions.$count() : 1, "last_usage": $max([experiments.updated_time, flags.updated_time]) }`, // Phase 2: Environment-specific analysis environmentHealthCheck: `environments.{ "environment": key, "enabled_flags": $parent[enabled=true].$count(), "total_flags": $parent.$count(), "health_score": $parent[enabled=true].$count() / $parent.$count(), "flags_with_experiments": $parent[experiments.$count() > 0].$count(), "avg_traffic_allocation": $parent.rollout_rules.percentage_included~>$average() }`, // Phase 2: Performance analysis patterns performanceMetrics: `{ "entity_count": $count(), "avg_complexity": variations.$count()~>$average(), "memory_footprint": $string().$length()~>$sum(), "last_activity": updated_time~>$max(), "activity_distribution": $group(updated_time~>$substring(0,10))~>$each(function($v, $k) { {"date": $k, "activity_count": $v.$count()} }) }` }; /** * Process SQL results through JSONata for complex filtering/transformation */ async processResults(sqlResults, jsonataExpression, context) { try { // Parse data_json fields if present const parsedResults = this.parseJsonFields(sqlResults); // Compile expression with caching let expression = this.expressions.get(jsonataExpression); if (!expression) { expression = jsonata(jsonataExpression); // Register custom functions this.registerCustomFunctions(expression); // Cache compiled expression this.expressions.set(jsonataExpression, expression); } // Apply expression to results const processed = await expression.evaluate(parsedResults, context); return processed; } catch (error) { throw new Error(`JSONata processing failed: ${error.message}`); } } /** * Build JSONata expression from enhanced intent */ buildExpression(intent) { const { jsonFilters, transforms, aggregations } = intent; let expression = '$'; // Add filters if (jsonFilters && jsonFilters.length > 0) { const filterExpressions = jsonFilters.map(f => this.buildFilterExpression(f)); expression += `[${filterExpressions.join(' and ')}]`; } // Add transformations if (transforms && transforms.length > 0) { expression = this.buildTransformExpression(expression, transforms); } // Add aggregations if (aggregations && aggregations.length > 0) { expression = this.wrapWithAggregations(expression, aggregations); } return expression; } /** * Apply a processing pipeline step */ async applyProcessingStep(data, step, context) { switch (step.type) { case 'filter': return this.applyFilterStep(data, step, context); case 'transform': return this.applyTransformStep(data, step, context); case 'aggregate': return this.applyAggregateStep(data, step, context); case 'sort': return this.applySortStep(data, step); case 'limit': return this.applyLimitStep(data, step); default: return data; } } /** * Get a pre-built expression pattern */ getPattern(patternName, ...args) { const pattern = this.COMMON_PATTERNS[patternName]; if (typeof pattern === 'function') { return pattern.apply(null, args); } return pattern; } /** * Phase 2: Build complex JSONata expressions from natural language intent */ buildComplexExpression(intent) { const { primaryEntity, action, groupBy, filters } = intent; // Handle environment grouping specifically if (groupBy && groupBy.includes('environment')) { return this.buildEnvironmentGroupingExpression(intent); } // Handle complexity analysis if (action === 'analyze' && intent.metrics?.includes('complexity')) { return this.buildComplexityAnalysisExpression(intent); } // Handle cross-entity correlation if (intent.relatedEntities && intent.relatedEntities.length > 0) { return this.buildCrossEntityExpression(intent); } // Handle traffic analysis if (intent.metrics?.some(m => m.includes('traffic') || m.includes('allocation'))) { return this.buildTrafficAnalysisExpression(intent); } // Default to basic grouping expression return this.buildBasicGroupingExpression(intent); } buildEnvironmentGroupingExpression(intent) { const { primaryEntity, filters } = intent; let expression = '$'; // Add filters first if (filters && filters.length > 0) { const filterExprs = filters .filter((f) => !f.field.includes('environment')) // Handle env filters separately .map((f) => this.buildQueryFilterExpression(f)); if (filterExprs.length > 0) { expression += `[${filterExprs.join(' and ')}]`; } } // Use the custom groupByEnvironment function return `${expression}~>$groupByEnvironment()`; } buildComplexityAnalysisExpression(intent) { return `$.{ "entity": key || name || id, "type": "${intent.primaryEntity}", "complexity": $analyzeComplexity($), "environments": environments ? $environmentHealth(environments) : null, "traffic": $trafficAnalysis($) }[complexity.level != "simple"]^(complexity.score)`; } buildCrossEntityExpression(intent) { const { primaryEntity, relatedEntities } = intent; return `{ "primary_entity": "${primaryEntity}", "correlation_analysis": $crossEntityCorrelation($, "project_id"), "entity_breakdown": $group(type || "${primaryEntity}")~>$each(function($v, $k) { { "entity_type": $k, "count": $v.$count(), "avg_complexity": $v.$analyzeComplexity($).$average(score), "samples": $v[0..2].{ "id": id || key, "name": name, "complexity": $analyzeComplexity($).level } } }), "activity_timeline": $timeSeriesAnalysis($, "updated_time") }`; } buildTrafficAnalysisExpression(intent) { return `$.{ "entity": key || name || id, "traffic_analysis": $trafficAnalysis($), "environment_health": environments ? $environmentHealth(environments) : null, "allocation_issues": $trafficAnalysis($).allocation_gaps > 0, "recommendation": $trafficAnalysis($).is_fully_allocated ? "optimal" : "needs_adjustment" }[allocation_issues = true or traffic_analysis.total_traffic > 0]^(traffic_analysis.total_traffic)`; } buildBasicGroupingExpression(intent) { const { groupBy, filters } = intent; let expression = '$'; // Apply filters if (filters && filters.length > 0) { const filterExprs = filters.map((f) => this.buildQueryFilterExpression(f)); expression += `[${filterExprs.join(' and ')}]`; } // Apply grouping if (groupBy && groupBy.length > 0) { const groupField = groupBy[0]; expression = `${expression}~>$group(${groupField})~>$each(function($v, $k) { { "${groupField}": $k, "count": $v.$count(), "entities": $v[0..4].{ "id": id || key, "name": name } } })`; } return expression; } parseJsonFields(results) { return results.map(row => { const parsed = { ...row }; // Parse data_json if it's a string if (parsed.data_json && typeof parsed.data_json === 'string') { try { parsed.data_json = JSON.parse(parsed.data_json); // Flatten data_json properties to top level for easier access Object.assign(parsed, parsed.data_json); } catch (e) { // Keep original if parse fails } } // Parse other JSON fields ['conditions', 'variables', 'audience_conditions', 'variations', 'metrics'].forEach(field => { if (parsed[field] && typeof parsed[field] === 'string') { try { parsed[field] = JSON.parse(parsed[field]); } catch (e) { // Keep original if parse fails } } }); return parsed; }); } registerCustomFunctions(expression) { // Register utility functions expression.registerFunction('countIf', (array, predicate) => { const predicateExpr = jsonata(predicate); return array.filter(item => predicateExpr.evaluate(item)).length; }); expression.registerFunction('percentOf', (value, total) => { return total > 0 ? (value / total) * 100 : 0; }); expression.registerFunction('hasVariable', (obj, varName) => { return obj?.variable_definitions?.[varName] !== undefined || obj?.variable_values?.[varName] !== undefined; }); expression.registerFunction('daysSince', (dateStr) => { const date = new Date(dateStr); const now = new Date(); const diffTime = Math.abs(now.getTime() - date.getTime()); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }); expression.registerFunction('flatten', (obj) => { const result = {}; function recurse(cur, prop) { if (Object(cur) !== cur) { result[prop] = cur; } else if (Array.isArray(cur)) { result[prop] = cur; } else { let isEmpty = true; for (const p in cur) { isEmpty = false; recurse(cur[p], prop ? prop + '.' + p : p); } if (isEmpty && prop) { result[prop] = {}; } } } recurse(obj, ''); return result; }); // Phase 2: Enhanced functions for complex nested queries expression.registerFunction('groupByEnvironment', (entities) => { const groups = {}; for (const entity of entities) { if (entity.environments && Array.isArray(entity.environments)) { for (const env of entity.environments) { const envKey = env.key || env.name || 'unknown'; if (!groups[envKey]) groups[envKey] = []; groups[envKey].push({ ...entity, environment_status: env.enabled ? 'enabled' : 'disabled', environment_config: env }); } } } return Object.entries(groups).map(([environment, items]) => ({ environment, count: items.length, entities: items })); }); expression.registerFunction('analyzeComplexity', (entity) => { let complexity = 0; let factors = []; // Count variations if (entity.variations && Array.isArray(entity.variations)) { const varCount = entity.variations.length; complexity += varCount * 2; factors.push(`${varCount} variations`); } // Count audiences if (entity.audience_ids && Array.isArray(entity.audience_ids)) { const audCount = entity.audience_ids.length; complexity += audCount * 3; factors.push(`${audCount} audiences`); } // Count environments if (entity.environments && Array.isArray(entity.environments)) { const envCount = entity.environments.length; complexity += envCount; factors.push(`${envCount} environments`); } // Check for custom targeting if (entity.url_targeting || entity.page_ids) { complexity += 5; factors.push('custom targeting'); } return { score: complexity, level: complexity < 5 ? 'simple' : complexity < 15 ? 'moderate' : 'complex', factors }; }); expression.registerFunction('environmentHealth', (environments) => { if (!Array.isArray(environments)) return { status: 'unknown' }; const enabled = environments.filter(env => env.enabled).length; const total = environments.length; const healthScore = total > 0 ? enabled / total : 0; return { enabled_count: enabled, total_count: total, health_score: Math.round(healthScore * 100), status: healthScore > 0.8 ? 'healthy' : healthScore > 0.5 ? 'warning' : 'critical', environments: environments.map(env => ({ name: env.key || env.name, enabled: env.enabled, traffic: env.rollout_rules ? env.rollout_rules.reduce((sum, rule) => sum + (rule.percentage_included || 0), 0) : 0 })) }; }); expression.registerFunction('trafficAnalysis', (entity) => { let totalTraffic = 0; let distribution = {}; // Analyze variations traffic if (entity.variations && Array.isArray(entity.variations)) { entity.variations.forEach((variation) => { const weight = variation.weight || 0; totalTraffic += weight; distribution[variation.key || variation.name] = weight; }); } // Analyze rules traffic if (entity.rules && Array.isArray(entity.rules)) { entity.rules.forEach((rule) => { const traffic = rule.percentage_included || 0; totalTraffic += traffic; distribution[rule.key || 'rule'] = traffic; }); } return { total_traffic: totalTraffic, distribution, is_fully_allocated: totalTraffic >= 10000, // 100% in basis points allocation_gaps: Math.max(0, 10000 - totalTraffic) }; }); expression.registerFunction('timeSeriesAnalysis', (entities, timeField) => { const timeSeries = {}; entities.forEach(entity => { const timestamp = entity[timeField]; if (timestamp) { const date = new Date(timestamp).toISOString().split('T')[0]; // YYYY-MM-DD timeSeries[date] = (timeSeries[date] || 0) + 1; } }); const dates = Object.keys(timeSeries).sort(); return dates.map(date => ({ date, count: timeSeries[date], cumulative: dates.slice(0, dates.indexOf(date) + 1) .reduce((sum, d) => sum + timeSeries[d], 0) })); }); expression.registerFunction('crossEntityCorrelation', (entities, correlateBy) => { const correlations = {}; entities.forEach(entity => { const key = entity[correlateBy]; if (key) { if (!correlations[key]) { correlations[key] = { count: 0, entities: [], types: new Set(), avg_complexity: 0 }; } correlations[key].count++; correlations[key].entities.push(entity.id || entity.key); correlations[key].types.add(entity.type || 'unknown'); // Add complexity if available const complexity = this.calculateEntityComplexity(entity); correlations[key].avg_complexity = (correlations[key].avg_complexity * (correlations[key].count - 1) + complexity) / correlations[key].count; } }); return Object.entries(correlations).map(([key, data]) => ({ [correlateBy]: key, count: data.count, entity_types: Array.from(data.types), avg_complexity: Math.round(data.avg_complexity * 100) / 100, entities: data.entities.slice(0, 10) // Limit for readability })); }); } calculateEntityComplexity(entity) { let complexity = 1; // Base complexity if (entity.variations) complexity += Array.isArray(entity.variations) ? entity.variations.length : 1; if (entity.audience_ids) complexity += Array.isArray(entity.audience_ids) ? entity.audience_ids.length : 1; if (entity.environments) complexity += Array.isArray(entity.environments) ? entity.environments.length : 1; if (entity.rules) complexity += Array.isArray(entity.rules) ? entity.rules.length : 1; return complexity; } buildFilterExpression(filter) { const path = filter.path; switch (filter.operator) { case 'exists': return `${path} exists`; case 'not_exists': return `not(${path} exists)`; case 'eq': return `${path} = ${JSON.stringify(filter.value)}`; case 'ne': return `${path} != ${JSON.stringify(filter.value)}`; case 'gt': return `${path} > ${filter.value}`; case 'lt': return `${path} < ${filter.value}`; case 'contains': if (filter.type === 'array') { return `${JSON.stringify(filter.value)} in ${path}`; } else { return `$contains(${path}, ${JSON.stringify(filter.value)})`; } case 'matches': return `$match(${path}, /${filter.value}/)`; default: return 'true'; } } // Separate method for QueryFilter (used by AnalyticsEngine) buildQueryFilterExpression(filter) { const field = filter.field; const value = filter.value; switch (filter.operator) { case 'eq': return `${field} = ${JSON.stringify(value)}`; case 'ne': return `${field} != ${JSON.stringify(value)}`; case 'gt': return `${field} > ${value}`; case 'lt': return `${field} < ${value}`; case 'gte': return `${field} >= ${value}`; case 'lte': return `${field} <= ${value}`; case 'in': if (Array.isArray(value)) { return `${field} in [${value.map(v => JSON.stringify(v)).join(', ')}]`; } return `${field} = ${JSON.stringify(value)}`; case 'not_in': if (Array.isArray(value)) { return `not(${field} in [${value.map(v => JSON.stringify(v)).join(', ')}])`; } return `${field} != ${JSON.stringify(value)}`; case 'contains': return `$contains(${field}, ${JSON.stringify(value)})`; case 'not_contains': return `not($contains(${field}, ${JSON.stringify(value)}))`; case 'exists': return `${field} exists`; case 'not_exists': return `not(${field} exists)`; case 'array_contains': return `${JSON.stringify(value)} in ${field}`; case 'array_length': return `${field}.$count() = ${value}`; case 'json_contains': if (filter.jsonPath) { return `${field}.${filter.jsonPath} exists`; } return `${field} exists`; default: return 'true'; } } buildTransformExpression(base, transforms) { let expression = base; for (const transform of transforms) { switch (transform.type) { case 'extract': const fields = transform.config.fields; expression = `(${expression}).{ ${fields.map(f => `"${f}": ${f}`).join(', ')} }`; break; case 'compute': const computations = transform.config.computations; const compStr = Object.entries(computations) .map(([key, expr]) => `"${key}": ${expr}`) .join(', '); expression = `(${expression}).{ *, ${compStr} }`; break; case 'flatten': expression = `(${expression}).$flatten()`; break; case 'pivot': const { rows, columns, values } = transform.config; expression = `(${expression}).$groupBy(${rows}).$pivot(${columns}, ${values})`; break; } } return expression; } wrapWithAggregations(expression, aggregations) { const aggMap = { count: '$count()', sum: '$sum()', avg: '$average()', min: '$min()', max: '$max()', distinct: '$distinct()', percent: '$percentOf($count(), $$.length)' }; const aggExprs = aggregations .filter(agg => aggMap[agg]) .map(agg => `"${agg}": ${aggMap[agg]}`); if (aggExprs.length === 0) { return expression; } return `{ "data": ${expression}, ${aggExprs.join(', ')} }`; } async applyFilterStep(data, step, context) { const filters = step.params; let expression = '$'; if (filters.length > 0) { const filterExprs = filters.map(f => this.buildFilterExpression(f)); expression += `[${filterExprs.join(' and ')}]`; } return this.processResults(data, expression, context); } async applyTransformStep(data, step, context) { let expression = '$'; switch (step.operation) { case 'aggregate': const metrics = step.params.metrics; const metricExprs = Object.entries(metrics) .map(([key, expr]) => `"${key}": ${expr}`) .join(', '); expression = `{ ${metricExprs} }`; break; case 'pivot': const { rows, columns, values } = step.params; expression = `$.$groupBy(${rows}).$pivot(${columns}, ${values})`; break; default: return data; } return this.processResults(data, expression, context); } async applyAggregateStep(data, step, context) { const aggregations = step.params; const expression = this.wrapWithAggregations('$', aggregations); const result = await this.processResults(data, expression, context); // Aggregations typically return a single result object return Array.isArray(result) ? result : [result]; } applySortStep(data, step) { const orderBy = step.params; return [...data].sort((a, b) => { for (const order of orderBy) { const aVal = this.getNestedValue(a, order.field); const bVal = this.getNestedValue(b, order.field); if (aVal < bVal) return order.direction === 'asc' ? -1 : 1; if (aVal > bVal) return order.direction === 'asc' ? 1 : -1; } return 0; }); } applyLimitStep(data, step) { const { limit, offset } = step.params; return data.slice(offset || 0, (offset || 0) + limit); } getNestedValue(obj, path) { return path.split('.').reduce((curr, prop) => curr?.[prop], obj); } } //# sourceMappingURL=JSONataProcessor.js.map