UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

266 lines 10.4 kB
/** * View-Only SQL Builder for Intelligent Query Engine * * This builder ONLY generates SQL queries against views. * NO FALLBACKS to base tables or complex JOINs. * If a field is not available in a view, it throws an error. */ import { getLogger } from '../../logging/Logger.js'; import { getViewMapping } from '../generated/ViewOnlyFieldMappings.generated.js'; import { DateFunctionHandler } from './DateFunctionHandler.js'; import { JSONPathHandler } from './JSONPathHandler.js'; const logger = getLogger(); export class ViewOnlySQLBuilder { dateHandler; jsonHandler; primaryView = ''; constructor() { this.dateHandler = new DateFunctionHandler(); this.jsonHandler = new JSONPathHandler(); } /** * Build SQL query from UniversalQuery - VIEW ONLY */ async buildSQL(query) { logger.info(`[VIEW-ONLY] Building SQL for entity: ${query.find}`); // Determine primary view based on entity type this.primaryView = this.determinePrimaryView(query); logger.info(`[VIEW-ONLY] Primary view determined: ${this.primaryView}`); // Build SELECT clause const selectClause = this.buildSelectClause(query); // Build FROM clause (always a view) const fromClause = this.primaryView; // Build WHERE clause const whereClause = this.buildWhereClause(query); // Build GROUP BY clause const groupByClause = this.buildGroupByClause(query); // Build ORDER BY clause const orderByClause = this.buildOrderByClause(query); // Build HAVING clause const havingClause = this.buildHavingClause(query); // Assemble final SQL let sql = `SELECT ${selectClause}\nFROM ${fromClause}`; if (whereClause) { sql += `\nWHERE ${whereClause}`; } if (groupByClause) { sql += `\nGROUP BY ${groupByClause}`; } if (havingClause) { sql += `\nHAVING ${havingClause}`; } if (orderByClause) { sql += `\nORDER BY ${orderByClause}`; } if (query.limit) { sql += `\nLIMIT ${query.limit}`; } if (query.offset) { sql += `\nOFFSET ${query.offset}`; } logger.info(`[VIEW-ONLY] Generated SQL: ${sql}`); return sql; } /** * Determine the primary view for the query based on entity type */ determinePrimaryView(query) { const entityViewMap = { 'flag': 'flags_unified_view', 'flags': 'flags_unified_view', 'experiment': 'experiments_unified_view', 'experiments': 'experiments_unified_view', 'page': 'entity_usage_view', 'pages': 'entity_usage_view', 'audience': 'entity_usage_view', 'audiences': 'entity_usage_view', 'event': 'entity_usage_view', 'events': 'entity_usage_view', 'attribute': 'entity_usage_view', 'attributes': 'entity_usage_view' }; const view = entityViewMap[query.find.toLowerCase()]; if (!view) { throw new Error(`Entity type '${query.find}' is not supported in the view-only system.\n` + `Supported entities: ${Object.keys(entityViewMap).join(', ')}`); } return view; } /** * Build SELECT clause using view field mappings */ buildSelectClause(query) { const mappedFields = []; // Handle aggregations first if (query.aggregations && query.aggregations.length > 0) { for (const agg of query.aggregations) { const aggFunction = agg.function.toUpperCase(); const aggAlias = agg.alias || `${aggFunction.toLowerCase()}_result`; if (agg.field === '*') { mappedFields.push(`${aggFunction}(*) as ${aggAlias}`); } else { // Validate field exists in view try { const mapping = getViewMapping(agg.field); mappedFields.push(`${aggFunction}(${mapping.view.columnName}) as ${aggAlias}`); } catch (error) { // Field might be entity name for COUNT if (aggFunction === 'COUNT' && agg.field === query.find) { mappedFields.push(`${aggFunction}(*) as ${aggAlias}`); } else { throw error; } } } } // Add GROUP BY fields if needed if (query.groupBy && query.groupBy.length > 0) { for (const groupField of query.groupBy) { if (!mappedFields.some(f => f.includes(groupField))) { const mapping = getViewMapping(groupField); mappedFields.push(mapping.view.columnName); } } } return mappedFields.join(', '); } // Handle regular SELECT fields const selectFields = query.select || ['*']; if (selectFields[0] === '*') { return '*'; } for (const field of selectFields) { // Handle aliased fields const aliasMatch = field.match(/^(.+?)\s+as\s+(.+)$/i); const fieldName = aliasMatch ? aliasMatch[1].trim() : field; const alias = aliasMatch ? aliasMatch[2].trim() : null; // Validate field exists in view const mapping = getViewMapping(fieldName); if (alias) { mappedFields.push(`${mapping.view.columnName} as ${alias}`); } else { mappedFields.push(mapping.view.columnName); } } return mappedFields.join(', '); } /** * Build WHERE clause using view field mappings */ buildWhereClause(query) { if (!query.where || query.where.length === 0) { return ''; } const conditions = []; for (const condition of query.where) { // Get the view column for this field const mapping = getViewMapping(condition.field); const sqlField = mapping.view.columnName; // Check if this is a date field if (this.dateHandler.isDateField(condition.field)) { const dateResult = this.dateHandler.parseDateFilter(condition); if (dateResult.isValid) { conditions.push(dateResult.sqlExpression.replace(condition.field, sqlField)); continue; } } // Build standard condition conditions.push(this.buildStandardCondition(sqlField, condition)); } return conditions.join(' AND '); } /** * Build standard condition */ buildStandardCondition(sqlField, condition) { let value = condition.value; // Handle different operators if (condition.operator === 'IN' && Array.isArray(value)) { const quotedValues = value.map(v => `'${String(v).replace(/'/g, "''")}'`); return `${sqlField} IN (${quotedValues.join(', ')})`; } else if (condition.operator === 'IN' && typeof value === 'string') { const values = value.split(',').map(v => v.trim()); const quotedValues = values.map(v => `'${v.replace(/'/g, "''")}'`); return `${sqlField} IN (${quotedValues.join(', ')})`; } else if (condition.operator === 'BETWEEN' && Array.isArray(value) && value.length === 2) { return `${sqlField} BETWEEN '${value[0]}' AND '${value[1]}'`; } else if (typeof value === 'string') { return `${sqlField} ${condition.operator} '${value.replace(/'/g, "''")}'`; } else if (value === null) { if (condition.operator === '=' || condition.operator === 'IS NULL') { return `${sqlField} IS NULL`; } else if (condition.operator === '!=' || condition.operator === 'IS NOT NULL') { return `${sqlField} IS NOT NULL`; } else { return `${sqlField} ${condition.operator} NULL`; } } else { return `${sqlField} ${condition.operator} ${value}`; } } /** * Build GROUP BY clause using view field mappings */ buildGroupByClause(query) { if (!query.groupBy || query.groupBy.length === 0) { return ''; } const groupFields = []; for (const field of query.groupBy) { // Check if field is already a SQL expression if (field.includes('(') && field.includes(')')) { groupFields.push(field); } else { // Get the view column for this field const mapping = getViewMapping(field); groupFields.push(mapping.view.columnName); } } return groupFields.join(', '); } /** * Build ORDER BY clause using view field mappings */ buildOrderByClause(query) { if (!query.orderBy || query.orderBy.length === 0) { return ''; } const orderClauses = []; for (const order of query.orderBy) { const mapping = getViewMapping(order.field); orderClauses.push(`${mapping.view.columnName} ${order.direction}`); } return orderClauses.join(', '); } /** * Build HAVING clause */ buildHavingClause(query) { if (!query.having || query.having.length === 0) { return ''; } const conditions = query.having.map(h => { // Check if the field in HAVING is an aggregate expression if (h.field.includes('(') && h.field.includes(')')) { return `${h.field} ${h.operator} ${h.value}`; } // Otherwise, get the view column const mapping = getViewMapping(h.field); return `${mapping.view.columnName} ${h.operator} ${h.value}`; }).join(' AND '); return conditions; } } //# sourceMappingURL=ViewOnlySQLBuilder.js.map