UNPKG

supa-seed

Version:

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

461 lines 18.6 kB
"use strict"; /** * RLS-Compliant Seeder * Implements user context-aware seeding that respects Row Level Security policies */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RLSCompliantSeeder = void 0; const logger_1 = require("../core/utils/logger"); const business_logic_analyzer_1 = require("../features/analysis/business-logic-analyzer"); class RLSCompliantSeeder { constructor(client, serviceRoleClient, options = {}) { this.cachedPolicies = []; this.client = client; this.serviceRoleClient = serviceRoleClient; this.businessLogicAnalyzer = new business_logic_analyzer_1.BusinessLogicAnalyzer(client); this.options = { enableRLSCompliance: true, useServiceRole: false, createUserContext: true, bypassOnFailure: false, validateAfterInsert: true, logPolicyViolations: true, maxRetries: 3, ...options }; } /** * Seed data with RLS compliance */ async seedWithRLSCompliance(table, data, userContext) { logger_1.Logger.info(`🔒 Starting RLS-compliant seeding for table: ${table}`); const result = { success: false, violatedPolicies: [], bypassesUsed: [], warnings: [], errors: [], insertedRecords: 0, validatedRecords: 0 }; try { // Check if RLS compliance is enabled if (!this.options.enableRLSCompliance) { logger_1.Logger.debug('RLS compliance disabled, using direct seeding'); return await this.seedDirectly(table, data, result); } // Get or create user context if (this.options.createUserContext && !userContext) { userContext = await this.createUserContext(); result.userContext = userContext; } // Analyze RLS policies for the table const policies = await this.getRLSPoliciesForTable(table); if (policies.length === 0) { logger_1.Logger.debug(`No RLS policies found for table ${table}, proceeding with normal seeding`); return await this.seedDirectly(table, data, result); } // Validate data against RLS policies const validation = await this.validateDataAgainstRLS(table, data, policies, userContext); if (!validation.isCompliant) { result.violatedPolicies = validation.violatedPolicies.map(v => v.policyName); if (this.options.logPolicyViolations) { logger_1.Logger.warn(`RLS policy violations detected for table ${table}:`, validation.violatedPolicies); } // Try to fix violations or use bypass return await this.handleRLSViolations(table, data, validation, result, userContext); } // Seed with user context const insertResult = await this.seedWithUserContext(table, data, userContext); result.success = insertResult.success; result.insertedRecords = insertResult.insertedRecords; // Validate after insert if enabled if (this.options.validateAfterInsert) { const postValidation = await this.validateInsertedData(table, data, userContext); result.validatedRecords = postValidation.validatedRecords; if (!postValidation.success) { result.warnings.push('Post-insert validation failed for some records'); } } logger_1.Logger.success(`✅ RLS-compliant seeding completed for ${table}: ${result.insertedRecords} records`); return result; } catch (error) { logger_1.Logger.error(`RLS-compliant seeding failed for ${table}:`, error); result.errors.push(error.message); // Try bypass if failure handling is enabled if (this.options.bypassOnFailure) { logger_1.Logger.warn('Attempting RLS bypass due to failure'); return await this.seedWithBypass(table, data, result); } return result; } } /** * Create user context for seeding */ async createUserContext() { logger_1.Logger.debug('Creating user context for RLS-compliant seeding'); try { // First, try to get current auth user const { data: { user }, error: authError } = await this.client.auth.getUser(); if (user && !authError) { logger_1.Logger.debug('Using existing authenticated user context'); return { userId: user.id, email: user.email, role: 'authenticated', metadata: user.user_metadata }; } // Create a temporary user for seeding context const tempUserData = { email: `seed-user-${Date.now()}@example.com`, password: 'TempPassword123!', email_confirm: true, user_metadata: { name: 'Seed User', created_for: 'rls_compliant_seeding' } }; const { data: newUser, error: createError } = await this.client.auth.admin.createUser(tempUserData); if (createError || !newUser.user) { throw new Error(`Failed to create user context: ${createError?.message}`); } logger_1.Logger.debug(`Created temporary user context: ${newUser.user.email}`); return { userId: newUser.user.id, email: newUser.user.email, role: 'authenticated', metadata: newUser.user.user_metadata }; } catch (error) { logger_1.Logger.error('Failed to create user context:', error); throw new Error(`User context creation failed: ${error.message}`); } } /** * Get RLS policies for a specific table */ async getRLSPoliciesForTable(table) { // Use cached policies if available if (this.cachedPolicies.length > 0) { return this.cachedPolicies.filter(p => p.tableName === table); } try { // Get policies from business logic analyzer const analysis = await this.businessLogicAnalyzer.analyzeBusinessLogic(); this.cachedPolicies = analysis.rlsPolicies; return this.cachedPolicies.filter(p => p.tableName === table); } catch (error) { logger_1.Logger.warn(`Failed to get RLS policies for ${table}:`, error); return []; } } /** * Validate data against RLS policies */ async validateDataAgainstRLS(table, data, policies, userContext) { const violations = []; const suggestedFixes = []; let requiresUserContext = false; for (const policy of policies) { if (policy.requiresUserContext && !userContext) { requiresUserContext = true; violations.push({ policyName: policy.policyName, tableName: table, reason: 'Policy requires user context but none provided', severity: 'high', suggestedFix: 'Create user context before seeding' }); } // Check policy conditions against data if (policy.using || policy.withCheck) { const violation = this.checkPolicyConditions(policy, data, userContext); if (violation) { violations.push(violation); } } } // Generate fix suggestions if (violations.length > 0) { suggestedFixes.push(...this.generateFixSuggestions(violations, policies)); } return { isCompliant: violations.length === 0, violatedPolicies: violations, suggestedFixes, requiresUserContext, bypassRecommended: violations.some(v => v.severity === 'critical') }; } /** * Check policy conditions against data */ checkPolicyConditions(policy, data, userContext) { // Simplified policy checking - in real implementation would parse SQL conditions const using = policy.using || ''; const withCheck = policy.withCheck || ''; // Check for auth.uid() requirements if ((using.includes('auth.uid()') || withCheck.includes('auth.uid()')) && !userContext) { return { policyName: policy.policyName, tableName: policy.tableName, reason: 'Policy requires auth.uid() but no user context provided', severity: 'high', suggestedFix: 'Provide user context or use service role' }; } // Check for account_id/user_id requirements if (using.includes('account_id') || withCheck.includes('account_id')) { const hasAccountId = data.some(record => record.account_id); if (!hasAccountId) { return { policyName: policy.policyName, tableName: policy.tableName, reason: 'Policy requires account_id but data does not include it', severity: 'medium', suggestedFix: 'Add account_id to data records' }; } } return null; } /** * Generate fix suggestions for RLS violations */ generateFixSuggestions(violations, policies) { const suggestions = []; if (violations.some(v => v.reason.includes('user context'))) { suggestions.push({ type: 'add_user_context', description: 'Create user context for RLS compliance', implementation: 'Enable createUserContext option or provide userContext parameter', riskLevel: 'low' }); } if (violations.some(v => v.reason.includes('account_id'))) { suggestions.push({ type: 'modify_data', description: 'Add required account_id to data records', implementation: 'Include account_id field in seeding data', riskLevel: 'low' }); } suggestions.push({ type: 'use_service_role', description: 'Use service role to bypass RLS policies', implementation: 'Configure SUPABASE_SERVICE_ROLE_KEY and enable useServiceRole', riskLevel: 'medium' }); return suggestions; } /** * Handle RLS violations with fixes or bypass */ async handleRLSViolations(table, data, validation, result, userContext) { logger_1.Logger.debug(`Handling RLS violations for table ${table}`); // Try to apply suggested fixes for (const fix of validation.suggestedFixes) { if (fix.type === 'modify_data' && fix.riskLevel === 'low') { const fixedData = this.applyDataFixes(data, fix); if (fixedData.length > 0) { logger_1.Logger.debug(`Applied data fix: ${fix.description}`); const retryResult = await this.seedWithUserContext(table, fixedData, userContext); if (retryResult.success) { result.success = true; result.insertedRecords = retryResult.insertedRecords; result.warnings.push(`Applied fix: ${fix.description}`); return result; } } } } // If fixes don't work and bypass is recommended, try service role if (validation.bypassRecommended || this.options.useServiceRole) { logger_1.Logger.warn('Using service role bypass for RLS compliance'); return await this.seedWithBypass(table, data, result); } // Log the violations and return failure result.errors.push('RLS policy violations could not be resolved'); for (const violation of validation.violatedPolicies) { result.errors.push(`${violation.policyName}: ${violation.reason}`); } return result; } /** * Apply data fixes based on suggestions */ applyDataFixes(data, fix) { // Simplified fix application - in real implementation would be more sophisticated if (fix.description.includes('account_id')) { return data.map(record => ({ ...record, account_id: record.account_id || this.currentUserContext?.accountId || 'default-account-id' })); } return data; } /** * Seed with user context */ async seedWithUserContext(table, data, userContext) { try { // Set user context if available if (userContext) { logger_1.Logger.debug(`Setting user context: ${userContext.email}`); this.currentUserContext = userContext; } // Insert data const { data: insertedData, error } = await this.client .from(table) .insert(data) .select(); if (error) { throw error; } return { success: true, insertedRecords: insertedData?.length || 0 }; } catch (error) { logger_1.Logger.error(`User context seeding failed for ${table}:`, error); return { success: false, insertedRecords: 0 }; } } /** * Seed directly without RLS compliance */ async seedDirectly(table, data, result) { try { const { data: insertedData, error } = await this.client .from(table) .insert(data) .select(); if (error) { throw error; } result.success = true; result.insertedRecords = insertedData?.length || 0; result.warnings.push('RLS compliance was disabled'); return result; } catch (error) { result.errors.push(error.message); return result; } } /** * Seed with RLS bypass using service role */ async seedWithBypass(table, data, result) { if (!this.serviceRoleClient) { result.errors.push('Service role client not available for RLS bypass'); return result; } try { logger_1.Logger.debug(`Using service role bypass for table ${table}`); const { data: insertedData, error } = await this.serviceRoleClient .from(table) .insert(data) .select(); if (error) { throw error; } result.success = true; result.insertedRecords = insertedData?.length || 0; result.bypassesUsed.push('service_role'); result.warnings.push('Used service role to bypass RLS policies'); return result; } catch (error) { result.errors.push(`Service role bypass failed: ${error.message}`); return result; } } /** * Validate inserted data */ async validateInsertedData(table, originalData, userContext) { try { // Query inserted data to verify it's accessible with current context const { data: queriedData, error } = await this.client .from(table) .select('*') .limit(originalData.length); if (error) { logger_1.Logger.warn(`Post-insert validation failed for ${table}:`, error); return { success: false, validatedRecords: 0 }; } return { success: true, validatedRecords: queriedData?.length || 0 }; } catch (error) { logger_1.Logger.warn(`Post-insert validation error for ${table}:`, error); return { success: false, validatedRecords: 0 }; } } /** * Check if table has RLS enabled */ async isRLSEnabled(table) { try { const { data, error } = await this.client .from('pg_class') .select('relrowsecurity') .eq('relname', table) .single(); if (error) { logger_1.Logger.debug(`Could not check RLS status for ${table}:`, error); return false; } return Boolean(data?.relrowsecurity) || false; } catch (error) { logger_1.Logger.debug(`RLS check failed for ${table}:`, error); return false; } } /** * Get RLS compliance recommendations */ getComplianceRecommendations(analysisResult) { const recommendations = []; if (analysisResult.rlsPolicies.length > 0) { recommendations.push('Enable RLS compliance mode for safer seeding'); if (analysisResult.rlsPolicies.some(p => p.requiresUserContext)) { recommendations.push('Create user context before seeding RLS-protected tables'); } if (analysisResult.rlsPolicies.some(p => p.command === 'INSERT')) { recommendations.push('Ensure seeding data includes required fields for RLS policies'); } } return recommendations; } /** * Clear temporary user context */ async clearUserContext() { if (this.currentUserContext?.email.includes('seed-user-')) { try { await this.client.auth.admin.deleteUser(this.currentUserContext.userId); logger_1.Logger.debug('Cleaned up temporary user context'); } catch (error) { logger_1.Logger.warn('Failed to cleanup temporary user context:', error); } } this.currentUserContext = undefined; } } exports.RLSCompliantSeeder = RLSCompliantSeeder; //# sourceMappingURL=rls-compliant-seeder.js.map