UNPKG

supa-seed

Version:

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

406 lines 14.9 kB
"use strict"; /** * Dependency Graph Types and Utilities * Manages table dependencies and seeding order for relationship-aware seeding */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DependencyGraphBuilder = void 0; class DependencyGraphBuilder { constructor() { this.nodes = new Map(); this.edges = []; this.cycles = []; } /** * Add a table node to the graph */ addNode(table, schema = 'public', metadata) { const node = { table, schema, dependencies: [], dependents: [], depth: 0, priority: 0, isCircular: false, metadata: { isJunctionTable: false, isTenantScoped: false, hasTimestamps: false, primaryKeyColumns: [], foreignKeyCount: 0, estimatedSize: 'medium', seedingComplexity: 'simple', ...metadata } }; this.nodes.set(table, node); } /** * Add a dependency edge between tables */ addEdge(from, to, relationship) { const edge = { from, to, type: relationship.isNullable ? 'optional' : 'required', weight: this.calculateRelationshipWeight(relationship), constraint: relationship.constraintName, canBeCircular: relationship.isNullable || relationship.isDeferrable }; this.edges.push(edge); // Update node dependencies const fromNode = this.nodes.get(from); const toNode = this.nodes.get(to); if (fromNode && !fromNode.dependencies.includes(to)) { fromNode.dependencies.push(to); } if (toNode && !toNode.dependents.includes(from)) { toNode.dependents.push(from); } } /** * Build the complete dependency graph */ build() { // Calculate node depths and priorities this.calculateDepthsAndPriorities(); // Detect circular dependencies this.detectCircularDependencies(); // Calculate seeding order const seedingOrder = this.calculateSeedingOrder(); // Generate metadata const metadata = this.generateGraphMetadata(); return { nodes: Array.from(this.nodes.values()), edges: this.edges, cycles: this.cycles, seedingOrder, creationOrder: seedingOrder, deletionOrder: [...seedingOrder].reverse(), metadata }; } /** * Calculate seeding order using topological sort */ calculateSeedingOrder(options = {}) { const opts = { respectCircularDependencies: true, prioritizeJunctionTables: false, groupByTenant: false, includeMetadata: true, optimizeForPerformance: true, handleOptionalRelationships: 'include', ...options }; const visited = new Set(); const visiting = new Set(); const order = []; // Get all nodes sorted by priority const sortedNodes = Array.from(this.nodes.values()) .sort((a, b) => { // Handle circular dependencies first if (a.isCircular !== b.isCircular) { return a.isCircular ? 1 : -1; } // Then by depth (lower depth = seed first) if (a.depth !== b.depth) { return a.depth - b.depth; } // Then by priority return b.priority - a.priority; }); const visit = (tableName) => { if (visiting.has(tableName)) { // Circular dependency detected - handle gracefully return; } if (visited.has(tableName)) { return; } visiting.add(tableName); const node = this.nodes.get(tableName); if (node) { // Visit dependencies first (tables this table depends on) for (const dependency of node.dependencies) { // Skip circular dependencies if option is set if (opts.respectCircularDependencies && this.isCircularEdge(tableName, dependency)) { continue; } visit(dependency); } } visiting.delete(tableName); visited.add(tableName); order.push(tableName); }; // Visit all nodes for (const node of sortedNodes) { if (!visited.has(node.table)) { visit(node.table); } } return order; } /** * Calculate seeding order with phases for parallel execution */ calculateSeedingOrderWithPhases(options = {}) { const seedingOrder = this.calculateSeedingOrder(options); const phases = []; // Group tables into phases based on dependencies const processedTables = new Set(); let phaseNumber = 1; while (processedTables.size < seedingOrder.length) { const phaseTabless = []; for (const table of seedingOrder) { if (processedTables.has(table)) continue; const node = this.nodes.get(table); if (!node) continue; // Check if all dependencies are satisfied const dependenciesSatisfied = node.dependencies.every(dep => processedTables.has(dep) || this.isCircularEdge(table, dep)); if (dependenciesSatisfied) { phaseTabless.push(table); } } if (phaseTabless.length === 0) { // Deadlock - add remaining tables with warnings const remainingTables = seedingOrder.filter(t => !processedTables.has(t)); phaseTabless.push(...remainingTables); } phases.push({ phase: phaseNumber, tables: phaseTabless, description: `Phase ${phaseNumber}: ${phaseTabless.length} tables`, canRunInParallel: phaseTabless.length > 1, estimatedTime: phaseTabless.length * 1000, // 1 second per table estimate dependencies: phaseNumber === 1 ? [] : [`Phase ${phaseNumber - 1}`], requirements: [] }); phaseTabless.forEach(table => processedTables.add(table)); phaseNumber++; } return { success: true, seedingOrder, phases, circularDependenciesResolved: this.cycles, warnings: [], errors: [], metadata: { totalPhases: phases.length, estimatedSeedingTime: phases.reduce((sum, phase) => sum + phase.estimatedTime, 0), complexity: this.calculateComplexity(), recommendations: this.generateRecommendations() } }; } calculateDepthsAndPriorities() { // Reset depths this.nodes.forEach(node => { node.depth = 0; node.priority = 0; }); // Calculate depths using BFS const visited = new Set(); const queue = []; // Start with nodes that have no dependencies (root nodes) this.nodes.forEach((node, table) => { if (node.dependencies.length === 0) { queue.push({ table, depth: 0 }); } }); while (queue.length > 0) { const { table, depth } = queue.shift(); if (visited.has(table)) continue; visited.add(table); const node = this.nodes.get(table); node.depth = Math.max(node.depth, depth); node.priority = this.calculateNodePriority(node); // Add dependents to queue for (const dependent of node.dependents) { if (!visited.has(dependent)) { queue.push({ table: dependent, depth: depth + 1 }); } } } } detectCircularDependencies() { const visited = new Set(); const visiting = new Set(); const findCycle = (table, path) => { if (visiting.has(table)) { // Found cycle - return the cycle path const cycleStart = path.indexOf(table); return path.slice(cycleStart); } if (visited.has(table)) { return null; } visiting.add(table); path.push(table); const node = this.nodes.get(table); if (node) { for (const dependency of node.dependencies) { const cycle = findCycle(dependency, [...path]); if (cycle) { return cycle; } } } visiting.delete(table); visited.add(table); return null; }; // Check each node for cycles for (const [table] of this.nodes) { if (!visited.has(table)) { const cycle = findCycle(table, []); if (cycle && cycle.length > 1) { this.addCircularDependency(cycle); } } } } addCircularDependency(tables) { // Mark nodes as circular tables.forEach(table => { const node = this.nodes.get(table); if (node) { node.isCircular = true; } }); // Find edges involved in the cycle const cycleEdges = this.edges.filter(edge => tables.includes(edge.from) && tables.includes(edge.to)); // Determine resolution strategy const resolutionStrategy = this.determineCircularResolutionStrategy(cycleEdges); this.cycles.push({ tables, edges: cycleEdges, resolutionStrategy, resolutionOrder: this.calculateCircularResolutionOrder(tables, cycleEdges), complexity: cycleEdges.length > 2 ? 'complex' : 'simple' }); } determineCircularResolutionStrategy(edges) { // If all edges can be made deferrable, use defer_constraints if (edges.every(edge => edge.canBeCircular)) { return 'defer_constraints'; } // If some edges are optional, use null_initially if (edges.some(edge => edge.type === 'optional')) { return 'null_initially'; } // Otherwise, use post_insert_update return 'post_insert_update'; } calculateCircularResolutionOrder(tables, edges) { // For now, return tables in dependency order return tables; } calculateRelationshipWeight(relationship) { let weight = 5; // Base weight // Adjust based on nullability if (!relationship.isNullable) weight += 3; // Adjust based on delete action switch (relationship.onDelete) { case 'CASCADE': weight += 2; break; case 'RESTRICT': weight += 3; break; case 'SET NULL': weight += 1; break; } return Math.min(weight, 10); } calculateNodePriority(node) { let priority = 100; // Base priority // Adjust based on dependencies (fewer dependencies = higher priority) priority -= node.dependencies.length * 10; // Adjust based on metadata if (node.metadata.isJunctionTable) priority -= 20; if (node.metadata.isTenantScoped) priority += 10; return Math.max(priority, 0); } isCircularEdge(from, to) { return this.cycles.some(cycle => cycle.tables.includes(from) && cycle.tables.includes(to)); } generateGraphMetadata() { const totalTables = this.nodes.size; const totalRelationships = this.edges.length; const circularDependencies = this.cycles.length; const maxDepth = Math.max(...Array.from(this.nodes.values()).map(n => n.depth)); let complexity = 'simple'; if (circularDependencies > 2 || maxDepth > 5) complexity = 'complex'; else if (circularDependencies > 0 || maxDepth > 3) complexity = 'moderate'; if (totalTables > 20 && circularDependencies > 3) complexity = 'very_complex'; return { totalTables, totalRelationships, circularDependencies, maxDepth, complexity, analysisTimestamp: new Date().toISOString(), confidence: this.calculateConfidence(), warnings: this.generateWarnings(), recommendations: this.generateRecommendations() }; } calculateConfidence() { const baseConfidence = 0.8; let confidence = baseConfidence; // Reduce confidence for complex scenarios if (this.cycles.length > 0) confidence -= 0.1; if (this.cycles.length > 2) confidence -= 0.2; return Math.max(confidence, 0.1); } generateWarnings() { const warnings = []; if (this.cycles.length > 0) { warnings.push(`${this.cycles.length} circular dependencies detected`); } if (this.cycles.length > 3) { warnings.push('High number of circular dependencies may affect seeding performance'); } const maxDepth = Math.max(...Array.from(this.nodes.values()).map(n => n.depth)); if (maxDepth > 5) { warnings.push('Deep dependency chain detected - consider optimizing schema design'); } return warnings; } generateRecommendations() { const recommendations = []; if (this.cycles.length > 0) { recommendations.push('Consider using nullable foreign keys or deferred constraints to reduce circular dependencies'); } const junctionTables = Array.from(this.nodes.values()).filter(n => n.metadata.isJunctionTable); if (junctionTables.length > 0) { recommendations.push('Junction tables detected - ensure proper many-to-many relationship handling'); } return recommendations; } calculateComplexity() { if (this.cycles.length > 3 || this.nodes.size > 20) return 'high'; if (this.cycles.length > 0 || this.nodes.size > 10) return 'medium'; return 'low'; } } exports.DependencyGraphBuilder = DependencyGraphBuilder; //# sourceMappingURL=dependency-graph.js.map