@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
462 lines (456 loc) • 17.2 kB
JavaScript
/**
* FieldMappingAutoGenerator - Automatically generates field mappings from view definitions
*
* This tool solves the critical challenge of maintaining 450+ field mappings by:
* 1. Parsing view definitions to extract columns
* 2. Tracing each column back to its source table/column
* 3. Generating TypeScript field mapping definitions
* 4. Supporting both legacy table paths and new view paths
*/
import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import { getLogger } from '../../logging/Logger.js';
const logger = getLogger();
export class FieldMappingAutoGenerator {
dbPath;
db;
viewDefinitions = new Map();
generatedMappings = new Map();
constructor(dbPath) {
this.dbPath = dbPath;
this.db = new Database(dbPath);
}
/**
* Main entry point - generates all field mappings
*/
async generateComprehensiveMappings() {
logger.debug('Starting field mapping auto-generation...');
// Step 1: Parse all view definitions
await this.parseAllViewDefinitions();
// Step 2: Extract columns from actual views in database
await this.extractViewColumnsFromDB();
// Step 3: Load existing field mappings for comparison
await this.loadExistingMappings();
// Step 4: Generate new mappings for view columns
await this.generateViewMappings();
// Step 5: Merge with legacy mappings
await this.mergeLegacyMappings();
// Step 6: Generate TypeScript code
await this.generateTypeScriptCode();
logger.debug(`Generated ${this.generatedMappings.size} field mappings`);
}
/**
* Parse SQL view definitions from files
*/
async parseAllViewDefinitions() {
const viewsDir = path.join(__dirname, '../views');
const files = fs.readdirSync(viewsDir).filter(f => f.endsWith('.sql'));
for (const file of files) {
const content = fs.readFileSync(path.join(viewsDir, file), 'utf-8');
const viewDef = this.parseViewSQL(content);
if (viewDef) {
this.viewDefinitions.set(viewDef.viewName, viewDef);
logger.debug(`Parsed view: ${viewDef.viewName} (${viewDef.columns.length} columns)`);
}
}
}
/**
* Parse a CREATE VIEW SQL statement
*/
parseViewSQL(sql) {
// Extract view name
const viewMatch = sql.match(/CREATE\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/i);
if (!viewMatch)
return null;
const viewName = viewMatch[1];
const columns = [];
const sourceTables = [];
// Extract SELECT clause
const selectMatch = sql.match(/SELECT\s+([\s\S]+?)\s+FROM/i);
if (!selectMatch)
return null;
const selectClause = selectMatch[1];
// Parse column definitions
const columnRegex = /(?:(?:CASE[\s\S]+?END)|(?:COALESCE\([^)]+\))|(?:CAST\([^)]+\))|(?:[a-zA-Z_][\w.]*(?:\([^)]*\))?))(?:\s+as\s+(\w+))?/gi;
let match;
while ((match = columnRegex.exec(selectClause)) !== null) {
const fullExpression = match[0];
const alias = match[1];
// Skip comments
if (fullExpression.trim().startsWith('--'))
continue;
const column = this.parseColumnExpression(fullExpression, alias);
if (column) {
columns.push(column);
}
}
// Extract source tables from FROM clause
const fromMatch = sql.match(/FROM\s+(\w+)(?:\s+(\w+))?/i);
if (fromMatch) {
sourceTables.push(fromMatch[1]);
}
// Extract JOINed tables
const joinRegex = /JOIN\s+(\w+)(?:\s+(\w+))?/gi;
while ((match = joinRegex.exec(sql)) !== null) {
sourceTables.push(match[1]);
}
return {
viewName,
columns,
sourceTables: [...new Set(sourceTables)],
joins: []
};
}
/**
* Parse individual column expression
*/
parseColumnExpression(expression, alias) {
const trimmed = expression.trim();
// Handle CASE expressions
if (trimmed.toUpperCase().startsWith('CASE')) {
return {
columnName: alias || 'computed_field',
columnAlias: alias,
transformation: 'CASE',
isComputed: true
};
}
// Handle COALESCE
if (trimmed.toUpperCase().startsWith('COALESCE')) {
const innerMatch = trimmed.match(/COALESCE\(([^,]+)/i);
if (innerMatch) {
const sourceField = this.extractSourceField(innerMatch[1]);
return {
columnName: alias || sourceField.column,
columnAlias: alias,
sourceTable: sourceField.table,
sourceColumn: sourceField.column,
transformation: 'COALESCE',
isComputed: true
};
}
}
// Handle CAST
if (trimmed.toUpperCase().startsWith('CAST')) {
const innerMatch = trimmed.match(/CAST\(([^)]+)\s+AS\s+(\w+)\)/i);
if (innerMatch) {
const sourceField = this.extractSourceField(innerMatch[1]);
return {
columnName: alias || sourceField.column,
columnAlias: alias,
sourceTable: sourceField.table,
sourceColumn: sourceField.column,
transformation: 'CAST',
dataType: innerMatch[2],
isComputed: true
};
}
}
// Handle JSON_EXTRACT
if (trimmed.includes('JSON_EXTRACT')) {
const jsonMatch = trimmed.match(/JSON_EXTRACT\(([^,]+),\s*'([^']+)'\)/i);
if (jsonMatch) {
const sourceField = this.extractSourceField(jsonMatch[1]);
return {
columnName: alias || 'json_field',
columnAlias: alias,
sourceTable: sourceField.table,
sourceColumn: sourceField.column,
jsonPath: jsonMatch[2],
transformation: 'JSON_EXTRACT',
isComputed: true
};
}
}
// Handle simple field references (table.column or just column)
const fieldMatch = trimmed.match(/^([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)$|^([a-zA-Z_]\w*)$/);
if (fieldMatch) {
if (fieldMatch[1] && fieldMatch[2]) {
// table.column format
return {
columnName: alias || fieldMatch[2],
columnAlias: alias,
sourceTable: fieldMatch[1],
sourceColumn: fieldMatch[2],
isComputed: false
};
}
else if (fieldMatch[3]) {
// just column name
return {
columnName: alias || fieldMatch[3],
columnAlias: alias,
sourceColumn: fieldMatch[3],
isComputed: false
};
}
}
return null;
}
/**
* Extract source table and column from expression
*/
extractSourceField(expression) {
const trimmed = expression.trim();
const match = trimmed.match(/^([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)$|^([a-zA-Z_]\w*)$/);
if (match) {
if (match[1] && match[2]) {
return { table: match[1], column: match[2] };
}
else if (match[3]) {
return { column: match[3] };
}
}
return { column: 'unknown' };
}
/**
* Extract actual column information from database views
*/
async extractViewColumnsFromDB() {
for (const [viewName, viewDef] of this.viewDefinitions) {
try {
// Get column info from SQLite (better-sqlite3 is synchronous)
const stmt = this.db.prepare(`PRAGMA table_info(${viewName})`);
const columns = stmt.all();
// Match with parsed definitions
for (const col of columns) {
const existingCol = viewDef.columns.find(c => c.columnName === col.name || c.columnAlias === col.name);
if (existingCol) {
existingCol.dataType = col.type;
}
else {
// Column exists in view but wasn't parsed - add it
viewDef.columns.push({
columnName: col.name,
dataType: col.type,
isComputed: false
});
}
}
logger.debug(`Verified ${viewName}: ${columns.length} columns in database`);
}
catch (error) {
logger.warn(` Could not verify ${viewName} in database: ${error}`);
}
}
}
/**
* Load existing field mappings from FieldLocalityResolver
*/
async loadExistingMappings() {
// This would load from the actual FieldLocalityResolver.ts file
// For now, we'll create some sample mappings
const sampleMappings = {
'enabled': {
tables: ['flag_environments'],
type: 'boolean',
platformSpecific: false
},
'rollout_percentage': {
tables: ['rules'],
column: 'percentage_included',
type: 'number',
platformSpecific: false
},
'flag_key': {
tables: ['flags'],
column: 'key',
type: 'string',
platformSpecific: false
}
};
logger.debug(`Loaded ${Object.keys(sampleMappings).length} existing field mappings`);
}
/**
* Generate new mappings for view columns
*/
async generateViewMappings() {
for (const [viewName, viewDef] of this.viewDefinitions) {
for (const column of viewDef.columns) {
const fieldName = column.columnAlias || column.columnName;
// Skip if this is a duplicate of an existing field
if (this.generatedMappings.has(fieldName)) {
const existing = this.generatedMappings.get(fieldName);
// Add this view as an additional source
if (!existing.view.viewName.includes(viewName)) {
existing.view.viewName += `, ${viewName}`;
}
continue;
}
const mapping = {
fieldName,
legacy: {
tables: column.sourceTable ? [column.sourceTable] : [],
column: column.sourceColumn,
jsonPath: column.jsonPath
},
view: {
viewName,
columnName: column.columnName,
preComputed: column.isComputed || false,
performance: this.determinePerformance(column)
},
metadata: {
type: this.inferDataType(column),
description: this.generateDescription(column),
platformSpecific: false
},
routing: {
preferView: true,
viewOnlyFeatures: column.isComputed ? ['pre-calculated'] : [],
fallbackRequired: false
}
};
this.generatedMappings.set(fieldName, mapping);
}
}
logger.debug(`Generated ${this.generatedMappings.size} view-based field mappings`);
}
/**
* Determine performance characteristics of a field
*/
determinePerformance(column) {
if (column.jsonPath)
return 'medium';
if (column.transformation === 'CASE')
return 'medium';
if (column.isComputed)
return 'medium';
return 'fast';
}
/**
* Infer data type from column information
*/
inferDataType(column) {
if (column.dataType) {
if (column.dataType.toUpperCase().includes('INT'))
return 'number';
if (column.dataType.toUpperCase().includes('REAL'))
return 'number';
if (column.dataType.toUpperCase().includes('BOOL'))
return 'boolean';
if (column.dataType.toUpperCase().includes('TEXT'))
return 'string';
}
// Infer from column name
if (column.columnName.includes('count') || column.columnName.includes('percentage'))
return 'number';
if (column.columnName.includes('enabled') || column.columnName.includes('archived'))
return 'boolean';
if (column.columnName.includes('time') || column.columnName.includes('date'))
return 'datetime';
return 'string';
}
/**
* Generate human-readable description
*/
generateDescription(column) {
const name = column.columnAlias || column.columnName;
if (column.isComputed) {
if (column.transformation === 'CASE')
return `Computed field: ${name}`;
if (column.transformation === 'COALESCE')
return `${name} with null handling`;
if (column.transformation === 'CAST')
return `${name} converted to ${column.dataType}`;
if (column.jsonPath)
return `Extracted from JSON: ${column.jsonPath}`;
}
// Generate from name
return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
/**
* Merge with legacy mappings
*/
async mergeLegacyMappings() {
// This would merge with existing FieldLocalityResolver mappings
logger.debug('Merged legacy mappings with new view mappings');
}
/**
* Generate TypeScript code
*/
async generateTypeScriptCode() {
const outputPath = path.join(__dirname, '../generated/ViewFieldMappings.generated.ts');
let code = `/**
* Auto-generated field mappings for view-based architecture
* Generated on: ${new Date().toISOString()}
* Total mappings: ${this.generatedMappings.size}
*/
export interface ViewFieldMapping {
fieldName: string;
legacy: {
tables: string[];
column?: string;
joins?: any[];
jsonPath?: string;
};
view: {
viewName: string;
columnName: string;
preComputed: boolean;
performance: 'fast' | 'medium' | 'slow';
};
metadata: {
type: string;
description?: string;
platformSpecific?: boolean;
};
routing: {
preferView: boolean;
viewOnlyFeatures?: string[];
fallbackRequired?: boolean;
};
}
export const VIEW_FIELD_MAPPINGS: Record<string, ViewFieldMapping> = {
`;
// Generate mapping entries
for (const [fieldName, mapping] of this.generatedMappings) {
code += ` '${fieldName}': ${JSON.stringify(mapping, null, 4).replace(/^/gm, ' ').trim()},\n`;
}
code += `};
// Export convenience functions
export function getViewMapping(fieldName: string): ViewFieldMapping | undefined {
return VIEW_FIELD_MAPPINGS[fieldName];
}
export function getViewColumnName(fieldName: string): string | undefined {
return VIEW_FIELD_MAPPINGS[fieldName]?.view.columnName;
}
export function shouldUseView(fieldName: string): boolean {
return VIEW_FIELD_MAPPINGS[fieldName]?.routing.preferView ?? false;
}
export function getFieldType(fieldName: string): string {
return VIEW_FIELD_MAPPINGS[fieldName]?.metadata.type ?? 'string';
}
`;
// Create directory if it doesn't exist
const dir = path.dirname(outputPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, code);
logger.info(`Generated TypeScript mappings at: ${outputPath}`);
}
/**
* Close database connection
*/
close() {
this.db.close();
}
}
// CLI interface
if (require.main === module) {
const dbPath = path.join(__dirname, '../../../data/optimizely-cache.db');
const generator = new FieldMappingAutoGenerator(dbPath);
generator.generateComprehensiveMappings()
.then(() => {
logger.info('Field mapping generation complete!');
generator.close();
})
.catch((error) => {
logger.error('Error generating field mappings:', error);
generator.close();
process.exit(1);
});
}
//# sourceMappingURL=FieldMappingAutoGenerator.js.map