UNPKG

supa-seed

Version:

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

642 lines 26.5 kB
"use strict"; /** * Schema Introspection System v2.2.0 * Dynamically discovers database structure, constraints, and relationships * Now includes deep PostgreSQL constraint discovery and business logic parsing * Part of the constraint-aware architecture evolution */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaIntrospector = void 0; const logger_1 = require("../core/utils/logger"); const constraint_discovery_engine_1 = require("../features/analysis/constraint-discovery-engine"); class SchemaIntrospector { constructor(client) { this.cache = new Map(); this.introspectionCache = null; this.client = client; this.constraintEngine = new constraint_discovery_engine_1.ConstraintDiscoveryEngine(client); } /** * Perform comprehensive schema introspection */ async introspectSchema() { if (this.introspectionCache) { return this.introspectionCache; } logger_1.Logger.info('🔍 Starting comprehensive schema introspection...'); const result = { tables: [], relationships: [], patterns: [], constraints: { userCreationConstraints: [], dataIntegrityRules: [], businessLogicConstraints: [] }, framework: { type: 'custom', version: 'unknown', confidence: 0, evidence: [] }, recommendations: [] }; try { // Step 1: Discover all tables result.tables = await this.discoverTables(); logger_1.Logger.debug(`Discovered ${result.tables.length} tables`); // Step 2: Analyze relationships result.relationships = await this.analyzeRelationships(result.tables); logger_1.Logger.debug(`Found ${result.relationships.length} relationships`); // Step 3: Identify table patterns and roles result.patterns = await this.identifyTablePatterns(result.tables, result.relationships); logger_1.Logger.debug(`Identified ${result.patterns.length} table patterns`); // Step 4: Extract legacy constraints (maintained for compatibility) result.constraints = await this.extractConstraints(result.tables, result.relationships); logger_1.Logger.debug(`Extracted legacy constraints: ${Object.values(result.constraints).flat().length} total`); // Step 4.5: v2.2.0 Deep constraint discovery try { const tableNames = result.tables.map(t => t.name); const discoveredConstraints = await this.constraintEngine.discoverConstraints(tableNames); result.constraints.discoveredConstraints = discoveredConstraints; // Merge discovered business rules into legacy format for compatibility const discoveredRules = discoveredConstraints.businessRules.map(rule => this.convertEngineRuleToLegacy(rule)); result.constraints.businessLogicConstraints.push(...discoveredRules); logger_1.Logger.success(`✅ v2.2.0 Deep constraint discovery: ${discoveredConstraints.businessRules.length} business rules found`); } catch (error) { logger_1.Logger.warn(`v2.2.0 constraint discovery failed: ${error.message}`); // Continue without deep discovery - maintain backward compatibility } // Step 5: Detect framework result.framework = await this.detectFramework(result.tables, result.patterns); logger_1.Logger.debug(`Framework detected: ${result.framework.type} (confidence: ${result.framework.confidence})`); // Step 6: Generate recommendations result.recommendations = await this.generateRecommendations(result); logger_1.Logger.debug(`Generated ${result.recommendations.length} recommendations`); this.introspectionCache = result; logger_1.Logger.success('✅ Schema introspection completed'); return result; } catch (error) { logger_1.Logger.error('Schema introspection failed:', error); throw new Error(`Schema introspection failed: ${error.message}`); } } /** * Discover all tables with their columns, constraints, and metadata */ async discoverTables() { const tables = []; // Get basic table information const { data: tableData, error: tableError } = await this.client .from('information_schema.tables') .select('table_name, table_schema') .eq('table_schema', 'public') .eq('table_type', 'BASE TABLE'); if (tableError) { logger_1.Logger.warn('Could not query information_schema, falling back to table discovery'); return this.fallbackTableDiscovery(); } for (const table of tableData || []) { const tableName = table.table_name; try { const tableInfo = { name: tableName, schema: table.table_schema, columns: await this.getTableColumns(tableName), constraints: await this.getTableConstraints(tableName), indexes: await this.getTableIndexes(tableName), triggers: await this.getTableTriggers(tableName), rowCount: await this.getTableRowCount(tableName), hasData: false }; tableInfo.hasData = tableInfo.rowCount > 0; tables.push(tableInfo); } catch (error) { logger_1.Logger.warn(`Failed to introspect table ${tableName}: ${error.message}`); // Continue with other tables } } return tables; } /** * Fallback table discovery when information_schema is not accessible */ async fallbackTableDiscovery() { const commonTables = [ 'profiles', 'accounts', 'users', 'setups', 'posts', 'categories', 'teams', 'organizations', 'memberships', 'subscriptions', 'roles', 'invitations', 'notifications', 'media_attachments', 'gear_items', 'base_templates', 'reviews', 'trips', 'modifications' ]; const tables = []; for (const tableName of commonTables) { if (await this.tableExists(tableName)) { try { const tableInfo = { name: tableName, schema: 'public', columns: await this.getTableColumns(tableName), constraints: [], // Limited info in fallback mode indexes: [], triggers: [], rowCount: await this.getTableRowCount(tableName), hasData: false }; tableInfo.hasData = tableInfo.rowCount > 0; tables.push(tableInfo); } catch (error) { logger_1.Logger.debug(`Fallback discovery failed for ${tableName}: ${error.message}`); } } } return tables; } /** * Get detailed column information for a table */ async getTableColumns(tableName) { const { data, error } = await this.client .from('information_schema.columns') .select(` column_name, data_type, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale `) .eq('table_name', tableName) .eq('table_schema', 'public') .order('ordinal_position'); if (error) { // Fallback: try to get columns by querying the table return this.fallbackGetColumns(tableName); } const columns = []; for (const col of data || []) { const column = { name: col.column_name, type: col.data_type, isNullable: col.is_nullable === 'YES', defaultValue: col.column_default, isPrimaryKey: false, // Will be set when we analyze constraints isForeignKey: false, maxLength: col.character_maximum_length }; // Handle enum types if (col.data_type === 'USER-DEFINED') { column.enumValues = await this.getEnumValues(tableName, col.column_name); } columns.push(column); } return columns; } /** * Fallback method to get column info when information_schema is not available */ async fallbackGetColumns(tableName) { try { // Try to do a select with limit 0 to get column info from error or metadata const { error } = await this.client .from(tableName) .select('*') .limit(0); // For now, return minimal column info // In a full implementation, we could parse error messages or use other methods return [ { name: 'id', type: 'uuid', isNullable: false, defaultValue: null, isPrimaryKey: true, isForeignKey: false } ]; } catch (error) { logger_1.Logger.debug(`Fallback column discovery failed for ${tableName}: ${error.message}`); return []; } } /** * Get table constraints (primary keys, foreign keys, checks, etc.) */ async getTableConstraints(tableName) { // This would query information_schema.table_constraints and related tables // Implementation details depend on PostgreSQL system catalogs const constraints = []; try { // Get constraint information from information_schema const { data, error } = await this.client .from('information_schema.table_constraints') .select('constraint_name, constraint_type') .eq('table_name', tableName) .eq('table_schema', 'public'); if (!error && data) { for (const constraint of data) { // Get detailed constraint info (columns, references, etc.) const constraintDetails = await this.getConstraintDetails(constraint.constraint_name, tableName); if (constraintDetails) { constraints.push(constraintDetails); } } } } catch (error) { logger_1.Logger.debug(`Constraint discovery failed for ${tableName}: ${error.message}`); } return constraints; } /** * Get detailed information about a specific constraint */ async getConstraintDetails(constraintName, tableName) { // Implementation would query information_schema.key_column_usage, referential_constraints, etc. // For now, return a basic structure return { name: constraintName, type: 'PRIMARY KEY', // Would be determined from actual query columns: ['id'], // Would be determined from actual query isDeferrable: false }; } /** * Analyze relationships between tables */ async analyzeRelationships(tables) { const relationships = []; for (const table of tables) { for (const constraint of table.constraints) { if (constraint.type === 'FOREIGN KEY' && constraint.referencedTable) { const relationship = { fromTable: table.name, fromColumn: constraint.columns[0], toTable: constraint.referencedTable, toColumn: constraint.referencedColumns?.[0] || 'id', relationshipType: this.determineRelationshipType(table.name, constraint), cascadeDelete: constraint.onDelete === 'CASCADE', isRequired: !table.columns.find(col => col.name === constraint.columns[0])?.isNullable }; relationships.push(relationship); } } } return relationships; } /** * Identify table patterns and their likely roles in the application */ async identifyTablePatterns(tables, relationships) { const patterns = []; for (const table of tables) { const pattern = await this.analyzeTablePattern(table, relationships); if (pattern) { patterns.push(pattern); } } return patterns; } /** * Analyze a single table to determine its pattern and role */ async analyzeTablePattern(table, relationships) { const columnNames = table.columns.map(col => col.name.toLowerCase()); const evidence = []; let confidence = 0; let suggestedRole = 'system'; const columnMappings = {}; // Analyze column patterns to determine table role // User table patterns if (this.hasUserTableColumns(columnNames)) { suggestedRole = 'user'; confidence += 30; evidence.push('Has user-like columns (email, name, etc.)'); columnMappings.email = this.findColumnVariants(columnNames, ['email', 'email_address', 'user_email']); columnMappings.name = this.findColumnVariants(columnNames, ['name', 'display_name', 'full_name', 'username']); columnMappings.avatar = this.findColumnVariants(columnNames, ['avatar_url', 'picture_url', 'profile_image', 'image_url']); columnMappings.bio = this.findColumnVariants(columnNames, ['bio', 'about', 'description']); } // Content table patterns if (this.hasContentTableColumns(columnNames)) { suggestedRole = 'content'; confidence += 25; evidence.push('Has content-like columns (title, description, etc.)'); columnMappings.title = this.findColumnVariants(columnNames, ['title', 'name', 'subject']); columnMappings.content = this.findColumnVariants(columnNames, ['content', 'body', 'description', 'text']); columnMappings.author = this.findColumnVariants(columnNames, ['user_id', 'author_id', 'creator_id', 'account_id']); } // Association table patterns if (this.hasAssociationTableColumns(columnNames, relationships)) { suggestedRole = 'association'; confidence += 20; evidence.push('Has association-like structure (multiple foreign keys)'); } // MakerKit-specific patterns if (this.hasMakerKitColumns(columnNames)) { confidence += 15; evidence.push('Has MakerKit-specific columns'); } // Auth table patterns if (table.name.includes('auth') || table.schema === 'auth') { suggestedRole = 'auth'; confidence += 40; evidence.push('Located in auth schema or has auth-related name'); } if (confidence < 10) { return null; // Not confident enough in the pattern } return { name: table.name, confidence: Math.min(confidence / 100, 1), evidence, suggestedRole, columnMappings: this.cleanColumnMappings(columnMappings) }; } /** * Helper methods for pattern recognition */ hasUserTableColumns(columns) { const userColumns = ['email', 'name', 'username', 'display_name', 'full_name']; return userColumns.some(col => columns.includes(col)); } hasContentTableColumns(columns) { const contentColumns = ['title', 'content', 'body', 'description']; return contentColumns.some(col => columns.includes(col)); } hasAssociationTableColumns(columns, relationships) { const tableRelationships = relationships.filter(rel => rel.fromTable === columns[0]); return tableRelationships.length >= 2; // Has multiple foreign keys } hasMakerKitColumns(columns) { const makerkitColumns = ['primary_owner_user_id', 'is_personal_account', 'slug']; return makerkitColumns.some(col => columns.includes(col)); } findColumnVariants(columns, variants) { return variants.filter(variant => columns.includes(variant)); } cleanColumnMappings(mappings) { const cleaned = {}; for (const [key, values] of Object.entries(mappings)) { if (values.length > 0) { cleaned[key] = values; } } return cleaned; } /** * Extract business constraints and rules from the schema */ async extractConstraints(tables, relationships) { const userCreationConstraints = []; const dataIntegrityRules = []; const businessLogicConstraints = []; // Analyze each table for constraint patterns for (const table of tables) { // Look for user creation constraints if (table.name === 'profiles' || table.name === 'accounts') { const constraints = await this.analyzeUserCreationConstraints(table, relationships); userCreationConstraints.push(...constraints); } // Look for data integrity rules const integrityRules = await this.analyzeDataIntegrityRules(table); dataIntegrityRules.push(...integrityRules); // Look for business logic constraints const businessRules = await this.analyzeBusinessLogicConstraints(table); businessLogicConstraints.push(...businessRules); } return { userCreationConstraints, dataIntegrityRules, businessLogicConstraints }; } /** * Detect framework type and version based on schema patterns */ async detectFramework(tables, patterns) { const evidence = []; let type = 'custom'; let version = 'unknown'; let confidence = 0; const tableNames = tables.map(t => t.name); // MakerKit detection if (tableNames.includes('accounts') && tableNames.includes('memberships')) { type = 'makerkit'; confidence += 40; evidence.push('Has accounts and memberships tables'); // Version detection based on table patterns if (tableNames.includes('role_permissions') && tableNames.includes('billing_customers')) { version = 'v3'; confidence += 20; evidence.push('Has v3-specific tables (role_permissions, billing_customers)'); } else if (tableNames.includes('subscriptions') && tableNames.includes('roles')) { version = 'v2'; confidence += 15; evidence.push('Has v2-specific tables (subscriptions, roles)'); } else { version = 'v1'; confidence += 10; evidence.push('Basic MakerKit pattern detected'); } } // Check for MakerKit-specific column patterns const accountsTable = tables.find(t => t.name === 'accounts'); if (accountsTable) { const accountsColumns = accountsTable.columns.map(c => c.name); if (accountsColumns.includes('primary_owner_user_id')) { confidence += 15; evidence.push('Has MakerKit-specific account structure'); } } return { type, version, confidence: Math.min(confidence / 100, 1), evidence }; } /** * Generate recommendations based on introspection results */ async generateRecommendations(result) { const recommendations = []; // Analyze patterns for recommendations const userTables = result.patterns.filter(p => p.suggestedRole === 'user'); if (userTables.length === 0) { recommendations.push({ type: 'warning', message: 'No user tables detected. User creation may fail.', priority: 'high', suggestedAction: 'Ensure you have a profiles, accounts, or users table' }); } else if (userTables.length > 1) { recommendations.push({ type: 'configuration', message: 'Multiple user tables detected. Specify primary user table in config.', priority: 'medium', suggestedAction: 'Set schema.primaryUserTable in your configuration' }); } // Check for missing relationships const orphanTables = result.tables.filter(table => !result.relationships.some(rel => rel.fromTable === table.name || rel.toTable === table.name)); if (orphanTables.length > 0) { recommendations.push({ type: 'optimization', message: `${orphanTables.length} tables have no relationships. This may indicate isolated data.`, priority: 'low', suggestedAction: 'Review table relationships for proper data seeding' }); } return recommendations; } /** * Helper methods for constraint analysis */ async analyzeUserCreationConstraints(table, relationships) { const constraints = []; // Look for CHECK constraints that might affect user creation for (const constraint of table.constraints) { if (constraint.type === 'CHECK' && constraint.checkDefinition) { constraints.push({ table: table.name, rule: constraint.name, type: 'conditional_insert', description: `Check constraint: ${constraint.checkDefinition}`, sqlCondition: constraint.checkDefinition, requiresValidation: true }); } } return constraints; } async analyzeDataIntegrityRules(table) { const rules = []; // Analyze NOT NULL constraints const requiredColumns = table.columns.filter(col => !col.isNullable && !col.isPrimaryKey); for (const col of requiredColumns) { rules.push({ table: table.name, rule: `${col.name}_required`, type: 'required_relationship', description: `Column ${col.name} is required`, sqlCondition: `${col.name} IS NOT NULL`, requiresValidation: true }); } return rules; } async analyzeBusinessLogicConstraints(table) { const constraints = []; // Look for business logic patterns in triggers for (const trigger of table.triggers) { constraints.push({ table: table.name, rule: trigger.name, type: 'business_rule', description: `Trigger ${trigger.name} enforces business logic`, sqlCondition: `-- Trigger: ${trigger.functionName}`, requiresValidation: false }); } return constraints; } /** * Utility methods */ async tableExists(tableName) { try { const { error } = await this.client.from(tableName).select('*').limit(1); return !error; } catch { return false; } } async getTableRowCount(tableName) { try { const { count, error } = await this.client .from(tableName) .select('*', { count: 'exact', head: true }); return error ? 0 : (count || 0); } catch { return 0; } } async getTableIndexes(tableName) { // Would query pg_indexes or information_schema.statistics return []; } async getTableTriggers(tableName) { // Would query information_schema.triggers return []; } async getEnumValues(tableName, columnName) { // Would query pg_enum for enum values return []; } determineRelationshipType(tableName, constraint) { // Simple heuristic - could be more sophisticated if (constraint.columns.length === 1 && constraint.referencedColumns?.length === 1) { return 'one_to_many'; } return 'many_to_many'; } /** * v2.2.0: Convert engine business rule to legacy format for compatibility */ convertEngineRuleToLegacy(rule) { return { table: rule.table, rule: rule.name, type: this.mapEngineRuleType(rule.type), description: rule.errorMessage || rule.condition, sqlCondition: rule.sqlPattern, requiresValidation: rule.action !== 'allow' }; } /** * v2.2.0: Map engine rule types to legacy types */ mapEngineRuleType(type) { switch (type) { case 'validation': return 'conditional_insert'; case 'dependency': return 'required_relationship'; case 'business_logic': return 'business_rule'; case 'transformation': return 'value_constraint'; default: return 'business_rule'; } } /** * v2.2.0: Access discovered constraints metadata */ getDiscoveredConstraints() { return this.introspectionCache?.constraints?.discoveredConstraints || null; } /** * Clear the introspection cache */ clearCache() { this.introspectionCache = null; this.cache.clear(); this.constraintEngine.clearCache(); // v2.2.0: Clear constraint engine cache } /** * Get cached introspection result */ getCachedResult() { return this.introspectionCache; } } exports.SchemaIntrospector = SchemaIntrospector; //# sourceMappingURL=schema-introspector.js.map