UNPKG

supa-seed

Version:

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

560 lines 23.3 kB
"use strict"; /** * Junction Table Handler * Handles many-to-many relationships and junction table seeding */ Object.defineProperty(exports, "__esModule", { value: true }); exports.JunctionTableHandler = exports.COMMON_JUNCTION_PATTERNS = void 0; const logger_1 = require("../core/utils/logger"); // Common junction table patterns exports.COMMON_JUNCTION_PATTERNS = [ { name: 'user_roles', pattern: /^(user|account)_roles?$/i, leftTable: 'users', rightTable: 'roles', confidence: 0.9, description: 'User-Role many-to-many relationship' }, { name: 'user_teams', pattern: /^(user|member)_teams?$/i, leftTable: 'users', rightTable: 'teams', confidence: 0.85, description: 'User-Team membership relationship' }, { name: 'product_categories', pattern: /^products?_categories$/i, leftTable: 'products', rightTable: 'categories', confidence: 0.8, description: 'Product-Category classification relationship' }, { name: 'post_tags', pattern: /^posts?_tags?$/i, leftTable: 'posts', rightTable: 'tags', confidence: 0.85, description: 'Post-Tag labeling relationship' }, { name: 'organization_members', pattern: /^(organization|org)_members?$/i, leftTable: 'organizations', rightTable: 'users', confidence: 0.9, description: 'Organization membership relationship' } ]; class JunctionTableHandler { constructor(client) { this.detectedJunctionTables = new Map(); this.relationshipPatterns = [...exports.COMMON_JUNCTION_PATTERNS]; this.client = client; } /** * Detect junction tables from dependency graph */ async detectJunctionTables(dependencyGraph) { logger_1.Logger.info('🔗 Detecting junction tables...'); try { const junctionTables = []; const warnings = []; const errors = []; // Analyze each node in the dependency graph for (const node of dependencyGraph.nodes) { if (node.metadata.isJunctionTable) { logger_1.Logger.debug(`Analyzing potential junction table: ${node.table}`); const junctionInfo = await this.analyzeJunctionTable(node.table, node.schema, dependencyGraph); if (junctionInfo) { junctionTables.push(junctionInfo); this.detectedJunctionTables.set(node.table, junctionInfo); logger_1.Logger.debug(`✅ Confirmed junction table: ${node.table} (${junctionInfo.leftTable}${junctionInfo.rightTable})`); } } } // Detect relationship patterns const detectedPatterns = this.detectRelationshipPatterns(junctionTables); const confidence = this.calculateDetectionConfidence(junctionTables); const recommendations = this.generateJunctionRecommendations(junctionTables); logger_1.Logger.success(`🔗 Junction table detection completed: ${junctionTables.length} tables found`); return { success: true, junctionTables, relationshipPatterns: detectedPatterns, totalRelationships: junctionTables.length, confidence, warnings, errors, recommendations }; } catch (error) { logger_1.Logger.error('Junction table detection failed:', error); return { success: false, junctionTables: [], relationshipPatterns: [], totalRelationships: 0, confidence: 0, warnings: [], errors: [error.message], recommendations: [] }; } } /** * Analyze a specific table to determine if it's a junction table */ async analyzeJunctionTable(tableName, schema, dependencyGraph) { try { // Get foreign keys for this table const node = dependencyGraph.nodes.find(n => n.table === tableName); if (!node) return null; const foreignKeys = dependencyGraph.edges .filter(edge => edge.from === tableName) .map(edge => this.findForeignKeyRelationship(edge, dependencyGraph)); if (foreignKeys.length < 2) return null; // For junction tables, we expect exactly 2 foreign keys (or 2 primary foreign keys) const primaryForeignKeys = foreignKeys.filter(fk => fk !== null).slice(0, 2); if (primaryForeignKeys.length < 2) return null; const [leftFk, rightFk] = primaryForeignKeys; // Get additional columns const additionalColumns = await this.getAdditionalColumns(tableName, schema, [leftFk.fromColumn, rightFk.fromColumn]); // Analyze cardinality const cardinality = await this.analyzeCardinality(tableName, leftFk, rightFk); // Calculate confidence const confidence = this.calculateJunctionConfidence(tableName, foreignKeys, additionalColumns); return { tableName, schema, leftTable: leftFk.toTable, leftColumn: leftFk.fromColumn, rightTable: rightFk.toTable, rightColumn: rightFk.fromColumn, leftForeignKey: leftFk, rightForeignKey: rightFk, additionalColumns, cardinality, isDetected: true, confidence }; } catch (error) { logger_1.Logger.warn(`Failed to analyze junction table ${tableName}:`, error); return null; } } /** * Find foreign key relationship from dependency edge */ findForeignKeyRelationship(edge, dependencyGraph) { // This is a simplified implementation - in a real scenario, we'd have access to the full FK info return { constraintName: edge.constraint || `fk_${edge.from}_${edge.to}`, fromTable: edge.from, fromColumn: `${edge.to}_id`, // Assumed convention toTable: edge.to, toColumn: 'id', onDelete: 'CASCADE', onUpdate: 'CASCADE', isNullable: edge.type === 'optional', isDeferrable: false, schema: 'public' }; } /** * Get additional columns in junction table (beyond foreign keys) */ async getAdditionalColumns(tableName, schema, foreignKeyColumns) { try { const { data: columns, error } = await this.client .from('information_schema.columns') .select('column_name, data_type, is_nullable, column_default') .eq('table_name', tableName) .eq('table_schema', schema); if (error || !columns) return []; return (columns || []) .filter(col => !foreignKeyColumns.includes(col.column_name)) .map(col => ({ name: col.column_name, type: col.data_type, isNullable: col.is_nullable === 'YES', hasDefault: col.column_default !== null, defaultValue: col.column_default, isTimestamp: ['created_at', 'updated_at', 'timestamp'].includes(col.column_name), isMetadata: ['created_by', 'updated_by', 'status', 'priority'].includes(col.column_name) })); } catch (error) { logger_1.Logger.warn(`Failed to get additional columns for ${tableName}:`, error); return []; } } /** * Analyze cardinality of the relationship */ async analyzeCardinality(junctionTable, leftFk, rightFk) { // For now, assume many-to-many since it's a junction table // In a full implementation, we'd analyze the actual data return { leftCardinality: 'many', rightCardinality: 'many', relationshipType: 'many_to_many', isOptional: leftFk.isNullable || rightFk.isNullable, estimatedDensity: 0.3 // Default estimate }; } /** * Calculate confidence score for junction table detection */ calculateJunctionConfidence(tableName, foreignKeys, additionalColumns) { let confidence = 0.5; // Base confidence // Boost confidence for exactly 2 foreign keys if (foreignKeys.filter(fk => fk !== null).length === 2) { confidence += 0.3; } // Boost confidence for minimal additional columns if (additionalColumns.length <= 2) { confidence += 0.2; } // Boost confidence for timestamp columns if (additionalColumns.some(col => col.isTimestamp)) { confidence += 0.1; } // Boost confidence for recognized naming patterns for (const pattern of this.relationshipPatterns) { if (pattern.pattern.test(tableName)) { confidence += 0.2; break; } } return Math.min(confidence, 1.0); } /** * Detect relationship patterns in junction tables */ detectRelationshipPatterns(junctionTables) { const detectedPatterns = []; for (const junction of junctionTables) { for (const pattern of this.relationshipPatterns) { if (pattern.pattern.test(junction.tableName)) { detectedPatterns.push({ ...pattern, leftTable: junction.leftTable, rightTable: junction.rightTable }); } } } return detectedPatterns; } /** * Seed junction table with relationships */ async seedJunctionTable(junctionTableName, options = {}) { const startTime = Date.now(); logger_1.Logger.info(`🌱 Seeding junction table: ${junctionTableName}`); const opts = { generateRelationships: true, relationshipDensity: 0.3, respectCardinality: true, avoidOrphans: true, distributionStrategy: 'random', clusterFactor: 0.7, generateMetadata: true, includeTimestamps: true, customDataGenerators: new Map(), batchSize: 100, maxRelationshipsPerRecord: 10, validateForeignKeys: true, ...options }; try { const junctionInfo = this.detectedJunctionTables.get(junctionTableName); if (!junctionInfo) { throw new Error(`Junction table ${junctionTableName} not detected or analyzed`); } // Get records from both related tables const [leftRecords, rightRecords] = await Promise.all([ this.getTableRecords(junctionInfo.leftTable), this.getTableRecords(junctionInfo.rightTable) ]); if (leftRecords.length === 0 || rightRecords.length === 0) { throw new Error(`Cannot seed junction table: one or both related tables are empty`); } // Generate relationships const relationships = this.generateRelationships(leftRecords, rightRecords, junctionInfo, opts); // Prepare junction records const junctionRecords = relationships.map(rel => this.createJunctionRecord(rel.left, rel.right, junctionInfo, opts)); // Insert in batches let insertedCount = 0; let skippedCount = 0; const batchCount = Math.ceil(junctionRecords.length / opts.batchSize); for (let i = 0; i < batchCount; i++) { const batch = junctionRecords.slice(i * opts.batchSize, (i + 1) * opts.batchSize); try { const { data, error } = await this.client .from(junctionTableName) .insert(batch) .select(); if (error) { logger_1.Logger.warn(`Batch ${i + 1} failed:`, error); skippedCount += batch.length; } else { insertedCount += data?.length || 0; } } catch (batchError) { logger_1.Logger.warn(`Batch ${i + 1} error:`, batchError); skippedCount += batch.length; } } const metadata = { leftTableRecords: leftRecords.length, rightTableRecords: rightRecords.length, possibleRelationships: leftRecords.length * rightRecords.length, actualDensity: insertedCount / (leftRecords.length * rightRecords.length), averageRelationshipsPerLeftRecord: insertedCount / leftRecords.length, averageRelationshipsPerRightRecord: insertedCount / rightRecords.length }; logger_1.Logger.success(`✅ Junction table seeding completed: ${insertedCount} relationships created`); return { success: true, junctionTable: junctionTableName, relationshipsCreated: insertedCount, relationshipsSkipped: skippedCount, batchesProcessed: batchCount, orphansAvoided: 0, // Would calculate if implementing orphan avoidance validationErrors: [], warnings: [], errors: [], executionTime: Date.now() - startTime, metadata }; } catch (error) { logger_1.Logger.error(`Junction table seeding failed for ${junctionTableName}:`, error); return { success: false, junctionTable: junctionTableName, relationshipsCreated: 0, relationshipsSkipped: 0, batchesProcessed: 0, orphansAvoided: 0, validationErrors: [], warnings: [], errors: [error.message], executionTime: Date.now() - startTime, metadata: { leftTableRecords: 0, rightTableRecords: 0, possibleRelationships: 0, actualDensity: 0, averageRelationshipsPerLeftRecord: 0, averageRelationshipsPerRightRecord: 0 } }; } } /** * Get records from a table for relationship generation */ async getTableRecords(tableName) { const { data, error } = await this.client .from(tableName) .select('*') .limit(1000); // Reasonable limit for seeding if (error) { throw new Error(`Failed to get records from ${tableName}: ${error.message}`); } return data || []; } /** * Generate relationships between two sets of records */ generateRelationships(leftRecords, rightRecords, junctionInfo, options) { const relationships = []; const targetCount = Math.floor(leftRecords.length * rightRecords.length * options.relationshipDensity); switch (options.distributionStrategy) { case 'random': return this.generateRandomRelationships(leftRecords, rightRecords, targetCount); case 'even': return this.generateEvenRelationships(leftRecords, rightRecords, targetCount); case 'clustered': return this.generateClusteredRelationships(leftRecords, rightRecords, targetCount, options.clusterFactor); default: return this.generateRandomRelationships(leftRecords, rightRecords, targetCount); } } /** * Generate random relationships */ generateRandomRelationships(leftRecords, rightRecords, targetCount) { const relationships = []; const usedPairs = new Set(); while (relationships.length < targetCount && relationships.length < leftRecords.length * rightRecords.length) { const leftRecord = leftRecords[Math.floor(Math.random() * leftRecords.length)]; const rightRecord = rightRecords[Math.floor(Math.random() * rightRecords.length)]; const pairKey = `${leftRecord.id}-${rightRecord.id}`; if (!usedPairs.has(pairKey)) { relationships.push({ left: leftRecord, right: rightRecord }); usedPairs.add(pairKey); } } return relationships; } /** * Generate even distribution of relationships */ generateEvenRelationships(leftRecords, rightRecords, targetCount) { const relationships = []; const relationshipsPerLeft = Math.ceil(targetCount / leftRecords.length); for (const leftRecord of leftRecords) { const shuffledRight = [...rightRecords].sort(() => Math.random() - 0.5); for (let i = 0; i < relationshipsPerLeft && i < shuffledRight.length && relationships.length < targetCount; i++) { relationships.push({ left: leftRecord, right: shuffledRight[i] }); } } return relationships; } /** * Generate clustered relationships (some records have many relationships, others few) */ generateClusteredRelationships(leftRecords, rightRecords, targetCount, clusterFactor) { const relationships = []; // Select a portion of left records to be "popular" (have many relationships) const popularCount = Math.floor(leftRecords.length * (1 - clusterFactor)); const popularRecords = leftRecords.slice(0, popularCount); const regularRecords = leftRecords.slice(popularCount); // Distribute relationships: popular records get more const popularRelationships = Math.floor(targetCount * 0.7); const regularRelationships = targetCount - popularRelationships; // Generate popular relationships for (let i = 0; i < popularRelationships; i++) { const leftRecord = popularRecords[Math.floor(Math.random() * popularRecords.length)]; const rightRecord = rightRecords[Math.floor(Math.random() * rightRecords.length)]; relationships.push({ left: leftRecord, right: rightRecord }); } // Generate regular relationships for (let i = 0; i < regularRelationships; i++) { const leftRecord = regularRecords[Math.floor(Math.random() * regularRecords.length)]; const rightRecord = rightRecords[Math.floor(Math.random() * rightRecords.length)]; relationships.push({ left: leftRecord, right: rightRecord }); } return relationships; } /** * Create a junction record from two related records */ createJunctionRecord(leftRecord, rightRecord, junctionInfo, options) { const junctionRecord = { [junctionInfo.leftColumn]: leftRecord.id, [junctionInfo.rightColumn]: rightRecord.id }; // Add timestamps if requested if (options.includeTimestamps) { const now = new Date().toISOString(); junctionRecord.created_at = now; junctionRecord.updated_at = now; } // Add metadata if requested and columns exist if (options.generateMetadata) { for (const column of junctionInfo.additionalColumns) { if (column.isMetadata && !junctionRecord[column.name]) { junctionRecord[column.name] = this.generateColumnValue(column, leftRecord, rightRecord); } } } // Apply custom data generators for (const [columnName, generator] of options.customDataGenerators) { if (junctionInfo.additionalColumns.some(col => col.name === columnName)) { junctionRecord[columnName] = generator(leftRecord, rightRecord); } } return junctionRecord; } /** * Generate value for a junction table column */ generateColumnValue(column, leftRecord, rightRecord) { switch (column.name) { case 'status': return 'active'; case 'priority': return Math.floor(Math.random() * 10) + 1; case 'weight': return Math.random(); case 'order': return Math.floor(Math.random() * 100); default: if (column.hasDefault) return column.defaultValue; if (column.isNullable) return null; return `value_${Date.now()}`; } } /** * Calculate overall detection confidence */ calculateDetectionConfidence(junctionTables) { if (junctionTables.length === 0) return 0; const totalConfidence = junctionTables.reduce((sum, table) => sum + table.confidence, 0); return totalConfidence / junctionTables.length; } /** * Generate recommendations for junction table handling */ generateJunctionRecommendations(junctionTables) { const recommendations = []; if (junctionTables.length > 0) { recommendations.push(`Detected ${junctionTables.length} junction tables for many-to-many relationships`); recommendations.push('Seed parent tables before junction tables to maintain referential integrity'); } const lowConfidenceTables = junctionTables.filter(t => t.confidence < 0.7); if (lowConfidenceTables.length > 0) { recommendations.push(`${lowConfidenceTables.length} junction tables have low confidence - verify manually`); } const complexJunctions = junctionTables.filter(t => t.additionalColumns.length > 3); if (complexJunctions.length > 0) { recommendations.push(`${complexJunctions.length} junction tables have additional columns - consider custom seeding logic`); } return recommendations; } /** * Get detected junction table information */ getJunctionTableInfo(tableName) { return this.detectedJunctionTables.get(tableName); } /** * Get all detected junction tables */ getAllJunctionTables() { return Array.from(this.detectedJunctionTables.values()); } /** * Add custom relationship pattern */ addRelationshipPattern(pattern) { this.relationshipPatterns.push(pattern); } /** * Clear detected junction tables */ clearDetectedTables() { this.detectedJunctionTables.clear(); } } exports.JunctionTableHandler = JunctionTableHandler; //# sourceMappingURL=junction-table-handler.js.map