UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

568 lines 21.5 kB
"use strict"; /** * Constraint Discovery Engine for PostgreSQL * Parses triggers, functions, and constraints to extract business logic rules * Part of supa-seed v2.2.0 constraint-aware architecture */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConstraintDiscoveryEngine = void 0; const logger_1 = require("../../core/utils/logger"); class ConstraintDiscoveryEngine { constructor(client) { this.discoveryCache = new Map(); this.functionCache = new Map(); this.client = client; } /** * Main entry point: discover all constraints for schema tables */ async discoverConstraints(tableNames) { const cacheKey = tableNames.sort().join(','); if (this.discoveryCache.has(cacheKey)) { logger_1.Logger.debug('Using cached constraint discovery results'); return this.discoveryCache.get(cacheKey); } logger_1.Logger.info('🔍 Starting PostgreSQL constraint discovery...'); const startTime = Date.now(); const metadata = { tables: [], businessRules: [], dependencies: [], triggers: [], functions: [], confidence: 0, discoveryTimestamp: new Date().toISOString() }; try { // Step 1: Discover all triggers for the tables const triggers = await this.discoverTriggers(tableNames); logger_1.Logger.debug(`Found ${triggers.length} triggers`); // Step 2: Discover and parse functions referenced by triggers const functions = await this.discoverTriggerFunctions(triggers); logger_1.Logger.debug(`Found ${functions.length} constraint functions`); // Step 3: Parse business rules from functions const businessRules = await this.parseBusinessLogicFromFunctions(functions); logger_1.Logger.debug(`Extracted ${businessRules.length} business rules`); // Step 4: Build table constraint metadata metadata.tables = await this.buildTableConstraints(tableNames, triggers, businessRules); // Step 5: Extract dependencies from business rules metadata.dependencies = await this.extractDependencies(businessRules); // Step 6: Process triggers into rules metadata.triggers = await this.processTriggerRules(triggers, functions); // Step 7: Process functions into rules metadata.functions = await this.processFunctionRules(functions); // Aggregate all business rules metadata.businessRules = businessRules; // Calculate confidence score metadata.confidence = this.calculateConfidenceScore(metadata); const duration = Date.now() - startTime; logger_1.Logger.success(`✅ Constraint discovery completed in ${duration}ms (confidence: ${(metadata.confidence * 100).toFixed(1)}%)`); // Cache results this.discoveryCache.set(cacheKey, metadata); return metadata; } catch (error) { logger_1.Logger.error('PostgreSQL constraint discovery failed:', error); throw new Error(`Constraint discovery failed: ${error.message}`); } } /** * Discover all triggers for specified tables */ async discoverTriggers(tableNames) { const triggers = []; try { // Query PostgreSQL system catalogs for trigger information const triggerQuery = ` SELECT t.tgname as trigger_name, c.relname as table_name, n.nspname as table_schema, CASE t.tgtype & 66 WHEN 2 THEN 'BEFORE' WHEN 64 THEN 'INSTEAD OF' ELSE 'AFTER' END as timing, CASE t.tgtype & 28 WHEN 4 THEN ARRAY['INSERT'] WHEN 8 THEN ARRAY['DELETE'] WHEN 12 THEN ARRAY['INSERT', 'DELETE'] WHEN 16 THEN ARRAY['UPDATE'] WHEN 20 THEN ARRAY['INSERT', 'UPDATE'] WHEN 24 THEN ARRAY['UPDATE', 'DELETE'] WHEN 28 THEN ARRAY['INSERT', 'UPDATE', 'DELETE'] ELSE ARRAY[]::text[] END as events, p.proname as function_name, pn.nspname as function_schema, t.tgenabled = 'O' as is_enabled, pg_get_triggerdef(t.oid) as trigger_definition FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid JOIN pg_proc p ON t.tgfoid = p.oid JOIN pg_namespace pn ON p.pronamespace = pn.oid WHERE n.nspname = 'public' AND c.relname = ANY($1) AND NOT t.tgisinternal ORDER BY c.relname, t.tgname `; const { data: triggerData, error } = await this.client.rpc('exec_sql', { sql: triggerQuery, params: [tableNames] }); if (error) { logger_1.Logger.warn('Failed to query pg_trigger, using fallback method'); return this.fallbackTriggerDiscovery(tableNames); } if (triggerData && Array.isArray(triggerData)) { for (const row of triggerData) { triggers.push({ name: row.trigger_name, tableName: row.table_name, schema: row.table_schema, timing: row.timing, events: row.events || [], functionName: row.function_name, functionSchema: row.function_schema, isEnabled: row.is_enabled }); } } } catch (error) { logger_1.Logger.warn(`Trigger discovery error: ${error.message}, using fallback`); return this.fallbackTriggerDiscovery(tableNames); } return triggers; } /** * Fallback trigger discovery using information_schema */ async fallbackTriggerDiscovery(tableNames) { const triggers = []; try { const { data: triggerData, error } = await this.client .from('information_schema.triggers') .select(` trigger_name, event_object_table, trigger_schema, action_timing, event_manipulation, action_statement `) .eq('trigger_schema', 'public') .in('event_object_table', tableNames); if (!error && triggerData) { for (const row of triggerData) { // Extract function name from action_statement (e.g., "EXECUTE PROCEDURE function_name()") const actionStatement = row.action_statement; const functionMatch = actionStatement.match(/EXECUTE (?:PROCEDURE|FUNCTION)\s+([^(]+)/i); const functionName = functionMatch ? functionMatch[1].trim() : 'unknown'; triggers.push({ name: row.trigger_name, tableName: row.event_object_table, schema: row.trigger_schema, timing: row.action_timing, events: [row.event_manipulation], functionName, functionSchema: 'public', isEnabled: true }); } } } catch (error) { logger_1.Logger.debug(`Fallback trigger discovery failed: ${error.message}`); } return triggers; } /** * Discover and load function definitions for triggers */ async discoverTriggerFunctions(triggers) { const functions = []; const functionNames = [...new Set(triggers.map(t => t.functionName))]; for (const functionName of functionNames) { if (this.functionCache.has(functionName)) { functions.push(this.functionCache.get(functionName)); continue; } try { const func = await this.loadFunctionDefinition(functionName); if (func) { functions.push(func); this.functionCache.set(functionName, func); } } catch (error) { logger_1.Logger.warn(`Failed to load function ${functionName}: ${error.message}`); } } return functions; } /** * Load a PostgreSQL function definition */ async loadFunctionDefinition(functionName) { try { const functionQuery = ` SELECT p.proname as function_name, n.nspname as schema_name, pg_get_functiondef(p.oid) as definition, t.typname as return_type, p.prolang as language_oid, l.lanname as language, p.provolatile as volatility, p.pronargs as param_count, p.proargnames as param_names, p.proargtypes as param_types FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid JOIN pg_type t ON p.prorettype = t.oid JOIN pg_language l ON p.prolang = l.oid WHERE p.proname = $1 AND n.nspname = 'public' LIMIT 1 `; const { data: funcData, error } = await this.client.rpc('exec_sql', { sql: functionQuery, params: [functionName] }); if (error || !funcData || !Array.isArray(funcData) || funcData.length === 0) { return null; } const row = funcData[0]; return { name: row.function_name, schema: row.schema_name, definition: row.definition, returnType: row.return_type, parameters: [], // Would parse from param_names/param_types if needed language: row.language, volatility: this.mapVolatility(row.volatility) }; } catch (error) { logger_1.Logger.debug(`Failed to load function definition for ${functionName}: ${error.message}`); return null; } } /** * Parse business logic rules from PostgreSQL functions */ async parseBusinessLogicFromFunctions(functions) { const businessRules = []; for (const func of functions) { try { const rules = await this.parseBusinessRulesFromFunction(func); businessRules.push(...rules); } catch (error) { logger_1.Logger.warn(`Failed to parse business rules from function ${func.name}: ${error.message}`); } } return businessRules; } /** * Parse business rules from a single function definition */ async parseBusinessRulesFromFunction(func) { const rules = []; const definition = func.definition.toLowerCase(); // Pattern 1: RAISE EXCEPTION with condition const exceptionPatterns = [ /if\s+not\s+exists\s*\(\s*select[^)]+from\s+(\w+)[^)]+where[^)]+(\w+)\s*=\s*(\w+)[^)]*\)\s+then\s+raise\s+exception\s+'([^']+)'/gi, /if\s+([^)]+)\s+then\s+raise\s+exception\s+'([^']+)'/gi ]; for (const pattern of exceptionPatterns) { let match; while ((match = pattern.exec(definition)) !== null) { const rule = this.extractRuleFromException(match, func); if (rule) { rules.push(rule); } } } // Pattern 2: Conditional logic with table lookups const conditionalPatterns = [ /select[^;]+from\s+(\w+)[^;]+where[^;]+(\w+)\s*=\s*(\w+)/gi ]; for (const pattern of conditionalPatterns) { let match; while ((match = pattern.exec(definition)) !== null) { const rule = this.extractRuleFromConditional(match, func); if (rule) { rules.push(rule); } } } return rules; } /** * Extract business rule from RAISE EXCEPTION pattern */ extractRuleFromException(match, func) { const fullMatch = match[0]; const errorMessage = match[match.length - 1]; // Last capture group is usually the error message // Parse the table and condition from the SQL pattern const tableMatch = fullMatch.match(/from\s+(\w+)/i); const conditionMatch = fullMatch.match(/where[^)]+(\w+)\s*=\s*(\w+)/i); if (!tableMatch) { return null; } const table = tableMatch[1]; const condition = conditionMatch ? `${conditionMatch[1]} = ${conditionMatch[2]}` : 'complex_condition'; // Generate auto-fix suggestion const autoFix = conditionMatch ? { type: 'set_field', description: `Set ${conditionMatch[1]} to ${conditionMatch[2]} to satisfy constraint`, confidence: 0.8, action: { table, field: conditionMatch[1], value: conditionMatch[2] === 'true' ? true : conditionMatch[2] } } : undefined; return { id: `${func.name}_${table}_${Date.now()}`, name: `${func.name}_constraint`, type: 'validation', table, condition, action: 'deny', errorMessage, autoFix, confidence: 0.9, sqlPattern: fullMatch, dependencies: [table] }; } /** * Extract business rule from conditional pattern */ extractRuleFromConditional(match, func) { const table = match[1]; const field = match[2]; const value = match[3]; return { id: `${func.name}_${table}_${field}_${Date.now()}`, name: `${func.name}_dependency`, type: 'dependency', table, condition: `${field} = ${value}`, action: 'require', confidence: 0.7, sqlPattern: match[0], dependencies: [table] }; } /** * Build table-specific constraint metadata */ async buildTableConstraints(tableNames, triggers, businessRules) { const tableConstraints = []; for (const tableName of tableNames) { const tableTriggers = triggers.filter(t => t.tableName === tableName); const tableRules = businessRules.filter(r => r.table === tableName); const tableDependencies = await this.extractTableDependencies(tableName, businessRules); const triggerRules = tableTriggers.map(trigger => ({ triggerName: trigger.name, tableName: trigger.tableName, functionName: trigger.functionName, extractedRules: businessRules.filter(r => r.sqlPattern.includes(trigger.functionName)), rawDefinition: trigger.condition || '', parsedSuccessfully: true })); tableConstraints.push({ tableName, constraints: tableRules, triggers: triggerRules, dependencies: tableDependencies }); } return tableConstraints; } /** * Extract dependencies from business rules */ async extractDependencies(businessRules) { const dependencies = []; for (const rule of businessRules) { for (const depTable of rule.dependencies) { if (depTable !== rule.table) { dependencies.push({ fromTable: rule.table, toTable: depTable, relationship: rule.action === 'require' ? 'required' : 'conditional', condition: rule.condition, constraint: rule.name }); } } } return dependencies; } /** * Extract table-specific dependencies */ async extractTableDependencies(tableName, businessRules) { return businessRules .filter(rule => rule.table === tableName) .flatMap(rule => rule.dependencies .filter(dep => dep !== tableName) .map(dep => ({ fromTable: tableName, toTable: dep, relationship: rule.action === 'require' ? 'required' : 'conditional', condition: rule.condition, constraint: rule.name }))); } /** * Process triggers into rule metadata */ async processTriggerRules(triggers, functions) { const triggerRules = []; for (const trigger of triggers) { const func = functions.find(f => f.name === trigger.functionName); const extractedRules = func ? await this.parseBusinessRulesFromFunction(func) : []; triggerRules.push({ triggerName: trigger.name, tableName: trigger.tableName, functionName: trigger.functionName, extractedRules, rawDefinition: func?.definition || '', parsedSuccessfully: func !== undefined }); } return triggerRules; } /** * Process functions into rule metadata */ async processFunctionRules(functions) { const functionRules = []; for (const func of functions) { const extractedRules = await this.parseBusinessRulesFromFunction(func); functionRules.push({ functionName: func.name, schema: func.schema, extractedRules, rawDefinition: func.definition, parsedSuccessfully: extractedRules.length > 0 }); } return functionRules; } /** * Build dependency graph for table creation ordering */ async buildDependencyGraph(constraints) { const nodes = []; const edges = []; const tableSet = new Set(); // Collect all tables constraints.dependencies.forEach(dep => { tableSet.add(dep.fromTable); tableSet.add(dep.toTable); }); // Build nodes for (const table of tableSet) { const dependencies = constraints.dependencies .filter(dep => dep.fromTable === table) .map(dep => dep.toTable); const dependents = constraints.dependencies .filter(dep => dep.toTable === table) .map(dep => dep.fromTable); nodes.push({ table, dependencies, dependents }); } // Build edges constraints.dependencies.forEach(dep => { edges.push({ from: dep.fromTable, to: dep.toTable, type: dep.relationship, constraint: dep.constraint }); }); // Detect cycles const cycles = this.detectCycles(nodes, edges); // Calculate creation order const creationOrder = this.calculateCreationOrder(nodes, edges); return { nodes, edges, cycles, creationOrder }; } /** * Utility methods */ mapVolatility(volatility) { switch (volatility) { case 'i': return 'IMMUTABLE'; case 's': return 'STABLE'; case 'v': return 'VOLATILE'; default: return 'VOLATILE'; } } calculateConfidenceScore(metadata) { let totalScore = 0; let totalRules = 0; metadata.businessRules.forEach(rule => { totalScore += rule.confidence; totalRules++; }); return totalRules > 0 ? totalScore / totalRules : 0; } detectCycles(nodes, edges) { // Simplified cycle detection - would implement proper algorithm return []; } calculateCreationOrder(nodes, edges) { // Topological sort implementation const visited = new Set(); const visiting = new Set(); const order = []; const visit = (table) => { if (visiting.has(table)) { // Cycle detected - handle gracefully return; } if (visited.has(table)) { return; } visiting.add(table); const node = nodes.find(n => n.table === table); if (node) { node.dependencies.forEach(dep => visit(dep)); } visiting.delete(table); visited.add(table); order.push(table); }; nodes.forEach(node => { if (!visited.has(node.table)) { visit(node.table); } }); return order; } /** * Clear discovery cache */ clearCache() { this.discoveryCache.clear(); this.functionCache.clear(); } } exports.ConstraintDiscoveryEngine = ConstraintDiscoveryEngine; //# sourceMappingURL=constraint-discovery-engine.js.map