UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

462 lines (456 loc) 17.2 kB
/** * 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