UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

467 lines 16.4 kB
/** * Date/Time Function Handler - Phase 4B Implementation * * Enables all time-based analytics queries with SQLite date functions. * Supports relative dates, date ranges, and time arithmetic. * * User Requirement: "experiments that were started in the last 30 days" * * Features: * - Relative date parsing (LAST_30_DAYS, THIS_MONTH, etc.) * - Date range filtering (BETWEEN dates) * - Year/month/day filtering * - SQLite native date functions for optimal performance */ import { getLogger } from '../../logging/Logger.js'; const logger = getLogger(); export class DateFunctionHandler { DATE_FUNCTIONS = { // Relative date functions 'LAST_30_DAYS': `DATE('now', '-30 days')`, 'LAST_7_DAYS': `DATE('now', '-7 days')`, 'LAST_1_DAY': `DATE('now', '-1 day')`, 'LAST_90_DAYS': `DATE('now', '-90 days')`, 'LAST_365_DAYS': `DATE('now', '-365 days')`, 'LAST_1_YEAR': `DATE('now', '-1 year')`, // Period functions 'TODAY': `DATE('now')`, 'YESTERDAY': `DATE('now', '-1 day')`, 'THIS_WEEK': `DATE('now', 'weekday 0', '-6 days')`, 'THIS_MONTH': `DATE('now', 'start of month')`, 'THIS_YEAR': `DATE('now', 'start of year')`, 'LAST_WEEK': `DATE('now', 'weekday 0', '-13 days')`, 'LAST_MONTH': `DATE('now', 'start of month', '-1 month')`, 'LAST_YEAR': `DATE('now', 'start of year', '-1 year')`, // Start/end of periods 'START_OF_MONTH': `DATE('now', 'start of month')`, 'END_OF_MONTH': `DATE('now', 'start of month', '+1 month', '-1 day')`, 'START_OF_YEAR': `DATE('now', 'start of year')`, 'END_OF_YEAR': `DATE('now', 'start of year', '+1 year', '-1 day')`, 'START_OF_WEEK': `DATE('now', 'weekday 0', '-6 days')`, 'END_OF_WEEK': `DATE('now', 'weekday 0')` }; TIME_PATTERNS = { // ISO 8601 date formats ISO_DATE: /^\d{4}-\d{2}-\d{2}$/, ISO_DATETIME: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, // Relative date patterns LAST_N_DAYS: /^LAST_(\d+)_DAYS?$/i, LAST_N_HOURS: /^LAST_(\d+)_HOURS?$/i, LAST_N_MONTHS: /^LAST_(\d+)_MONTHS?$/i, LAST_N_YEARS: /^LAST_(\d+)_YEARS?$/i, // Numeric patterns YEAR_ONLY: /^\d{4}$/, MONTH_YEAR: /^\d{4}-\d{2}$/ }; constructor() { logger.info('DateFunctionHandler initialized with SQLite date functions'); } /** * Parse date filter condition and convert to SQL */ parseDateFilter(condition) { try { const { field, operator, value } = condition; logger.debug(`Parsing date filter: ${field} ${operator} ${value}`); // Handle different operator types switch (operator) { case 'BETWEEN': return this.handleBetweenOperator(field, value); case '>': case '>=': case '<': case '<=': case '=': return this.handleComparisonOperator(field, operator, value); default: return this.handleSpecialOperators(field, operator, value); } } catch (error) { logger.error(`Date filter parsing failed: ${error instanceof Error ? error.message : String(error)}`); return { sqlExpression: '', isValid: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Handle BETWEEN operator for date ranges */ handleBetweenOperator(field, value) { if (!Array.isArray(value) || value.length !== 2) { return { sqlExpression: '', isValid: false, error: 'BETWEEN operator requires array with exactly 2 values' }; } const [startDate, endDate] = value; const startSQL = this.convertToSQLDate(startDate); const endSQL = this.convertToSQLDate(endDate); if (!startSQL.isValid || !endSQL.isValid) { return { sqlExpression: '', isValid: false, error: `Invalid date values: ${startSQL.error || endSQL.error}` }; } const sqlExpression = `${field} BETWEEN ${startSQL.sqlExpression} AND ${endSQL.sqlExpression}`; return { sqlExpression, isValid: true, parsedValue: [startDate, endDate] }; } /** * Handle comparison operators (>, <, >=, <=, =) */ handleComparisonOperator(field, operator, value) { const dateSQL = this.convertToSQLDate(value); if (!dateSQL.isValid) { return { sqlExpression: '', isValid: false, error: dateSQL.error }; } const sqlExpression = `${field} ${operator} ${dateSQL.sqlExpression}`; return { sqlExpression, isValid: true, parsedValue: value }; } /** * Handle special date operators (YEAR, MONTH, etc.) */ handleSpecialOperators(field, operator, value) { switch (operator) { case 'YEAR': return this.handleYearFilter(field, value); case 'MONTH': return this.handleMonthFilter(field, value); case 'DAY': return this.handleDayFilter(field, value); case 'LAST_N_DAYS': return this.handleLastNDays(field, value); default: return { sqlExpression: '', isValid: false, error: `Unsupported date operator: ${operator}` }; } } /** * Validate a date string in YYYY-MM-DD format */ isValidDate(dateStr) { const parts = dateStr.split('-'); if (parts.length !== 3) return false; const year = parseInt(parts[0]); const month = parseInt(parts[1]); const day = parseInt(parts[2]); // Validate month if (month < 1 || month > 12) return false; // Days in each month (non-leap year) const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // Check for leap year const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); if (isLeapYear && month === 2) { daysInMonth[1] = 29; } // Validate day if (day < 1 || day > daysInMonth[month - 1]) return false; return true; } /** * Convert value to SQL date expression */ convertToSQLDate(value) { if (typeof value === 'string') { // Check if it's a predefined function if (this.DATE_FUNCTIONS[value.toUpperCase()]) { return { sqlExpression: this.DATE_FUNCTIONS[value.toUpperCase()], isValid: true, parsedValue: value }; } // Check for dynamic patterns const lastNDaysMatch = value.match(this.TIME_PATTERNS.LAST_N_DAYS); if (lastNDaysMatch) { const days = parseInt(lastNDaysMatch[1]); return { sqlExpression: `DATE('now', '-${days} days')`, isValid: true, parsedValue: value }; } const lastNHoursMatch = value.match(this.TIME_PATTERNS.LAST_N_HOURS); if (lastNHoursMatch) { const hours = parseInt(lastNHoursMatch[1]); return { sqlExpression: `DATETIME('now', '-${hours} hours')`, isValid: true, parsedValue: value }; } const lastNMonthsMatch = value.match(this.TIME_PATTERNS.LAST_N_MONTHS); if (lastNMonthsMatch) { const months = parseInt(lastNMonthsMatch[1]); return { sqlExpression: `DATE('now', '-${months} months')`, isValid: true, parsedValue: value }; } // Check for ISO date formats with validation if (this.TIME_PATTERNS.ISO_DATE.test(value)) { if (!this.isValidDate(value)) { return { sqlExpression: '', isValid: false, error: `Invalid date: ${value}` }; } return { sqlExpression: `'${value}'`, isValid: true, parsedValue: value }; } if (this.TIME_PATTERNS.ISO_DATETIME.test(value)) { // Extract date part and validate const datePart = value.split('T')[0]; if (!this.isValidDate(datePart)) { return { sqlExpression: '', isValid: false, error: `Invalid datetime: ${value}` }; } return { sqlExpression: `'${value}'`, isValid: true, parsedValue: value }; } // Check for year-only format if (this.TIME_PATTERNS.YEAR_ONLY.test(value)) { return { sqlExpression: `'${value}-01-01'`, isValid: true, parsedValue: value }; } // Check for month-year format if (this.TIME_PATTERNS.MONTH_YEAR.test(value)) { return { sqlExpression: `'${value}-01'`, isValid: true, parsedValue: value }; } } if (typeof value === 'number') { // Assume it's a year if it's a 4-digit number if (value >= 1000 && value <= 9999) { return { sqlExpression: `'${value}-01-01'`, isValid: true, parsedValue: value }; } } return { sqlExpression: '', isValid: false, error: `Cannot convert value to SQL date: ${value}` }; } /** * Handle YEAR filtering */ handleYearFilter(field, value) { const year = typeof value === 'string' ? parseInt(value) : value; if (isNaN(year) || year < 1000 || year > 9999) { return { sqlExpression: '', isValid: false, error: 'Year must be a 4-digit number' }; } const sqlExpression = `STRFTIME('%Y', ${field}) = '${year}'`; return { sqlExpression, isValid: true, parsedValue: year }; } /** * Handle MONTH filtering */ handleMonthFilter(field, value) { const month = typeof value === 'string' ? parseInt(value) : value; if (isNaN(month) || month < 1 || month > 12) { return { sqlExpression: '', isValid: false, error: 'Month must be a number between 1 and 12' }; } const monthStr = month.toString().padStart(2, '0'); const sqlExpression = `STRFTIME('%m', ${field}) = '${monthStr}'`; return { sqlExpression, isValid: true, parsedValue: month }; } /** * Handle DAY filtering */ handleDayFilter(field, value) { const day = typeof value === 'string' ? parseInt(value) : value; if (isNaN(day) || day < 1 || day > 31) { return { sqlExpression: '', isValid: false, error: 'Day must be a number between 1 and 31' }; } const dayStr = day.toString().padStart(2, '0'); const sqlExpression = `STRFTIME('%d', ${field}) = '${dayStr}'`; return { sqlExpression, isValid: true, parsedValue: day }; } /** * Handle LAST_N_DAYS operator */ handleLastNDays(field, value) { const days = typeof value === 'string' ? parseInt(value) : value; if (isNaN(days) || days < 1) { return { sqlExpression: '', isValid: false, error: 'Days must be a positive number' }; } const sqlExpression = `${field} >= DATE('now', '-${days} days')`; return { sqlExpression, isValid: true, parsedValue: days }; } /** * Validate date value format */ validateDateValue(value) { const result = this.convertToSQLDate(value); return { isValid: result.isValid, error: result.error }; } /** * Get available date functions */ getAvailableFunctions() { return Object.keys(this.DATE_FUNCTIONS); } /** * Get SQL for relative date */ getRelativeDateSQL(relativeDate) { return this.DATE_FUNCTIONS[relativeDate.toUpperCase()] || null; } /** * Check if a field appears to be a date field */ isDateField(fieldName) { const dateFieldPatterns = [ /created/i, /updated/i, /modified/i, /time/i, /date/i, /start/i, /end/i, /earliest/i, /latest/i ]; return dateFieldPatterns.some(pattern => pattern.test(fieldName)); } /** * Get example queries for documentation */ getExampleQueries() { return [ { description: 'Experiments started in the last 30 days', query: { find: 'experiments', select: ['name', 'created_time'], where: [{ field: 'created_time', operator: '>', value: 'LAST_30_DAYS' }] } }, { description: 'Flags created this month', query: { find: 'flags', select: ['key', 'name', 'created_time'], where: [{ field: 'created_time', operator: '>=', value: 'THIS_MONTH' }] } }, { description: 'Experiments in date range', query: { find: 'experiments', select: ['name', 'start_date'], where: [{ field: 'start_date', operator: 'BETWEEN', value: ['2025-01-01', '2025-01-31'] }] } }, { description: 'Flags created in 2025', query: { find: 'flags', select: ['key', 'name'], where: [{ field: 'created_time', operator: 'YEAR', value: 2025 }] } }, { description: 'Recent activity (last 7 days)', query: { find: 'experiments', select: ['name', 'last_modified'], where: [{ field: 'last_modified', operator: '>', value: 'LAST_7_DAYS' }], orderBy: [{ field: 'last_modified', direction: 'DESC' }] } } ]; } /** * Get statistics about date function usage */ getStatistics() { const functionKeys = Object.keys(this.DATE_FUNCTIONS); const relativeFunctions = functionKeys.filter(key => key.includes('LAST_')).length; const periodFunctions = functionKeys.filter(key => key.includes('THIS_') || key.includes('START_') || key.includes('END_')).length; return { totalFunctions: functionKeys.length, relativeFunctions, periodFunctions, supportedPatterns: Object.keys(this.TIME_PATTERNS).length }; } } //# sourceMappingURL=DateFunctionHandler.js.map