supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
583 lines • 24.8 kB
JavaScript
;
/**
* Custom Relationship Manager for Epic 7: Configuration Extensibility Framework
* Manages custom table relationship definitions and validation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CustomRelationshipManager = void 0;
const logger_1 = require("../core/utils/logger");
class CustomRelationshipManager {
constructor(client) {
this.relationshipCache = new Map();
this.client = client;
}
/**
* Comprehensive validation of custom relationship definitions
*/
async validateCustomRelationships(relationships) {
const errors = [];
const warnings = [];
const recommendations = [];
if (!relationships?.enabled) {
return {
valid: true,
errors,
warnings: ['Custom relationships disabled'],
recommendations: ['Consider enabling custom relationships for better data modeling'],
relationshipGraph: { nodes: [], edges: [], cycles: [], orphanTables: [] }
};
}
logger_1.Logger.info('🔗 Validating custom relationship definitions...');
try {
// Build relationship graph
const relationshipGraph = await this.buildRelationshipGraph(relationships);
// Validate individual relationships
if (relationships.relationships) {
for (const relationship of relationships.relationships) {
const relationshipErrors = await this.validateSingleRelationship(relationship);
errors.push(...relationshipErrors);
}
}
// Validate junction tables
if (relationships.junctionTables) {
for (const junctionTable of relationships.junctionTables) {
const junctionErrors = await this.validateJunctionTable(junctionTable);
errors.push(...junctionErrors);
}
}
// Validate inheritance rules
if (relationships.inheritanceRules) {
for (const inheritanceRule of relationships.inheritanceRules) {
const inheritanceErrors = await this.validateInheritanceRule(inheritanceRule);
errors.push(...inheritanceErrors);
}
}
// Analyze relationship graph for issues
const graphAnalysis = this.analyzeRelationshipGraph(relationshipGraph);
warnings.push(...graphAnalysis.warnings);
recommendations.push(...graphAnalysis.recommendations);
// Generate final recommendations
recommendations.push(`Validated ${relationships.relationships?.length || 0} custom relationships`);
recommendations.push(`Validated ${relationships.junctionTables?.length || 0} junction tables`);
recommendations.push(`Validated ${relationships.inheritanceRules?.length || 0} inheritance rules`);
if (relationshipGraph.cycles.length > 0) {
warnings.push(`Detected ${relationshipGraph.cycles.length} circular dependencies in relationship graph`);
recommendations.push('Consider breaking circular dependencies with nullable foreign keys or junction tables');
}
logger_1.Logger.info(`✅ Custom relationship validation completed: ${errors.length} errors, ${warnings.length} warnings`);
return {
valid: errors.length === 0,
errors,
warnings,
recommendations,
relationshipGraph
};
}
catch (error) {
logger_1.Logger.error('Custom relationship validation failed:', error);
return {
valid: false,
errors: [`Validation error: ${error.message}`],
warnings: [],
recommendations: ['Fix validation errors and retry'],
relationshipGraph: { nodes: [], edges: [], cycles: [], orphanTables: [] }
};
}
}
/**
* Generate execution plan for relationship seeding
*/
async generateRelationshipExecutionPlan(relationships) {
if (!relationships?.enabled || !relationships.relationships) {
return {
executionOrder: [],
batchGroups: [],
junctionTableCreation: [],
estimatedComplexity: 'low'
};
}
logger_1.Logger.info('📋 Generating relationship execution plan...');
const relationshipGraph = await this.buildRelationshipGraph(relationships);
// Perform topological sort to determine execution order
const executionOrder = this.topologicalSortTables(relationshipGraph);
// Group tables into batches that can execute in parallel
const batchGroups = this.createBatchGroups(relationshipGraph, executionOrder);
// Plan junction table creation
const junctionTableCreation = this.planJunctionTableCreation(relationships.junctionTables || []);
// Estimate complexity
const estimatedComplexity = this.estimateRelationshipComplexity(relationshipGraph, relationships);
const plan = {
executionOrder,
batchGroups,
junctionTableCreation,
estimatedComplexity
};
logger_1.Logger.info(`✅ Generated execution plan: ${executionOrder.length} tables, ${batchGroups.length} batch groups`);
return plan;
}
/**
* Create custom relationship generators
*/
createRelationshipGenerators(relationships) {
const generators = new Map();
for (const relationship of relationships) {
const generatorKey = `${relationship.fromTable}_to_${relationship.toTable}`;
switch (relationship.generationStrategy) {
case 'sequential':
generators.set(generatorKey, this.createSequentialGenerator(relationship));
break;
case 'random':
generators.set(generatorKey, this.createRandomGenerator(relationship));
break;
case 'weighted':
generators.set(generatorKey, this.createWeightedGenerator(relationship));
break;
case 'custom':
if (relationship.customGenerator) {
generators.set(generatorKey, this.createCustomGenerator(relationship));
}
break;
}
}
return generators;
}
/**
* Apply inheritance rules to data
*/
applyInheritanceRules(data, inheritanceRules) {
const processedData = { ...data };
for (const rule of inheritanceRules) {
if (!processedData[rule.parentTable] || !processedData[rule.childTable]) {
continue;
}
const parentData = processedData[rule.parentTable];
const childData = processedData[rule.childTable];
processedData[rule.childTable] = childData.map(childRecord => {
// Find matching parent record (assuming ID-based inheritance)
const parentRecord = parentData.find(parent => parent.id === childRecord.parent_id);
if (!parentRecord) {
return childRecord;
}
// Apply inheritance based on strategy
switch (rule.overrideStrategy) {
case 'extend':
return {
...childRecord,
...this.pickFields(parentRecord, rule.inheritedFields)
};
case 'replace':
return {
...this.pickFields(parentRecord, rule.inheritedFields),
...childRecord
};
case 'merge':
return this.mergeRecords(this.pickFields(parentRecord, rule.inheritedFields), childRecord);
default:
return childRecord;
}
});
}
return processedData;
}
/**
* Private helper methods
*/
async validateSingleRelationship(relationship) {
const errors = [];
// Basic validation
if (!relationship.id) {
errors.push('Relationship ID is required');
}
if (!relationship.fromTable || !relationship.toTable) {
errors.push(`Relationship '${relationship.id}' must have fromTable and toTable`);
}
if (!relationship.fromColumn || !relationship.toColumn) {
errors.push(`Relationship '${relationship.id}' must have fromColumn and toColumn`);
}
if (!['one_to_one', 'one_to_many', 'many_to_many'].includes(relationship.relationshipType)) {
errors.push(`Invalid relationship type '${relationship.relationshipType}' for relationship '${relationship.id}'`);
}
if (!['sequential', 'random', 'weighted', 'custom'].includes(relationship.generationStrategy)) {
errors.push(`Invalid generation strategy '${relationship.generationStrategy}' for relationship '${relationship.id}'`);
}
// Custom generator validation
if (relationship.generationStrategy === 'custom' && !relationship.customGenerator) {
errors.push(`Custom generation strategy specified but no customGenerator provided for relationship '${relationship.id}'`);
}
// Table existence validation
try {
const fromTableExists = await this.tableExists(relationship.fromTable);
const toTableExists = await this.tableExists(relationship.toTable);
if (!fromTableExists) {
errors.push(`From table '${relationship.fromTable}' does not exist for relationship '${relationship.id}'`);
}
if (!toTableExists) {
errors.push(`To table '${relationship.toTable}' does not exist for relationship '${relationship.id}'`);
}
// Column existence validation if tables exist
if (fromTableExists) {
const fromColumnExists = await this.columnExists(relationship.fromTable, relationship.fromColumn);
if (!fromColumnExists) {
errors.push(`From column '${relationship.fromColumn}' does not exist in table '${relationship.fromTable}'`);
}
}
if (toTableExists) {
const toColumnExists = await this.columnExists(relationship.toTable, relationship.toColumn);
if (!toColumnExists) {
errors.push(`To column '${relationship.toColumn}' does not exist in table '${relationship.toTable}'`);
}
}
}
catch (error) {
errors.push(`Could not validate tables for relationship '${relationship.id}': ${error.message}`);
}
return errors;
}
async validateJunctionTable(junctionTable) {
const errors = [];
if (!junctionTable.tableName) {
errors.push('Junction table name is required');
}
if (!junctionTable.leftTable || !junctionTable.rightTable) {
errors.push(`Junction table '${junctionTable.tableName}' must have leftTable and rightTable`);
}
if (!junctionTable.leftColumn || !junctionTable.rightColumn) {
errors.push(`Junction table '${junctionTable.tableName}' must have leftColumn and rightColumn`);
}
// Check table existence
try {
const junctionExists = await this.tableExists(junctionTable.tableName);
const leftExists = await this.tableExists(junctionTable.leftTable);
const rightExists = await this.tableExists(junctionTable.rightTable);
if (!junctionExists) {
errors.push(`Junction table '${junctionTable.tableName}' does not exist`);
}
if (!leftExists) {
errors.push(`Left table '${junctionTable.leftTable}' does not exist for junction table '${junctionTable.tableName}'`);
}
if (!rightExists) {
errors.push(`Right table '${junctionTable.rightTable}' does not exist for junction table '${junctionTable.tableName}'`);
}
}
catch (error) {
errors.push(`Could not validate junction table '${junctionTable.tableName}': ${error.message}`);
}
return errors;
}
async validateInheritanceRule(inheritanceRule) {
const errors = [];
if (!inheritanceRule.parentTable || !inheritanceRule.childTable) {
errors.push('Inheritance rule must have parentTable and childTable');
}
if (!inheritanceRule.inheritedFields || inheritanceRule.inheritedFields.length === 0) {
errors.push('Inheritance rule must specify inheritedFields');
}
if (!['extend', 'replace', 'merge'].includes(inheritanceRule.overrideStrategy)) {
errors.push(`Invalid override strategy '${inheritanceRule.overrideStrategy}' - must be one of: extend, replace, merge`);
}
// Check table existence
try {
const parentExists = await this.tableExists(inheritanceRule.parentTable);
const childExists = await this.tableExists(inheritanceRule.childTable);
if (!parentExists) {
errors.push(`Parent table '${inheritanceRule.parentTable}' does not exist`);
}
if (!childExists) {
errors.push(`Child table '${inheritanceRule.childTable}' does not exist`);
}
}
catch (error) {
errors.push(`Could not validate inheritance rule: ${error.message}`);
}
return errors;
}
async buildRelationshipGraph(relationships) {
const nodes = [];
const edges = [];
const tableSet = new Set();
// Collect all tables involved in relationships
if (relationships?.relationships) {
for (const rel of relationships.relationships) {
tableSet.add(rel.fromTable);
tableSet.add(rel.toTable);
}
}
// Add junction tables
if (relationships?.junctionTables) {
for (const jt of relationships.junctionTables) {
tableSet.add(jt.tableName);
tableSet.add(jt.leftTable);
tableSet.add(jt.rightTable);
}
}
// Create nodes
for (const table of tableSet) {
const isJunction = relationships?.junctionTables?.some(jt => jt.tableName === table) || false;
const columns = await this.getTableColumns(table);
nodes.push({
table,
type: isJunction ? 'junction' : 'entity',
columns
});
}
// Create edges
if (relationships?.relationships) {
for (const rel of relationships.relationships) {
edges.push({
from: rel.fromTable,
to: rel.toTable,
relationship: rel,
strength: rel.isRequired ? 'strong' : 'weak'
});
}
}
// Detect cycles
const cycles = this.detectCycles(nodes, edges);
// Find orphan tables
const orphanTables = this.findOrphanTables(nodes, edges);
return {
nodes,
edges,
cycles,
orphanTables
};
}
analyzeRelationshipGraph(graph) {
const warnings = [];
const recommendations = [];
// Check for complex many-to-many relationships without junction tables
const manyToManyRels = graph.edges.filter(edge => edge.relationship.relationshipType === 'many_to_many');
if (manyToManyRels.length > 0) {
recommendations.push(`Consider creating junction tables for ${manyToManyRels.length} many-to-many relationships`);
}
// Check for high-degree nodes (tables with many relationships)
const nodeDegrees = new Map();
graph.edges.forEach(edge => {
nodeDegrees.set(edge.from, (nodeDegrees.get(edge.from) || 0) + 1);
nodeDegrees.set(edge.to, (nodeDegrees.get(edge.to) || 0) + 1);
});
const highDegreeNodes = Array.from(nodeDegrees.entries())
.filter(([, degree]) => degree > 5)
.map(([table]) => table);
if (highDegreeNodes.length > 0) {
warnings.push(`High-degree tables detected: ${highDegreeNodes.join(', ')} - may indicate design issues`);
}
// Check for orphan tables
if (graph.orphanTables.length > 0) {
warnings.push(`Orphan tables detected: ${graph.orphanTables.join(', ')} - no relationships defined`);
}
return { warnings, recommendations };
}
topologicalSortTables(graph) {
const visited = new Set();
const visiting = new Set();
const result = [];
const visit = (table) => {
if (visiting.has(table)) {
// Cycle detected - handle gracefully
return;
}
if (visited.has(table)) {
return;
}
visiting.add(table);
// Visit all dependencies first
const dependencies = graph.edges
.filter(edge => edge.to === table)
.map(edge => edge.from);
for (const dep of dependencies) {
visit(dep);
}
visiting.delete(table);
visited.add(table);
result.push(table);
};
// Visit all nodes
graph.nodes.forEach(node => visit(node.table));
return result;
}
createBatchGroups(graph, executionOrder) {
const batchGroups = [];
const processed = new Set();
for (const table of executionOrder) {
if (processed.has(table))
continue;
// Find all tables that can be processed in parallel with this one
const batch = [table];
const dependencies = this.getTableDependencies(graph, table);
// Add tables that have the same dependencies and can be processed in parallel
for (const otherTable of executionOrder) {
if (otherTable === table || processed.has(otherTable))
continue;
const otherDependencies = this.getTableDependencies(graph, otherTable);
const canProcessInParallel = this.canProcessInParallel(dependencies, otherDependencies);
if (canProcessInParallel) {
batch.push(otherTable);
}
}
batch.forEach(t => processed.add(t));
batchGroups.push({
tables: batch,
canExecuteInParallel: batch.length > 1,
dependencies: Array.from(dependencies)
});
}
return batchGroups;
}
planJunctionTableCreation(junctionTables) {
return junctionTables.map((jt, index) => ({
junctionTable: jt.tableName,
dependsOn: [jt.leftTable, jt.rightTable],
executionOrder: index + 1000 // Execute after main tables
}));
}
estimateRelationshipComplexity(graph, relationships) {
const nodeCount = graph.nodes.length;
const edgeCount = graph.edges.length;
const cycleCount = graph.cycles.length;
const junctionTableCount = relationships?.junctionTables?.length || 0;
if (nodeCount <= 5 && edgeCount <= 10 && cycleCount === 0) {
return 'low';
}
else if (nodeCount <= 15 && edgeCount <= 30 && cycleCount <= 2) {
return 'medium';
}
else if (nodeCount <= 50 && edgeCount <= 100 && cycleCount <= 5) {
return 'high';
}
else {
return 'critical';
}
}
detectCycles(nodes, edges) {
const cycles = [];
const visited = new Set();
const recStack = new Set();
const dfs = (node, path) => {
visited.add(node);
recStack.add(node);
path.push(node);
const neighbors = edges
.filter(edge => edge.from === node)
.map(edge => edge.to);
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
dfs(neighbor, [...path]);
}
else if (recStack.has(neighbor)) {
// Found a cycle
const cycleStart = path.indexOf(neighbor);
if (cycleStart >= 0) {
cycles.push(path.slice(cycleStart));
}
}
}
recStack.delete(node);
};
nodes.forEach(node => {
if (!visited.has(node.table)) {
dfs(node.table, []);
}
});
return cycles;
}
findOrphanTables(nodes, edges) {
const connectedTables = new Set();
edges.forEach(edge => {
connectedTables.add(edge.from);
connectedTables.add(edge.to);
});
return nodes
.filter(node => !connectedTables.has(node.table))
.map(node => node.table);
}
getTableDependencies(graph, table) {
return new Set(graph.edges
.filter(edge => edge.to === table)
.map(edge => edge.from));
}
canProcessInParallel(deps1, deps2) {
// Tables can be processed in parallel if they don't depend on each other
// and have similar dependency sets
const intersection = new Set([...deps1].filter(x => deps2.has(x)));
const union = new Set([...deps1, ...deps2]);
// Simple heuristic: if they share most dependencies, they can be parallel
return intersection.size / union.size > 0.7;
}
createSequentialGenerator(relationship) {
let counter = 0;
return (fromId, context) => {
counter++;
return { [relationship.toColumn]: `${fromId}_${counter}` };
};
}
createRandomGenerator(relationship) {
return (fromId, context) => {
const randomValue = Math.floor(Math.random() * 1000000);
return { [relationship.toColumn]: `${fromId}_${randomValue}` };
};
}
createWeightedGenerator(relationship) {
return (fromId, context) => {
// Simple weighted generation based on context
const weight = context?.weight || 1;
const value = Math.floor(Math.random() * 1000 * weight);
return { [relationship.toColumn]: `${fromId}_${value}` };
};
}
createCustomGenerator(relationship) {
return (fromId, context) => {
// Placeholder for custom generator implementation
// In a real implementation, this would load and execute the custom function
return { [relationship.toColumn]: `custom_${fromId}` };
};
}
pickFields(record, fields) {
const result = {};
fields.forEach(field => {
if (record.hasOwnProperty(field)) {
result[field] = record[field];
}
});
return result;
}
mergeRecords(parent, child) {
const result = { ...parent };
Object.keys(child).forEach(key => {
if (child[key] !== null && child[key] !== undefined) {
result[key] = child[key];
}
});
return result;
}
async tableExists(tableName) {
try {
const { error } = await this.client.from(tableName).select('*').limit(1);
return !error;
}
catch {
return false;
}
}
async columnExists(tableName, columnName) {
try {
const { error } = await this.client.from(tableName).select(columnName).limit(1);
return !error;
}
catch {
return false;
}
}
async getTableColumns(tableName) {
try {
// This would need to be implemented based on available database introspection
// For now, return a basic set of columns
return ['id', 'created_at', 'updated_at'];
}
catch {
return [];
}
}
}
exports.CustomRelationshipManager = CustomRelationshipManager;
//# sourceMappingURL=custom-relationship-manager.js.map