UNPKG

supa-seed

Version:

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

583 lines 24.8 kB
"use strict"; /** * Custom Relationship Manager for Epic 7: Configuration Extensibility Framework * Manages custom table relationship definitions and validation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomRelationshipManager = void 0; const logger_1 = require("../core/utils/logger"); class CustomRelationshipManager { constructor(client) { this.relationshipCache = new Map(); this.client = client; } /** * Comprehensive validation of custom relationship definitions */ async validateCustomRelationships(relationships) { const errors = []; const warnings = []; const recommendations = []; if (!relationships?.enabled) { return { valid: true, errors, warnings: ['Custom relationships disabled'], recommendations: ['Consider enabling custom relationships for better data modeling'], relationshipGraph: { nodes: [], edges: [], cycles: [], orphanTables: [] } }; } logger_1.Logger.info('🔗 Validating custom relationship definitions...'); try { // Build relationship graph const relationshipGraph = await this.buildRelationshipGraph(relationships); // Validate individual relationships if (relationships.relationships) { for (const relationship of relationships.relationships) { const relationshipErrors = await this.validateSingleRelationship(relationship); errors.push(...relationshipErrors); } } // Validate junction tables if (relationships.junctionTables) { for (const junctionTable of relationships.junctionTables) { const junctionErrors = await this.validateJunctionTable(junctionTable); errors.push(...junctionErrors); } } // Validate inheritance rules if (relationships.inheritanceRules) { for (const inheritanceRule of relationships.inheritanceRules) { const inheritanceErrors = await this.validateInheritanceRule(inheritanceRule); errors.push(...inheritanceErrors); } } // Analyze relationship graph for issues const graphAnalysis = this.analyzeRelationshipGraph(relationshipGraph); warnings.push(...graphAnalysis.warnings); recommendations.push(...graphAnalysis.recommendations); // Generate final recommendations recommendations.push(`Validated ${relationships.relationships?.length || 0} custom relationships`); recommendations.push(`Validated ${relationships.junctionTables?.length || 0} junction tables`); recommendations.push(`Validated ${relationships.inheritanceRules?.length || 0} inheritance rules`); if (relationshipGraph.cycles.length > 0) { warnings.push(`Detected ${relationshipGraph.cycles.length} circular dependencies in relationship graph`); recommendations.push('Consider breaking circular dependencies with nullable foreign keys or junction tables'); } logger_1.Logger.info(`✅ Custom relationship validation completed: ${errors.length} errors, ${warnings.length} warnings`); return { valid: errors.length === 0, errors, warnings, recommendations, relationshipGraph }; } catch (error) { logger_1.Logger.error('Custom relationship validation failed:', error); return { valid: false, errors: [`Validation error: ${error.message}`], warnings: [], recommendations: ['Fix validation errors and retry'], relationshipGraph: { nodes: [], edges: [], cycles: [], orphanTables: [] } }; } } /** * Generate execution plan for relationship seeding */ async generateRelationshipExecutionPlan(relationships) { if (!relationships?.enabled || !relationships.relationships) { return { executionOrder: [], batchGroups: [], junctionTableCreation: [], estimatedComplexity: 'low' }; } logger_1.Logger.info('📋 Generating relationship execution plan...'); const relationshipGraph = await this.buildRelationshipGraph(relationships); // Perform topological sort to determine execution order const executionOrder = this.topologicalSortTables(relationshipGraph); // Group tables into batches that can execute in parallel const batchGroups = this.createBatchGroups(relationshipGraph, executionOrder); // Plan junction table creation const junctionTableCreation = this.planJunctionTableCreation(relationships.junctionTables || []); // Estimate complexity const estimatedComplexity = this.estimateRelationshipComplexity(relationshipGraph, relationships); const plan = { executionOrder, batchGroups, junctionTableCreation, estimatedComplexity }; logger_1.Logger.info(`✅ Generated execution plan: ${executionOrder.length} tables, ${batchGroups.length} batch groups`); return plan; } /** * Create custom relationship generators */ createRelationshipGenerators(relationships) { const generators = new Map(); for (const relationship of relationships) { const generatorKey = `${relationship.fromTable}_to_${relationship.toTable}`; switch (relationship.generationStrategy) { case 'sequential': generators.set(generatorKey, this.createSequentialGenerator(relationship)); break; case 'random': generators.set(generatorKey, this.createRandomGenerator(relationship)); break; case 'weighted': generators.set(generatorKey, this.createWeightedGenerator(relationship)); break; case 'custom': if (relationship.customGenerator) { generators.set(generatorKey, this.createCustomGenerator(relationship)); } break; } } return generators; } /** * Apply inheritance rules to data */ applyInheritanceRules(data, inheritanceRules) { const processedData = { ...data }; for (const rule of inheritanceRules) { if (!processedData[rule.parentTable] || !processedData[rule.childTable]) { continue; } const parentData = processedData[rule.parentTable]; const childData = processedData[rule.childTable]; processedData[rule.childTable] = childData.map(childRecord => { // Find matching parent record (assuming ID-based inheritance) const parentRecord = parentData.find(parent => parent.id === childRecord.parent_id); if (!parentRecord) { return childRecord; } // Apply inheritance based on strategy switch (rule.overrideStrategy) { case 'extend': return { ...childRecord, ...this.pickFields(parentRecord, rule.inheritedFields) }; case 'replace': return { ...this.pickFields(parentRecord, rule.inheritedFields), ...childRecord }; case 'merge': return this.mergeRecords(this.pickFields(parentRecord, rule.inheritedFields), childRecord); default: return childRecord; } }); } return processedData; } /** * Private helper methods */ async validateSingleRelationship(relationship) { const errors = []; // Basic validation if (!relationship.id) { errors.push('Relationship ID is required'); } if (!relationship.fromTable || !relationship.toTable) { errors.push(`Relationship '${relationship.id}' must have fromTable and toTable`); } if (!relationship.fromColumn || !relationship.toColumn) { errors.push(`Relationship '${relationship.id}' must have fromColumn and toColumn`); } if (!['one_to_one', 'one_to_many', 'many_to_many'].includes(relationship.relationshipType)) { errors.push(`Invalid relationship type '${relationship.relationshipType}' for relationship '${relationship.id}'`); } if (!['sequential', 'random', 'weighted', 'custom'].includes(relationship.generationStrategy)) { errors.push(`Invalid generation strategy '${relationship.generationStrategy}' for relationship '${relationship.id}'`); } // Custom generator validation if (relationship.generationStrategy === 'custom' && !relationship.customGenerator) { errors.push(`Custom generation strategy specified but no customGenerator provided for relationship '${relationship.id}'`); } // Table existence validation try { const fromTableExists = await this.tableExists(relationship.fromTable); const toTableExists = await this.tableExists(relationship.toTable); if (!fromTableExists) { errors.push(`From table '${relationship.fromTable}' does not exist for relationship '${relationship.id}'`); } if (!toTableExists) { errors.push(`To table '${relationship.toTable}' does not exist for relationship '${relationship.id}'`); } // Column existence validation if tables exist if (fromTableExists) { const fromColumnExists = await this.columnExists(relationship.fromTable, relationship.fromColumn); if (!fromColumnExists) { errors.push(`From column '${relationship.fromColumn}' does not exist in table '${relationship.fromTable}'`); } } if (toTableExists) { const toColumnExists = await this.columnExists(relationship.toTable, relationship.toColumn); if (!toColumnExists) { errors.push(`To column '${relationship.toColumn}' does not exist in table '${relationship.toTable}'`); } } } catch (error) { errors.push(`Could not validate tables for relationship '${relationship.id}': ${error.message}`); } return errors; } async validateJunctionTable(junctionTable) { const errors = []; if (!junctionTable.tableName) { errors.push('Junction table name is required'); } if (!junctionTable.leftTable || !junctionTable.rightTable) { errors.push(`Junction table '${junctionTable.tableName}' must have leftTable and rightTable`); } if (!junctionTable.leftColumn || !junctionTable.rightColumn) { errors.push(`Junction table '${junctionTable.tableName}' must have leftColumn and rightColumn`); } // Check table existence try { const junctionExists = await this.tableExists(junctionTable.tableName); const leftExists = await this.tableExists(junctionTable.leftTable); const rightExists = await this.tableExists(junctionTable.rightTable); if (!junctionExists) { errors.push(`Junction table '${junctionTable.tableName}' does not exist`); } if (!leftExists) { errors.push(`Left table '${junctionTable.leftTable}' does not exist for junction table '${junctionTable.tableName}'`); } if (!rightExists) { errors.push(`Right table '${junctionTable.rightTable}' does not exist for junction table '${junctionTable.tableName}'`); } } catch (error) { errors.push(`Could not validate junction table '${junctionTable.tableName}': ${error.message}`); } return errors; } async validateInheritanceRule(inheritanceRule) { const errors = []; if (!inheritanceRule.parentTable || !inheritanceRule.childTable) { errors.push('Inheritance rule must have parentTable and childTable'); } if (!inheritanceRule.inheritedFields || inheritanceRule.inheritedFields.length === 0) { errors.push('Inheritance rule must specify inheritedFields'); } if (!['extend', 'replace', 'merge'].includes(inheritanceRule.overrideStrategy)) { errors.push(`Invalid override strategy '${inheritanceRule.overrideStrategy}' - must be one of: extend, replace, merge`); } // Check table existence try { const parentExists = await this.tableExists(inheritanceRule.parentTable); const childExists = await this.tableExists(inheritanceRule.childTable); if (!parentExists) { errors.push(`Parent table '${inheritanceRule.parentTable}' does not exist`); } if (!childExists) { errors.push(`Child table '${inheritanceRule.childTable}' does not exist`); } } catch (error) { errors.push(`Could not validate inheritance rule: ${error.message}`); } return errors; } async buildRelationshipGraph(relationships) { const nodes = []; const edges = []; const tableSet = new Set(); // Collect all tables involved in relationships if (relationships?.relationships) { for (const rel of relationships.relationships) { tableSet.add(rel.fromTable); tableSet.add(rel.toTable); } } // Add junction tables if (relationships?.junctionTables) { for (const jt of relationships.junctionTables) { tableSet.add(jt.tableName); tableSet.add(jt.leftTable); tableSet.add(jt.rightTable); } } // Create nodes for (const table of tableSet) { const isJunction = relationships?.junctionTables?.some(jt => jt.tableName === table) || false; const columns = await this.getTableColumns(table); nodes.push({ table, type: isJunction ? 'junction' : 'entity', columns }); } // Create edges if (relationships?.relationships) { for (const rel of relationships.relationships) { edges.push({ from: rel.fromTable, to: rel.toTable, relationship: rel, strength: rel.isRequired ? 'strong' : 'weak' }); } } // Detect cycles const cycles = this.detectCycles(nodes, edges); // Find orphan tables const orphanTables = this.findOrphanTables(nodes, edges); return { nodes, edges, cycles, orphanTables }; } analyzeRelationshipGraph(graph) { const warnings = []; const recommendations = []; // Check for complex many-to-many relationships without junction tables const manyToManyRels = graph.edges.filter(edge => edge.relationship.relationshipType === 'many_to_many'); if (manyToManyRels.length > 0) { recommendations.push(`Consider creating junction tables for ${manyToManyRels.length} many-to-many relationships`); } // Check for high-degree nodes (tables with many relationships) const nodeDegrees = new Map(); graph.edges.forEach(edge => { nodeDegrees.set(edge.from, (nodeDegrees.get(edge.from) || 0) + 1); nodeDegrees.set(edge.to, (nodeDegrees.get(edge.to) || 0) + 1); }); const highDegreeNodes = Array.from(nodeDegrees.entries()) .filter(([, degree]) => degree > 5) .map(([table]) => table); if (highDegreeNodes.length > 0) { warnings.push(`High-degree tables detected: ${highDegreeNodes.join(', ')} - may indicate design issues`); } // Check for orphan tables if (graph.orphanTables.length > 0) { warnings.push(`Orphan tables detected: ${graph.orphanTables.join(', ')} - no relationships defined`); } return { warnings, recommendations }; } topologicalSortTables(graph) { const visited = new Set(); const visiting = new Set(); const result = []; const visit = (table) => { if (visiting.has(table)) { // Cycle detected - handle gracefully return; } if (visited.has(table)) { return; } visiting.add(table); // Visit all dependencies first const dependencies = graph.edges .filter(edge => edge.to === table) .map(edge => edge.from); for (const dep of dependencies) { visit(dep); } visiting.delete(table); visited.add(table); result.push(table); }; // Visit all nodes graph.nodes.forEach(node => visit(node.table)); return result; } createBatchGroups(graph, executionOrder) { const batchGroups = []; const processed = new Set(); for (const table of executionOrder) { if (processed.has(table)) continue; // Find all tables that can be processed in parallel with this one const batch = [table]; const dependencies = this.getTableDependencies(graph, table); // Add tables that have the same dependencies and can be processed in parallel for (const otherTable of executionOrder) { if (otherTable === table || processed.has(otherTable)) continue; const otherDependencies = this.getTableDependencies(graph, otherTable); const canProcessInParallel = this.canProcessInParallel(dependencies, otherDependencies); if (canProcessInParallel) { batch.push(otherTable); } } batch.forEach(t => processed.add(t)); batchGroups.push({ tables: batch, canExecuteInParallel: batch.length > 1, dependencies: Array.from(dependencies) }); } return batchGroups; } planJunctionTableCreation(junctionTables) { return junctionTables.map((jt, index) => ({ junctionTable: jt.tableName, dependsOn: [jt.leftTable, jt.rightTable], executionOrder: index + 1000 // Execute after main tables })); } estimateRelationshipComplexity(graph, relationships) { const nodeCount = graph.nodes.length; const edgeCount = graph.edges.length; const cycleCount = graph.cycles.length; const junctionTableCount = relationships?.junctionTables?.length || 0; if (nodeCount <= 5 && edgeCount <= 10 && cycleCount === 0) { return 'low'; } else if (nodeCount <= 15 && edgeCount <= 30 && cycleCount <= 2) { return 'medium'; } else if (nodeCount <= 50 && edgeCount <= 100 && cycleCount <= 5) { return 'high'; } else { return 'critical'; } } detectCycles(nodes, edges) { const cycles = []; const visited = new Set(); const recStack = new Set(); const dfs = (node, path) => { visited.add(node); recStack.add(node); path.push(node); const neighbors = edges .filter(edge => edge.from === node) .map(edge => edge.to); for (const neighbor of neighbors) { if (!visited.has(neighbor)) { dfs(neighbor, [...path]); } else if (recStack.has(neighbor)) { // Found a cycle const cycleStart = path.indexOf(neighbor); if (cycleStart >= 0) { cycles.push(path.slice(cycleStart)); } } } recStack.delete(node); }; nodes.forEach(node => { if (!visited.has(node.table)) { dfs(node.table, []); } }); return cycles; } findOrphanTables(nodes, edges) { const connectedTables = new Set(); edges.forEach(edge => { connectedTables.add(edge.from); connectedTables.add(edge.to); }); return nodes .filter(node => !connectedTables.has(node.table)) .map(node => node.table); } getTableDependencies(graph, table) { return new Set(graph.edges .filter(edge => edge.to === table) .map(edge => edge.from)); } canProcessInParallel(deps1, deps2) { // Tables can be processed in parallel if they don't depend on each other // and have similar dependency sets const intersection = new Set([...deps1].filter(x => deps2.has(x))); const union = new Set([...deps1, ...deps2]); // Simple heuristic: if they share most dependencies, they can be parallel return intersection.size / union.size > 0.7; } createSequentialGenerator(relationship) { let counter = 0; return (fromId, context) => { counter++; return { [relationship.toColumn]: `${fromId}_${counter}` }; }; } createRandomGenerator(relationship) { return (fromId, context) => { const randomValue = Math.floor(Math.random() * 1000000); return { [relationship.toColumn]: `${fromId}_${randomValue}` }; }; } createWeightedGenerator(relationship) { return (fromId, context) => { // Simple weighted generation based on context const weight = context?.weight || 1; const value = Math.floor(Math.random() * 1000 * weight); return { [relationship.toColumn]: `${fromId}_${value}` }; }; } createCustomGenerator(relationship) { return (fromId, context) => { // Placeholder for custom generator implementation // In a real implementation, this would load and execute the custom function return { [relationship.toColumn]: `custom_${fromId}` }; }; } pickFields(record, fields) { const result = {}; fields.forEach(field => { if (record.hasOwnProperty(field)) { result[field] = record[field]; } }); return result; } mergeRecords(parent, child) { const result = { ...parent }; Object.keys(child).forEach(key => { if (child[key] !== null && child[key] !== undefined) { result[key] = child[key]; } }); return result; } async tableExists(tableName) { try { const { error } = await this.client.from(tableName).select('*').limit(1); return !error; } catch { return false; } } async columnExists(tableName, columnName) { try { const { error } = await this.client.from(tableName).select(columnName).limit(1); return !error; } catch { return false; } } async getTableColumns(tableName) { try { // This would need to be implemented based on available database introspection // For now, return a basic set of columns return ['id', 'created_at', 'updated_at']; } catch { return []; } } } exports.CustomRelationshipManager = CustomRelationshipManager; //# sourceMappingURL=custom-relationship-manager.js.map