supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
585 lines • 23.2 kB
JavaScript
"use strict";
/**
* Business Logic Analyzer
* Analyzes database business logic patterns and data flows to determine optimal seeding strategies
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BusinessLogicAnalyzer = void 0;
const logger_1 = require("../../core/utils/logger");
const business_logic_types_1 = require("./business-logic-types");
class BusinessLogicAnalyzer {
constructor(client, options = {}) {
this.analysisCache = new Map();
this.client = client;
this.options = {
analyzeDepth: 'detailed',
includeTriggerAnalysis: true,
includeRLSAnalysis: true,
includeFunctionAnalysis: true,
frameworkHints: [],
expectedPatterns: [],
maxConcurrentQueries: 10,
queryTimeout: 30000,
enableCaching: true,
includeRecommendations: true,
includeAlternatives: true,
verboseOutput: false,
...options
};
}
/**
* Analyze business logic patterns in the database
*/
async analyzeBusinessLogic() {
const startTime = Date.now();
logger_1.Logger.info('🔍 Starting business logic analysis...');
try {
// Check cache first
const cacheKey = await this.generateCacheKey();
if (this.options.enableCaching && this.analysisCache.has(cacheKey)) {
logger_1.Logger.debug('Using cached business logic analysis');
return this.analysisCache.get(cacheKey).analysisResult;
}
const [triggerAnalysis, rlsPolicies, businessRules] = await Promise.all([
this.analyzeTriggers(),
this.analyzeRLSPolicies(),
this.extractBusinessRules()
]);
// Determine data flow pattern
const dataFlowPattern = this.determineDataFlowPattern(triggerAnalysis);
// Generate recommendations
const recommendedStrategy = this.generateRecommendedStrategy(dataFlowPattern, triggerAnalysis, rlsPolicies);
// Detect framework
const framework = this.detectFramework(triggerAnalysis, businessRules);
const confidence = this.calculateOverallConfidence(triggerAnalysis, dataFlowPattern, businessRules);
const result = {
success: true,
framework,
confidence,
dataFlowPattern,
recommendedStrategy,
triggerAnalysis,
rlsPolicies,
businessRules,
warnings: [],
errors: [],
executionTime: Date.now() - startTime
};
// Cache the result
if (this.options.enableCaching) {
this.cacheResult(cacheKey, result);
}
logger_1.Logger.success(`✅ Business logic analysis completed in ${result.executionTime}ms (confidence: ${(confidence * 100).toFixed(1)}%)`);
return result;
}
catch (error) {
logger_1.Logger.error('Business logic analysis failed:', error);
return {
success: false,
framework: 'unknown',
confidence: 0,
dataFlowPattern: {
type: 'unknown',
confidence: 0,
description: 'Analysis failed',
detectedFlow: [],
requiredSteps: [],
bypasses: []
},
recommendedStrategy: {
strategy: 'direct_table_insert',
reason: 'Analysis failed, falling back to direct insertion',
confidence: 0,
alternativeStrategies: [],
requiresUserContext: false,
respectsBusinessLogic: false
},
triggerAnalysis: {
triggersFound: [],
triggerFunctions: [],
userCreationFlow: null,
accountCreationFlow: null,
profileCreationFlow: null,
businessLogicTriggers: [],
confidence: 0
},
rlsPolicies: [],
businessRules: [],
warnings: [],
errors: [error.message],
executionTime: Date.now() - startTime
};
}
}
/**
* Analyze database triggers and their functions
*/
async analyzeTriggers() {
logger_1.Logger.debug('Analyzing database triggers...');
try {
// Query for triggers
const { data: triggers, error: triggerError } = await this.client
.from('information_schema.triggers')
.select(`
trigger_name,
event_object_table,
action_timing,
event_manipulation,
action_statement,
trigger_schema
`);
if (triggerError) {
logger_1.Logger.warn('Failed to query triggers from information_schema:', triggerError);
return this.fallbackTriggerAnalysis();
}
// Query for functions
const { data: functions, error: functionError } = await this.client
.from('information_schema.routines')
.select('routine_name, routine_schema, routine_definition, data_type');
if (functionError) {
logger_1.Logger.debug('Failed to get detailed functions, trying basic query');
}
const triggersFound = [];
const triggerFunctions = [];
// Process triggers
for (const trigger of triggers || []) {
const analyzedTrigger = await this.analyzeTrigger(trigger);
triggersFound.push(analyzedTrigger);
}
// Process functions
if (functions && Array.isArray(functions)) {
for (const func of functions) {
const analyzedFunction = await this.analyzeFunction(func);
triggerFunctions.push(analyzedFunction);
}
}
// Analyze specific flows
const userCreationFlow = this.analyzeUserCreationFlow(triggersFound, triggerFunctions);
const accountCreationFlow = this.analyzeAccountCreationFlow(triggersFound, triggerFunctions);
const profileCreationFlow = this.analyzeProfileCreationFlow(triggersFound, triggerFunctions);
// Identify business logic triggers
const businessLogicTriggers = this.identifyBusinessLogicTriggers(triggersFound);
const confidence = this.calculateTriggerAnalysisConfidence(triggersFound, triggerFunctions, userCreationFlow, accountCreationFlow, profileCreationFlow);
return {
triggersFound,
triggerFunctions,
userCreationFlow,
accountCreationFlow,
profileCreationFlow,
businessLogicTriggers,
confidence
};
}
catch (error) {
logger_1.Logger.error('Trigger analysis failed:', error);
return this.fallbackTriggerAnalysis();
}
}
/**
* Analyze individual trigger
*/
async analyzeTrigger(trigger) {
const functionName = this.extractFunctionName(trigger.action_statement);
return {
triggerName: trigger.trigger_name,
tableName: trigger.event_object_table,
timing: trigger.action_timing,
events: [trigger.event_manipulation],
functionName,
functionSchema: trigger.trigger_schema || 'public',
isEnabled: true,
analysisResult: {
businessLogicType: this.classifyTriggerBusinessLogic(trigger, functionName),
affectedTables: [trigger.event_object_table],
dependsOnAuth: this.triggerDependsOnAuth(functionName, trigger.action_statement),
requiresUserContext: this.triggerRequiresUserContext(functionName),
confidence: 0.8,
extractedRules: []
}
};
}
/**
* Analyze individual function
*/
async analyzeFunction(func) {
const businessRules = await this.parseBusinessRulesFromFunction(func);
return {
functionName: func.routine_name || func.name,
schema: func.routine_schema || func.schema || 'public',
definition: func.routine_definition || func.definition || '',
parameters: [],
returnType: func.data_type || 'void',
language: func.external_language || 'plpgsql',
volatility: 'VOLATILE',
parsedBusinessRules: businessRules,
analysisConfidence: businessRules.length > 0 ? 0.8 : 0.3
};
}
/**
* Parse business rules from function definition
*/
async parseBusinessRulesFromFunction(func) {
// This is a simplified version - in a real implementation you'd have more sophisticated parsing
const definition = func.routine_definition || func.definition || '';
const rules = [];
// Look for MakerKit patterns
if (definition.includes('setup_new_user') || definition.includes('kit.')) {
rules.push({
rule: 'MakerKit user setup workflow',
type: 'workflow',
affectedTables: ['accounts', 'profiles'],
conditions: ['auth.users trigger'],
actions: ['create_account', 'create_profile'],
confidence: 0.9
});
}
return rules;
}
/**
* Analyze RLS policies
*/
async analyzeRLSPolicies() {
if (!this.options.includeRLSAnalysis) {
return [];
}
logger_1.Logger.debug('Analyzing RLS policies...');
try {
const { data: policies, error } = await this.client
.from('pg_policies')
.select(`
policyname,
tablename,
permissive,
roles,
cmd,
qual,
with_check
`);
if (error) {
logger_1.Logger.warn('Failed to query RLS policies:', error);
return [];
}
return (policies || []).map(policy => ({
policyName: String(policy.policyname || 'unknown'),
tableName: String(policy.tablename || 'unknown'),
permissiveRestrictive: policy.permissive ? 'PERMISSIVE' : 'RESTRICTIVE',
roles: Array.isArray(policy.roles) ? policy.roles : [],
command: String(policy.cmd || 'ALL'),
using: policy.qual ? String(policy.qual) : undefined,
withCheck: policy.with_check ? String(policy.with_check) : undefined,
isEnabled: true,
affectsSeeding: this.policyAffectsSeeding(policy),
requiresUserContext: this.policyRequiresUserContext(policy),
bypassStrategies: [
{
strategy: 'service_role',
description: 'Use service role key to bypass RLS',
riskLevel: 'low',
suitableForSeeding: true
}
]
}));
}
catch (error) {
logger_1.Logger.warn('RLS policy analysis failed:', error);
return [];
}
}
/**
* Extract business rules from various sources
*/
async extractBusinessRules() {
const rules = [];
// Check for MakerKit-specific patterns
for (const pattern of business_logic_types_1.MAKERKIT_BUSINESS_LOGIC_PATTERNS) {
const rule = await this.checkForPattern(pattern);
if (rule) {
rules.push(rule);
}
}
return rules;
}
/**
* Check for specific business logic pattern
*/
async checkForPattern(pattern) {
// Simplified pattern matching - in real implementation would be more sophisticated
return {
id: `pattern_${pattern.name}`,
name: pattern.name,
type: 'trigger_based',
description: `Detected ${pattern.framework} pattern: ${pattern.name}`,
enforcementLevel: 'strict',
affectedTables: ['accounts', 'profiles'],
requirements: [
{
type: 'user_context',
description: 'Requires authenticated user context',
required: true,
autoFixable: false
}
],
violations: [],
autoFixAvailable: false,
confidence: pattern.confidence
};
}
/**
* Determine overall data flow pattern
*/
determineDataFlowPattern(triggerAnalysis) {
if (triggerAnalysis.userCreationFlow?.usesAuthTriggers) {
return {
type: 'auth_triggered',
confidence: 0.9,
description: 'Uses auth.users triggers for user creation workflow',
detectedFlow: [
'auth.admin.createUser() → auth.users INSERT',
'auth.users trigger → create account',
'account trigger → create profile'
],
requiredSteps: [
{
step: 1,
action: 'auth_create_user',
description: 'Create user via auth.admin.createUser()',
requiredContext: ['email', 'password'],
expectedOutcome: 'User created in auth.users'
},
{
step: 2,
action: 'wait_for_trigger',
description: 'Wait for trigger to create account and profile',
requiredContext: [],
expectedOutcome: 'Account and profile created automatically'
}
],
bypasses: []
};
}
return {
type: 'direct_insertion',
confidence: 0.7,
description: 'Direct table insertion pattern',
detectedFlow: ['Direct INSERT into tables'],
requiredSteps: [
{
step: 1,
action: 'direct_insert',
description: 'Insert directly into tables',
requiredContext: ['table_data'],
expectedOutcome: 'Records created directly'
}
],
bypasses: [
{
bypassType: 'rls_disable',
reason: 'Direct insertion may require RLS bypass',
riskLevel: 'medium',
suitableForSeeding: true
}
]
};
}
/**
* Generate recommended strategy based on analysis
*/
generateRecommendedStrategy(dataFlowPattern, triggerAnalysis, rlsPolicies) {
if (dataFlowPattern.type === 'auth_triggered') {
return {
strategy: 'auth_admin_create_user',
reason: 'Database uses auth triggers for user creation workflow',
confidence: 0.9,
alternativeStrategies: [
{
strategy: 'direct_table_insert',
reason: 'Bypass triggers for faster seeding',
confidence: 0.6,
tradeoffs: ['Bypasses business logic', 'May violate constraints']
}
],
requiresUserContext: true,
respectsBusinessLogic: true
};
}
return {
strategy: 'direct_table_insert',
reason: 'No auth trigger workflow detected',
confidence: 0.7,
alternativeStrategies: [],
requiresUserContext: false,
respectsBusinessLogic: false
};
}
/**
* Detect framework based on analysis
*/
detectFramework(triggerAnalysis, businessRules) {
// Look for MakerKit patterns
if (triggerAnalysis.triggerFunctions.some(f => f.functionName.includes('kit.') ||
f.functionName.includes('setup_new_user'))) {
return 'makerkit';
}
if (businessRules.some(rule => rule.name.includes('makerkit'))) {
return 'makerkit';
}
return 'generic';
}
/**
* Helper methods for trigger analysis
*/
extractFunctionName(actionStatement) {
const match = actionStatement?.match(/EXECUTE (?:FUNCTION|PROCEDURE) ([^\(]+)/i);
return match ? match[1].trim() : 'unknown';
}
classifyTriggerBusinessLogic(trigger, functionName) {
if (functionName.includes('setup_new_user') || functionName.includes('handle_new_user')) {
return 'user_creation';
}
return 'custom';
}
triggerDependsOnAuth(functionName, actionStatement) {
return functionName.includes('auth') ||
functionName.includes('user') ||
actionStatement?.includes('auth.');
}
triggerRequiresUserContext(functionName) {
return functionName.includes('user') || functionName.includes('auth');
}
policyAffectsSeeding(policy) {
return policy.cmd !== 'SELECT'; // INSERT, UPDATE, DELETE policies affect seeding
}
policyRequiresUserContext(policy) {
const using = policy.qual || '';
const withCheck = policy.with_check || '';
return using.includes('auth.uid()') || withCheck.includes('auth.uid()');
}
analyzeUserCreationFlow(triggers, functions) {
const authTriggers = triggers.filter(t => t.tableName === 'users' ||
t.analysisResult.dependsOnAuth);
if (authTriggers.length > 0) {
return {
usesAuthTriggers: true,
triggerFunction: authTriggers[0].functionName,
createsAccount: true,
createsProfile: true,
requiredFields: ['email'],
optionalFields: ['name', 'avatar_url'],
businessLogicSteps: ['create_account', 'create_profile'],
confidence: 0.8
};
}
return null;
}
analyzeAccountCreationFlow(triggers, functions) {
const accountTriggers = triggers.filter(t => t.tableName === 'accounts');
if (accountTriggers.length > 0) {
return {
triggeredByAuth: true,
triggerFunction: accountTriggers[0].functionName,
defaultValues: { is_personal_account: true, slug: null },
constraints: ['accounts_slug_null_if_personal_account_true'],
requiredRelationships: ['auth.users'],
confidence: 0.8
};
}
return null;
}
analyzeProfileCreationFlow(triggers, functions) {
const profileTriggers = triggers.filter(t => t.tableName === 'profiles');
if (profileTriggers.length > 0) {
return {
triggeredByAccount: true,
triggerFunction: profileTriggers[0].functionName,
defaultValues: {},
linkedToAccount: true,
linkedToAuth: true,
confidence: 0.8
};
}
return null;
}
identifyBusinessLogicTriggers(triggers) {
return triggers
.filter(t => t.analysisResult.businessLogicType !== 'custom')
.map(t => ({
triggerName: t.triggerName,
purpose: t.analysisResult.businessLogicType,
description: `${t.analysisResult.businessLogicType} trigger on ${t.tableName}`,
affectedWorkflows: ['user_creation'],
bypassable: false,
criticality: 'high'
}));
}
calculateTriggerAnalysisConfidence(...args) {
// Simplified confidence calculation
return 0.8;
}
calculateOverallConfidence(triggerAnalysis, dataFlowPattern, businessRules) {
return (triggerAnalysis.confidence + dataFlowPattern.confidence + (businessRules.length > 0 ? 0.8 : 0.2)) / 3;
}
fallbackTriggerAnalysis() {
return {
triggersFound: [],
triggerFunctions: [],
userCreationFlow: null,
accountCreationFlow: null,
profileCreationFlow: null,
businessLogicTriggers: [],
confidence: 0
};
}
async generateCacheKey() {
return `business_logic_${Date.now()}`;
}
cacheResult(key, result) {
const cacheEntry = {
key,
schemaHash: 'temp_hash',
timestamp: new Date().toISOString(),
expiresAt: new Date(Date.now() + 1000 * 60 * 60).toISOString(), // 1 hour
analysisResult: result,
confidence: result.confidence
};
this.analysisCache.set(key, cacheEntry);
}
/**
* Get analysis recommendations
*/
getRecommendations(analysisResult) {
const recommendations = [];
if (analysisResult.dataFlowPattern.type === 'auth_triggered') {
recommendations.push({
type: 'strategy',
priority: 'high',
title: 'Use Auth-Based User Creation',
description: 'Use auth.admin.createUser() to respect trigger-based workflow',
implementation: 'Configure strategy to use auth.admin.createUser() instead of direct insertion',
benefits: ['Respects business logic', 'Maintains data integrity', 'Follows intended workflow'],
risks: ['Slower than direct insertion', 'Requires auth configuration'],
applicableFrameworks: ['makerkit']
});
}
if (analysisResult.rlsPolicies.length > 0) {
recommendations.push({
type: 'configuration',
priority: 'medium',
title: 'Configure RLS Bypass',
description: 'Use service role for seeding operations',
implementation: 'Ensure SUPABASE_SERVICE_ROLE_KEY is configured',
benefits: ['Bypasses RLS restrictions', 'Enables full data access'],
risks: ['Full database access', 'Must be used carefully'],
applicableFrameworks: ['all']
});
}
return recommendations;
}
/**
* Clear analysis cache
*/
clearCache() {
this.analysisCache.clear();
}
}
exports.BusinessLogicAnalyzer = BusinessLogicAnalyzer;
//# sourceMappingURL=business-logic-analyzer.js.map