UNPKG

supa-seed

Version:

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

532 lines 21.2 kB
"use strict"; /** * Table Relationship Discovery System * Discovers and maps database relationships for proper foreign key handling * Enables intelligent data seeding that respects referential integrity */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RelationshipDiscoverer = void 0; const logger_1 = require("../core/utils/logger"); class RelationshipDiscoverer { constructor(client) { this.schemaInfo = null; this.client = client; } /** * Analyze all table relationships and create seeding strategies */ async analyzeRelationships(schemaInfo) { this.schemaInfo = schemaInfo; logger_1.Logger.info('🔗 Analyzing table relationships for seeding strategy...'); const graph = await this.buildRelationshipGraph(); const strategies = await this.createSeedingStrategies(graph); const warnings = this.identifyWarnings(graph); const recommendations = this.generateRecommendations(graph, strategies); logger_1.Logger.success(`✅ Relationship analysis complete: ${graph.nodes.length} tables, ${graph.edges.length} relationships`); return { graph, strategies, warnings, recommendations }; } /** * Build a complete relationship graph */ async buildRelationshipGraph() { if (!this.schemaInfo) { throw new Error('Schema information not available'); } const nodes = []; const edges = []; // Create nodes for each table for (const table of this.schemaInfo.tables) { const node = await this.createTableNode(table); nodes.push(node); } // Create edges from relationships for (const relationship of this.schemaInfo.relationships) { const edge = await this.createRelationshipEdge(relationship); edges.push(edge); } // Enhance edges with additional analysis await this.enhanceRelationshipEdges(edges); // Update node dependencies based on edges this.updateNodeDependencies(nodes, edges); // Detect cycles const cycles = this.detectCycles(nodes, edges); // Calculate seeding order const seedingOrder = this.calculateSeedingOrder(nodes, edges, cycles); // Calculate dependency levels const dependencyLevels = this.calculateDependencyLevels(nodes, edges); return { nodes, edges, cycles, seedingOrder, dependencyLevels }; } /** * Create a table node with dependency analysis */ async createTableNode(table) { const dependencies = []; const dependents = []; const constraints = []; // Analyze foreign key constraints to find dependencies for (const constraint of table.constraints) { if (constraint.type === 'FOREIGN KEY' && constraint.referencedTable) { dependencies.push(constraint.referencedTable); // Create constraint for required parent if (!this.isColumnNullable(table, constraint.columns[0])) { constraints.push({ type: 'required_parent', description: `Table ${table.name} requires ${constraint.referencedTable} to exist first`, tables: [table.name, constraint.referencedTable], resolution: 'create_parent_first' }); } } } // Determine node type let nodeType = 'intermediate'; if (dependencies.length === 0) { nodeType = 'root'; } else if (dependents.length === 0) { nodeType = 'leaf'; } return { tableName: table.name, nodeType, dependencies, dependents, // Will be populated later seedingPriority: this.calculateSeedingPriority(table, dependencies.length), constraints }; } /** * Create a relationship edge with detailed analysis */ async createRelationshipEdge(relationship) { return { fromTable: relationship.fromTable, toTable: relationship.toTable, foreignKey: relationship.fromColumn, referencedKey: relationship.toColumn, relationship: relationship.relationshipType, cascadeDelete: relationship.cascadeDelete, isNullable: !relationship.isRequired, constraintName: `fk_${relationship.fromTable}_${relationship.fromColumn}` }; } /** * Enhance relationship edges with additional database analysis */ async enhanceRelationshipEdges(edges) { for (const edge of edges) { try { // Check if the foreign key is actually nullable in the database const fromTable = this.schemaInfo?.tables.find(t => t.name === edge.fromTable); if (fromTable) { const column = fromTable.columns.find(c => c.name === edge.foreignKey); if (column) { edge.isNullable = column.isNullable; } } // Enhance relationship type detection edge.relationship = await this.detectRelationshipType(edge); } catch (error) { logger_1.Logger.warn(`Failed to enhance edge ${edge.fromTable}->${edge.toTable}: ${error.message}`); } } } /** * Detect the actual relationship type between tables */ async detectRelationshipType(edge) { try { // Check if foreign key has unique constraint (indicates one-to-one) const fromTable = this.schemaInfo?.tables.find(t => t.name === edge.fromTable); if (fromTable) { const hasUniqueConstraint = fromTable.constraints.some(c => c.type === 'UNIQUE' && c.columns.includes(edge.foreignKey)); if (hasUniqueConstraint) { return 'one_to_one'; } } // Check for junction table pattern (indicates many-to-many) if (this.isJunctionTable(edge.fromTable)) { return 'many_to_many'; } // Default to one-to-many return 'one_to_many'; } catch (error) { return 'one_to_many'; // Safe default } } /** * Check if a table is a junction table (for many-to-many relationships) */ isJunctionTable(tableName) { const table = this.schemaInfo?.tables.find(t => t.name === tableName); if (!table) return false; // Junction tables typically have only foreign keys and maybe an ID const foreignKeyCount = table.constraints.filter(c => c.type === 'FOREIGN KEY').length; const totalColumns = table.columns.length; // Heuristic: if most columns are foreign keys, it's likely a junction table return foreignKeyCount >= 2 && foreignKeyCount >= totalColumns - 2; } /** * Update node dependencies based on edges */ updateNodeDependencies(nodes, edges) { // Clear existing dependencies/dependents for (const node of nodes) { node.dependencies = []; node.dependents = []; } // Rebuild based on edges for (const edge of edges) { const fromNode = nodes.find(n => n.tableName === edge.fromTable); const toNode = nodes.find(n => n.tableName === edge.toTable); if (fromNode && toNode) { // From table depends on To table if (!fromNode.dependencies.includes(edge.toTable)) { fromNode.dependencies.push(edge.toTable); } // To table has From table as dependent if (!toNode.dependents.includes(edge.fromTable)) { toNode.dependents.push(edge.fromTable); } } } // Update node types based on final dependencies for (const node of nodes) { if (node.dependencies.length === 0 && node.dependents.length > 0) { node.nodeType = 'root'; } else if (node.dependencies.length > 0 && node.dependents.length === 0) { node.nodeType = 'leaf'; } else { node.nodeType = 'intermediate'; } } } /** * Detect circular dependencies in the relationship graph */ detectCycles(nodes, edges) { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const hasCycle = (tableName, path) => { if (recursionStack.has(tableName)) { // Found a cycle const cycleStart = path.indexOf(tableName); const cycleTables = path.slice(cycleStart); cycleTables.push(tableName); // Complete the cycle const cycle = this.createTableCycle(cycleTables, edges); cycles.push(cycle); return true; } if (visited.has(tableName)) { return false; } visited.add(tableName); recursionStack.add(tableName); path.push(tableName); const node = nodes.find(n => n.tableName === tableName); if (node) { for (const dependency of node.dependencies) { if (hasCycle(dependency, [...path])) { return true; } } } recursionStack.delete(tableName); return false; }; // Check each node for cycles for (const node of nodes) { if (!visited.has(node.tableName)) { hasCycle(node.tableName, []); } } return cycles; } /** * Create a table cycle with break point strategies */ createTableCycle(tables, edges) { const breakPoints = []; // Find the best places to break the cycle for (let i = 0; i < tables.length - 1; i++) { const fromTable = tables[i]; const toTable = tables[i + 1]; const edge = edges.find(e => e.fromTable === fromTable && e.toTable === toTable); if (edge) { let strategy = 'allow_null'; if (edge.isNullable) { strategy = 'allow_null'; } else { // Check if we can defer the constraint strategy = 'defer_constraint'; } breakPoints.push({ table: fromTable, column: edge.foreignKey, strategy }); } } return { tables: tables.slice(0, -1), // Remove duplicate last element breakPoints }; } /** * Calculate optimal seeding order considering dependencies and cycles */ calculateSeedingOrder(nodes, edges, cycles) { const order = []; const visited = new Set(); // First, add all root nodes (no dependencies) const rootNodes = nodes.filter(n => n.nodeType === 'root').sort((a, b) => b.seedingPriority - a.seedingPriority); for (const node of rootNodes) { order.push(node.tableName); visited.add(node.tableName); } // Then add nodes in dependency order let changed = true; while (changed && visited.size < nodes.length) { changed = false; for (const node of nodes) { if (visited.has(node.tableName)) continue; // Check if all dependencies are satisfied const dependenciesSatisfied = node.dependencies.every(dep => { // Allow cycle break points to be skipped initially const isInCycle = cycles.some(cycle => cycle.tables.includes(node.tableName) && cycle.tables.includes(dep)); return visited.has(dep) || isInCycle; }); if (dependenciesSatisfied) { order.push(node.tableName); visited.add(node.tableName); changed = true; } } } // Add any remaining nodes (shouldn't happen with proper cycle handling) for (const node of nodes) { if (!visited.has(node.tableName)) { order.push(node.tableName); } } return order; } /** * Calculate dependency levels for parallel seeding */ calculateDependencyLevels(nodes, edges) { const levels = new Map(); const visited = new Set(); const calculateLevel = (tableName) => { if (levels.has(tableName)) { return levels.get(tableName); } const node = nodes.find(n => n.tableName === tableName); if (!node || node.dependencies.length === 0) { levels.set(tableName, 0); return 0; } let maxDependencyLevel = -1; for (const dependency of node.dependencies) { if (!visited.has(dependency)) { visited.add(dependency); const depLevel = calculateLevel(dependency); maxDependencyLevel = Math.max(maxDependencyLevel, depLevel); } } const level = maxDependencyLevel + 1; levels.set(tableName, level); return level; }; // Calculate level for each node for (const node of nodes) { if (!visited.has(node.tableName)) { visited.add(node.tableName); calculateLevel(node.tableName); } } return levels; } /** * Create seeding strategies for each table */ async createSeedingStrategies(graph) { const strategies = new Map(); for (const node of graph.nodes) { const strategy = await this.createTableSeedingStrategy(node, graph); strategies.set(node.tableName, strategy); } return strategies; } /** * Create seeding strategy for a specific table */ async createTableSeedingStrategy(node, graph) { const isInCycle = graph.cycles.some(cycle => cycle.tables.includes(node.tableName)); const creationOrder = graph.seedingOrder.indexOf(node.tableName); let strategy = 'independent'; let specialHandling; if (node.dependencies.length === 0) { strategy = 'independent'; } else if (isInCycle) { strategy = 'circular'; specialHandling = { type: 'use_temp_values', instructions: 'Create with null foreign keys initially, then update with batch operation' }; } else if (node.dependencies.length > 0) { strategy = 'dependent'; // Check if any dependencies have complex constraints const hasComplexConstraints = node.constraints.some(c => c.type === 'required_parent'); if (hasComplexConstraints) { specialHandling = { type: 'create_parent_first', instructions: 'Ensure all parent records exist before creating this record' }; } } return { tableName: node.tableName, strategy, prerequisites: node.dependencies, creationOrder, specialHandling }; } /** * Identify potential warnings in the relationship graph */ identifyWarnings(graph) { const warnings = []; // Circular dependency warnings for (const cycle of graph.cycles) { warnings.push({ type: 'circular_dependency', message: `Circular dependency detected between tables: ${cycle.tables.join(' -> ')}`, tables: cycle.tables, severity: 'high', suggestedAction: `Use nullable foreign keys or deferred constraints at: ${cycle.breakPoints.map(bp => `${bp.table}.${bp.column}`).join(', ')}` }); } // Complex relationship warnings const complexNodes = graph.nodes.filter(n => n.dependencies.length > 3); for (const node of complexNodes) { warnings.push({ type: 'complex_relationship', message: `Table ${node.tableName} has ${node.dependencies.length} dependencies`, tables: [node.tableName, ...node.dependencies], severity: 'medium', suggestedAction: `Consider seeding ${node.tableName} after all dependencies are created` }); } return warnings; } /** * Generate recommendations for improved seeding */ generateRecommendations(graph, strategies) { const recommendations = []; // Recommend parallel seeding for independent tables const independentTables = Array.from(strategies.values()).filter(s => s.strategy === 'independent'); if (independentTables.length > 1) { recommendations.push(`${independentTables.length} independent tables can be seeded in parallel: ${independentTables.map(t => t.tableName).join(', ')}`); } // Recommend batch operations for circular dependencies if (graph.cycles.length > 0) { recommendations.push(`${graph.cycles.length} circular dependencies detected. Use batch operations with nullable foreign keys for optimal performance.`); } // Recommend level-based seeding const maxLevel = Math.max(...Array.from(graph.dependencyLevels.values())); if (maxLevel > 3) { recommendations.push(`Deep dependency chain detected (${maxLevel} levels). Consider level-based parallel seeding.`); } return recommendations; } /** * Utility methods */ isColumnNullable(table, columnName) { const column = table.columns.find(c => c.name === columnName); return column?.isNullable ?? true; } calculateSeedingPriority(table, dependencyCount) { // Higher priority for tables with fewer dependencies let priority = 100 - (dependencyCount * 10); // Boost priority for user-related tables if (table.name.includes('user') || table.name.includes('profile') || table.name.includes('account')) { priority += 20; } // Boost priority for core system tables if (table.name.includes('auth') || table.name.includes('role') || table.name.includes('permission')) { priority += 15; } return Math.max(0, priority); } /** * Get seeding order for a specific set of tables */ async getSeedingOrder(tableNames) { if (!this.schemaInfo) { throw new Error('Schema information not available. Call analyzeRelationships first.'); } // Filter the global seeding order to only include requested tables const analysis = await this.analyzeRelationships(this.schemaInfo); return analysis.graph.seedingOrder.filter(table => tableNames.includes(table)); } /** * Check if two tables can be seeded in parallel */ async canSeedInParallel(table1, table2) { if (!this.schemaInfo) { throw new Error('Schema information not available. Call analyzeRelationships first.'); } const analysis = await this.analyzeRelationships(this.schemaInfo); // Check if either table depends on the other const node1 = analysis.graph.nodes.find(n => n.tableName === table1); const node2 = analysis.graph.nodes.find(n => n.tableName === table2); if (!node1 || !node2) return false; const table1DependsOnTable2 = node1.dependencies.includes(table2); const table2DependsOnTable1 = node2.dependencies.includes(table1); return !table1DependsOnTable2 && !table2DependsOnTable1; } /** * Get all tables that can be seeded at a specific dependency level */ async getTablesAtLevel(level) { if (!this.schemaInfo) { throw new Error('Schema information not available. Call analyzeRelationships first.'); } const analysis = await this.analyzeRelationships(this.schemaInfo); const tables = []; for (const [tableName, tableLevel] of analysis.graph.dependencyLevels) { if (tableLevel === level) { tables.push(tableName); } } return tables; } } exports.RelationshipDiscoverer = RelationshipDiscoverer; //# sourceMappingURL=relationship-discoverer.js.map