supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
464 lines • 19.2 kB
JavaScript
;
/**
* Complex Multi-Table Constraint Resolution System for Epic 1: Universal MakerKit Core System
* Advanced strategies for resolving constraints that span multiple tables and complex relationships
* Part of Task 1.5.4: Add complex multi-table constraint resolution strategies
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiTableConstraintResolver = void 0;
const logger_1 = require("../core/utils/logger");
const slug_management_system_1 = require("./slug-management-system");
/**
* Advanced Multi-Table Constraint Resolver
*/
class MultiTableConstraintResolver {
constructor(client, config = {}) {
this.dependencyGraph = new Map();
this.client = client;
this.slugManager = new slug_management_system_1.AdvancedSlugManager(client);
this.config = {
enableCascadeResolution: true,
enableDependencyCreation: true,
maxResolutionDepth: 5,
enableParallelResolution: false,
dryRun: false,
strictMode: false,
...config
};
}
/**
* Resolve complex multi-table constraints for MakerKit data
*/
async resolveMultiTableConstraints(data, tableName, constraints) {
const startTime = Date.now();
const result = {
success: true,
resolvedConstraints: 0,
createdDependencies: [],
modifiedRecords: [],
warnings: [],
errors: [],
executionMetrics: {
totalExecutionTime: 0,
tablesAnalyzed: 0,
constraintsProcessed: 0,
dependenciesCreated: 0
}
};
try {
// Step 1: Build constraint dependency graph
await this.buildConstraintDependencyGraph(constraints, tableName);
// Step 2: Resolve constraints in dependency order
await this.resolveConstraintsInOrder(data, tableName, result);
// Step 3: Handle MakerKit-specific multi-table patterns
await this.resolveMakerKitPatterns(data, tableName, result);
// Step 4: Execute dependency operations
if (this.config.enableDependencyCreation && !this.config.dryRun) {
await this.executeDependencyOperations(result);
}
result.executionMetrics.totalExecutionTime = Date.now() - startTime;
logger_1.Logger.info(`Multi-table constraint resolution completed in ${result.executionMetrics.totalExecutionTime}ms`);
}
catch (error) {
result.success = false;
result.errors.push(`Multi-table constraint resolution failed: ${error.message}`);
logger_1.Logger.error('Multi-table constraint resolution error:', error);
}
return result;
}
/**
* Build constraint dependency graph for resolution ordering
*/
async buildConstraintDependencyGraph(constraints, tableName) {
this.dependencyGraph.clear();
for (const constraint of constraints) {
const nodeId = `${tableName}.${constraint.constraintName}`;
const dependencies = await this.analyzeConstraintDependencies(constraint, tableName);
this.dependencyGraph.set(nodeId, {
table: tableName,
constraint,
dependencies,
dependents: [],
resolved: false,
priority: this.calculateConstraintPriority(constraint)
});
}
// Build dependent relationships
for (const [nodeId, node] of this.dependencyGraph) {
for (const dependency of node.dependencies) {
const depNode = this.dependencyGraph.get(dependency);
if (depNode) {
depNode.dependents.push(nodeId);
}
}
}
logger_1.Logger.debug(`Built constraint dependency graph with ${this.dependencyGraph.size} nodes`);
}
/**
* Analyze dependencies for a specific constraint
*/
async analyzeConstraintDependencies(constraint, tableName) {
const dependencies = [];
// Foreign key dependencies
if (constraint.constraintType === 'foreign_key') {
const referencedTable = constraint.referencedTable;
if (referencedTable && referencedTable !== tableName) {
dependencies.push(`${referencedTable}.*`);
}
}
// Check constraint dependencies (analyze SQL)
if (constraint.constraintType === 'check' && constraint.checkClause) {
const referencedTables = this.extractReferencedTables(constraint.checkClause);
dependencies.push(...referencedTables.map(table => `${table}.*`));
}
// MakerKit-specific dependencies
if (this.isMakerKitAccountConstraint(constraint, tableName)) {
dependencies.push('auth.users.*', 'profiles.*');
}
if (this.isMakerKitOrganizationConstraint(constraint, tableName)) {
dependencies.push('accounts.*', 'organization_members.*');
}
return dependencies;
}
/**
* Resolve constraints in dependency order
*/
async resolveConstraintsInOrder(data, tableName, result) {
const resolutionQueue = Array.from(this.dependencyGraph.values())
.sort((a, b) => b.priority - a.priority);
for (const node of resolutionQueue) {
if (node.resolved)
continue;
// Check if all dependencies are resolved
const unresolvedDeps = node.dependencies.filter(dep => {
const depNode = this.findNodeByDependency(dep);
return depNode && !depNode.resolved;
});
if (unresolvedDeps.length > 0 && this.config.strictMode) {
result.warnings.push(`Skipping constraint ${node.constraint.constraintName} - unresolved dependencies: ${unresolvedDeps.join(', ')}`);
continue;
}
// Resolve the constraint
const resolutionResult = await this.resolveComplexConstraint(node.constraint, data, tableName);
if (resolutionResult.success) {
node.resolved = true;
result.resolvedConstraints++;
result.modifiedRecords.push(...this.convertToRecordModifications(resolutionResult, tableName));
result.warnings.push(...resolutionResult.warnings);
}
else {
result.errors.push(...resolutionResult.errors);
}
result.executionMetrics.constraintsProcessed++;
}
}
/**
* Resolve MakerKit-specific multi-table patterns
*/
async resolveMakerKitPatterns(data, tableName, result) {
// Pattern 1: Account creation with organization and membership
if (tableName === 'accounts' && data.is_personal_account === false) {
await this.resolveAccountOrganizationPattern(data, result);
}
// Pattern 2: User creation with profile and auth identity
if (tableName === 'users' || (tableName === 'accounts' && data.user_id)) {
await this.resolveUserProfilePattern(data, result);
}
// Pattern 3: Organization creation with owner membership
if (tableName === 'organizations') {
await this.resolveOrganizationOwnerPattern(data, result);
}
// Pattern 4: Subscription creation with account validation
if (tableName === 'subscriptions') {
await this.resolveSubscriptionAccountPattern(data, result);
}
// Pattern 5: Invitation workflow with expiry and role validation
if (tableName === 'invitations') {
await this.resolveInvitationWorkflowPattern(data, result);
}
}
/**
* Resolve account-organization pattern
*/
async resolveAccountOrganizationPattern(data, result) {
// Team accounts need organization
if (!data.organization_id) {
const organizationData = {
name: data.name || `${data.user_name || 'User'}'s Organization`,
slug: await this.slugManager.generateUniqueSlug(data.name || data.user_name || 'organization', 'organizations'),
created_by: data.user_id,
account_id: data.id
};
result.createdDependencies.push({
operation: 'create',
targetTable: 'organizations',
data: organizationData,
reason: 'Team account requires organization',
priority: 1,
dependencies: []
});
// Create owner membership
result.createdDependencies.push({
operation: 'create',
targetTable: 'organization_members',
data: {
organization_id: '${organizations.id}', // Reference to created org
user_id: data.user_id,
role: 'owner',
is_owner: true,
invited_by: data.user_id
},
reason: 'Create owner membership for new organization',
priority: 2,
dependencies: ['organizations']
});
}
}
/**
* Resolve user-profile pattern
*/
async resolveUserProfilePattern(data, result) {
const userId = data.user_id || data.id;
if (userId && !data.profile_id) {
// Check if profile exists
const { data: existingProfile } = await this.client
.from('profiles')
.select('id')
.eq('id', userId)
.limit(1);
if (!existingProfile || existingProfile.length === 0) {
result.createdDependencies.push({
operation: 'create',
targetTable: 'profiles',
data: {
id: userId,
email: data.email || data.auth_email,
full_name: data.full_name || data.name,
avatar_url: data.avatar_url,
updated_at: new Date().toISOString()
},
reason: 'Create profile for user account',
priority: 1,
dependencies: []
});
}
}
}
/**
* Resolve organization-owner pattern
*/
async resolveOrganizationOwnerPattern(data, result) {
if (data.created_by && !data.owner_membership_created) {
result.createdDependencies.push({
operation: 'create',
targetTable: 'organization_members',
data: {
organization_id: data.id,
user_id: data.created_by,
role: 'owner',
is_owner: true,
invited_by: data.created_by,
joined_at: new Date().toISOString()
},
reason: 'Create owner membership for new organization',
priority: 1,
dependencies: []
});
}
}
/**
* Resolve subscription-account pattern
*/
async resolveSubscriptionAccountPattern(data, result) {
// Validate account exists and is compatible with subscription
if (data.account_id) {
const { data: account } = await this.client
.from('accounts')
.select('is_personal_account, account_type')
.eq('id', data.account_id)
.limit(1);
if (account && account.length > 0) {
const accountData = account[0];
// Team subscriptions require team accounts
if (data.subscription_type === 'team' && accountData.is_personal_account) {
result.warnings.push('Team subscription assigned to personal account - consider subscription type mismatch');
}
// Personal subscriptions should use personal accounts
if (data.subscription_type === 'individual' && !accountData.is_personal_account) {
result.warnings.push('Individual subscription assigned to team account - consider subscription type mismatch');
}
}
}
}
/**
* Resolve invitation workflow pattern
*/
async resolveInvitationWorkflowPattern(data, result) {
// Set expiry if not provided
if (!data.expires_at) {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + 7); // 7 days from now
result.modifiedRecords.push({
table: 'invitations',
recordId: data.id || 'new',
field: 'expires_at',
oldValue: null,
newValue: expiryDate.toISOString(),
reason: 'Set default invitation expiry',
confidence: 0.9
});
}
// Validate role against organization permissions
if (data.role && data.organization_id) {
const validRoles = ['owner', 'admin', 'member', 'viewer'];
if (!validRoles.includes(data.role)) {
result.modifiedRecords.push({
table: 'invitations',
recordId: data.id || 'new',
field: 'role',
oldValue: data.role,
newValue: 'member',
reason: 'Invalid role, defaulting to member',
confidence: 0.8
});
}
}
}
/**
* Execute dependency operations
*/
async executeDependencyOperations(result) {
// Sort by priority
const sortedOperations = result.createdDependencies.sort((a, b) => a.priority - b.priority);
for (const operation of sortedOperations) {
try {
switch (operation.operation) {
case 'create':
await this.executeDependencyCreate(operation);
break;
case 'update':
await this.executeDependencyUpdate(operation);
break;
case 'link':
await this.executeDependencyLink(operation);
break;
}
result.executionMetrics.dependenciesCreated++;
logger_1.Logger.debug(`Executed dependency operation: ${operation.operation} on ${operation.targetTable}`);
}
catch (error) {
result.errors.push(`Failed to execute dependency operation: ${error.message}`);
logger_1.Logger.error(`Dependency operation failed:`, error);
}
}
}
/**
* Execute dependency create operation
*/
async executeDependencyCreate(operation) {
const { data: created, error } = await this.client
.from(operation.targetTable)
.insert(operation.data)
.select()
.single();
if (error) {
throw new Error(`Failed to create ${operation.targetTable}: ${error.message}`);
}
// Store created record ID for reference resolution
this.storeCreatedReference(operation.targetTable, created.id);
}
/**
* Execute dependency update operation
*/
async executeDependencyUpdate(operation) {
const { error } = await this.client
.from(operation.targetTable)
.update(operation.data)
.match({ id: operation.data.id });
if (error) {
throw new Error(`Failed to update ${operation.targetTable}: ${error.message}`);
}
}
/**
* Execute dependency link operation
*/
async executeDependencyLink(operation) {
// Implementation for linking existing records
logger_1.Logger.debug(`Linking operation for ${operation.targetTable} not yet implemented`);
}
/**
* Helper methods
*/
resolveComplexConstraint(constraint, data, tableName) {
// Implement complex constraint resolution logic
return Promise.resolve({
success: true,
originalData: data,
modifiedData: data,
appliedFixes: [],
warnings: [],
errors: [],
bypassRequired: false
});
}
extractReferencedTables(checkClause) {
// Simple regex to extract table references from SQL
const tablePattern = /FROM\s+(\w+)|JOIN\s+(\w+)|EXISTS\s*\([^)]*FROM\s+(\w+)/gi;
const matches = [];
let match;
while ((match = tablePattern.exec(checkClause)) !== null) {
const table = match[1] || match[2] || match[3];
if (table)
matches.push(table);
}
return [...new Set(matches)];
}
isMakerKitAccountConstraint(constraint, tableName) {
return tableName === 'accounts' && (constraint.constraintName?.toLowerCase().includes('account') ||
constraint.checkClause?.toLowerCase().includes('is_personal_account'));
}
isMakerKitOrganizationConstraint(constraint, tableName) {
return (tableName === 'organizations' || tableName === 'organization_members') && (constraint.constraintName?.toLowerCase().includes('organization') ||
constraint.checkClause?.toLowerCase().includes('organization'));
}
calculateConstraintPriority(constraint) {
// Higher priority for critical constraints
if (constraint.constraintType === 'not_null')
return 100;
if (constraint.constraintType === 'primary_key')
return 90;
if (constraint.constraintType === 'foreign_key')
return 80;
if (constraint.constraintType === 'unique')
return 70;
if (constraint.constraintType === 'check')
return 60;
return 50;
}
findNodeByDependency(dependency) {
for (const node of this.dependencyGraph.values()) {
if (dependency.startsWith(node.table)) {
return node;
}
}
return undefined;
}
convertToRecordModifications(result, tableName) {
return result.appliedFixes.map(fix => ({
table: tableName,
recordId: 'unknown',
field: fix.field || 'unknown',
oldValue: fix.oldValue,
newValue: fix.newValue,
reason: fix.reason,
confidence: fix.confidence || 0.8
}));
}
storeCreatedReference(table, id) {
// Store reference for later resolution (simplified implementation)
logger_1.Logger.debug(`Stored reference: ${table}.${id}`);
}
}
exports.MultiTableConstraintResolver = MultiTableConstraintResolver;
exports.default = MultiTableConstraintResolver;
//# sourceMappingURL=multi-table-constraint-resolver.js.map