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