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