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
JavaScript
"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()}.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