UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

285 lines 9.97 kB
/** * Cache Key Generator for Intelligent Query Caching * * Generates deterministic cache keys from normalized queries * ensuring consistent keys for semantically equivalent queries. */ import { createHash } from 'crypto'; import { getLogger } from '../../../logging/Logger.js'; export class CacheKeyGenerator { logger = getLogger(); keyPrefix = 'iqe'; // Intelligent Query Engine separator = ':'; maxKeyLength = 250; // Redis key length limit /** * Generate a deterministic cache key from normalized query and context */ generateKey(query, context = {}) { this.logger.debug({ query, context }, 'Generating cache key'); const keyComponents = [ this.keyPrefix, query.entity, query.operation, ]; // Add context components if (context.projectId) { keyComponents.push(`p${context.projectId}`); } if (context.userId) { keyComponents.push(`u${this.hashValue(context.userId)}`); } if (context.environment) { keyComponents.push(`e${context.environment}`); } // Add filter hash if filters exist if (Object.keys(query.filters).length > 0) { keyComponents.push(`f${this.hashFilters(query.filters)}`); } // Add time component const timeComponent = this.getTimeComponent(query.timeRange); if (timeComponent) { keyComponents.push(`t${timeComponent}`); } // Add joins if present if (query.joins.length > 0) { keyComponents.push(`j${this.hashArray(query.joins)}`); } // Add aggregations if present if (query.aggregations.length > 0) { keyComponents.push(`a${this.hashArray(query.aggregations)}`); } // Add group by if present if (query.groupBy && query.groupBy.length > 0) { keyComponents.push(`g${this.hashArray(query.groupBy)}`); } // Add limit if present if (query.limit) { keyComponents.push(`l${query.limit}`); } // Add order by if present if (query.orderBy && query.orderBy.length > 0) { keyComponents.push(`o${this.hashOrderBy(query.orderBy)}`); } // Add projections hash if specific fields requested if (query.projections.length > 0) { keyComponents.push(`s${this.hashArray(query.projections)}`); } // Join components let key = keyComponents.join(this.separator); // Ensure key doesn't exceed max length if (key.length > this.maxKeyLength) { // Hash the entire key if too long const hash = this.hashValue(key); key = `${this.keyPrefix}:${query.entity}:${query.operation}:h${hash}`; } this.logger.debug({ key, components: keyComponents }, 'Cache key generated'); return key; } /** * Generate pattern for cache invalidation */ generateInvalidationPattern(entity, operation, context) { const components = [this.keyPrefix]; if (entity) { components.push(entity); } else { components.push('*'); } if (operation) { components.push(operation); } else { components.push('*'); } // Add context patterns if (context?.projectId) { components.push(`p${context.projectId}`); } // Always end with wildcard for remaining components components.push('*'); return components.join(this.separator); } /** * Hash filters object deterministically */ hashFilters(filters) { // Sort keys for consistent ordering const sortedKeys = Object.keys(filters).sort(); const filterString = sortedKeys .map(key => `${key}=${this.serializeValue(filters[key])}`) .join('&'); return this.hashValue(filterString).substring(0, 8); } /** * Hash array of values */ hashArray(arr) { // Sort for consistent ordering (joins and aggregations order doesn't matter) const sorted = [...arr].sort(); return this.hashValue(sorted.join(',')).substring(0, 8); } /** * Hash order by array */ hashOrderBy(orderBy) { // Order matters for orderBy, so don't sort const orderString = orderBy .map(o => `${o.field}:${o.direction}`) .join(','); return this.hashValue(orderString).substring(0, 8); } /** * Serialize value for consistent string representation */ serializeValue(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'object') { return JSON.stringify(value, Object.keys(value).sort()); } return String(value); } /** * Hash a string value using SHA256 */ hashValue(value) { return createHash('sha256').update(value).digest('hex'); } /** * Get time component for cache key */ getTimeComponent(timeRange) { if (!timeRange) return null; const now = new Date(); if (timeRange.type === 'relative') { switch (timeRange.duration) { case 'today': return this.formatDate(now); case 'yesterday': const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); return this.formatDate(yesterday); case 'this_week': return `w${this.getWeekNumber(now)}-${now.getFullYear()}`; case 'this_month': return `m${now.getMonth() + 1}-${now.getFullYear()}`; case 'this_year': return `y${now.getFullYear()}`; default: // Handle "last_X_days" patterns if (timeRange.duration?.startsWith('last_')) { const match = timeRange.duration.match(/last_(\d+)_(\w+)/); if (match) { const [, count, unit] = match; return `${unit.charAt(0)}${count}-${this.formatDate(now)}`; } } return this.hashValue(timeRange.duration || '').substring(0, 8); } } else { // Absolute time range const startStr = timeRange.start ? this.normalizeDate(timeRange.start) : 'open'; const endStr = timeRange.end ? this.normalizeDate(timeRange.end) : 'open'; return `${startStr}-${endStr}`; } } /** * Format date as YYYY-MM-DD */ formatDate(date) { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; } /** * Normalize date string or Date object */ normalizeDate(date) { if (typeof date === 'string') { // Try to parse and reformat const parsed = new Date(date); if (!isNaN(parsed.getTime())) { return this.formatDate(parsed); } // If parsing fails, hash the string return this.hashValue(date).substring(0, 8); } else { return this.formatDate(date); } } /** * Get ISO week number */ getWeekNumber(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); } /** * Parse cache key to extract components (for debugging/monitoring) */ parseKey(key) { const parts = key.split(this.separator); const parsed = {}; if (parts.length < 3) return parsed; parsed.prefix = parts[0]; parsed.entity = parts[1]; parsed.operation = parts[2]; // Parse remaining components for (let i = 3; i < parts.length; i++) { const component = parts[i]; const type = component.charAt(0); const value = component.substring(1); switch (type) { case 'p': parsed.projectId = value; break; case 'u': parsed.userHash = value; break; case 'e': parsed.environment = value; break; case 'f': parsed.filterHash = value; break; case 't': parsed.timeComponent = value; break; case 'j': parsed.joinHash = value; break; case 'a': parsed.aggregationHash = value; break; case 'g': parsed.groupByHash = value; break; case 'l': parsed.limit = value; break; case 'o': parsed.orderByHash = value; break; case 's': parsed.projectionHash = value; break; case 'h': parsed.fullHash = value; break; } } return parsed; } } //# sourceMappingURL=CacheKeyGenerator.js.map