supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
582 lines • 22.4 kB
JavaScript
;
/**
* Constraint-Aware Validation System
* Validates operations against actual database constraints before execution
* Prevents the constraint violations identified in beta testing
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConstraintValidator = void 0;
const logger_1 = require("../../core/utils/logger");
class ConstraintValidator {
constructor(client) {
this.validationRules = new Map();
this.schemaInfo = null;
this.client = client;
}
/**
* Initialize validator with schema information and build validation rules
*/
async initialize(schemaInfo) {
this.schemaInfo = schemaInfo;
logger_1.Logger.info('🔍 Initializing constraint validator...');
// Build validation rules from schema constraints
await this.buildValidationRules();
logger_1.Logger.success(`✅ Constraint validator initialized with ${this.getTotalRuleCount()} rules`);
}
/**
* Validate data against all applicable constraints before operation
*/
async validateOperation(context) {
const startTime = Date.now();
const result = {
valid: true,
errors: [],
warnings: [],
autoFixes: [],
constraintsChecked: 0,
executionTime: 0
};
if (!this.schemaInfo) {
throw new Error('Constraint validator not initialized. Call initialize() first.');
}
logger_1.Logger.debug(`Validating ${context.operation} operation on ${context.table}`);
// Get validation rules for this table
const tableRules = this.validationRules.get(context.table) || [];
// Execute validation rules
for (const rule of tableRules) {
try {
const ruleResult = await this.executeValidationRule(rule, context);
result.constraintsChecked++;
if (!ruleResult.valid) {
if (rule.severity === 'error') {
result.errors.push({
rule: rule.id,
field: rule.column,
message: ruleResult.message,
severity: 'error',
constraint: rule.name,
suggestedFix: ruleResult.suggestedFix,
blockExecution: true
});
result.valid = false;
}
else if (rule.severity === 'warning') {
result.warnings.push({
rule: rule.id,
field: rule.column,
message: ruleResult.message,
impact: 'medium', // Could be computed based on rule type
recommendation: ruleResult.suggestedFix || 'Review and fix manually'
});
}
// Check if auto-fix is possible
if (rule.autoFix?.possible && ruleResult.autoFix) {
result.autoFixes.push(ruleResult.autoFix);
}
}
}
catch (error) {
logger_1.Logger.warn(`Validation rule ${rule.id} failed to execute: ${error.message}`);
result.warnings.push({
rule: rule.id,
message: `Validation rule execution failed: ${error.message}`,
impact: 'low',
recommendation: 'Check rule configuration'
});
}
}
result.executionTime = Date.now() - startTime;
logger_1.Logger.debug(`Validation completed: ${result.valid ? 'PASSED' : 'FAILED'} (${result.constraintsChecked} rules in ${result.executionTime}ms)`);
if (!result.valid) {
logger_1.Logger.warn(`Validation failed with ${result.errors.length} errors and ${result.warnings.length} warnings`);
}
return result;
}
/**
* Apply auto-fixes to data before operation
*/
async applyAutoFixes(data, autoFixes) {
const modifiedData = { ...data };
const appliedFixes = [];
for (const fix of autoFixes) {
try {
switch (fix.strategy) {
case 'provide_default':
if (modifiedData[fix.field] === undefined || modifiedData[fix.field] === null) {
modifiedData[fix.field] = fix.fixedValue;
fix.applied = true;
appliedFixes.push(fix);
logger_1.Logger.debug(`Applied auto-fix: Set ${fix.field} = ${fix.fixedValue}`);
}
break;
case 'modify_value':
modifiedData[fix.field] = fix.fixedValue;
fix.applied = true;
appliedFixes.push(fix);
logger_1.Logger.debug(`Applied auto-fix: Modified ${fix.field} from ${fix.originalValue} to ${fix.fixedValue}`);
break;
case 'skip_field':
delete modifiedData[fix.field];
fix.applied = true;
appliedFixes.push(fix);
logger_1.Logger.debug(`Applied auto-fix: Removed ${fix.field} from data`);
break;
case 'create_dependency':
// This would create required related records
logger_1.Logger.debug(`Auto-fix requires dependency creation: ${fix.rule}`);
break;
}
}
catch (error) {
logger_1.Logger.warn(`Failed to apply auto-fix for ${fix.field}: ${error.message}`);
}
}
return { data: modifiedData, appliedFixes };
}
/**
* Build validation rules from schema constraints
*/
async buildValidationRules() {
if (!this.schemaInfo)
return;
for (const table of this.schemaInfo.tables) {
const rules = [];
// Build rules from database constraints
rules.push(...await this.buildConstraintRules(table));
// Build rules from business logic constraints
rules.push(...await this.buildBusinessLogicRules(table));
// Build rules from schema patterns
rules.push(...await this.buildPatternRules(table));
this.validationRules.set(table.name, rules);
logger_1.Logger.debug(`Built ${rules.length} validation rules for table ${table.name}`);
}
}
/**
* Build validation rules from database constraints
*/
async buildConstraintRules(table) {
const rules = [];
for (const constraint of table.constraints) {
switch (constraint.type) {
case 'NOT NULL':
rules.push(...this.buildNotNullRules(table, constraint));
break;
case 'UNIQUE':
rules.push(...this.buildUniqueRules(table, constraint));
break;
case 'FOREIGN KEY':
rules.push(...this.buildForeignKeyRules(table, constraint));
break;
case 'CHECK':
rules.push(...this.buildCheckConstraintRules(table, constraint));
break;
}
}
return rules;
}
/**
* Build NOT NULL constraint rules
*/
buildNotNullRules(table, constraint) {
const rules = [];
for (const columnName of constraint.columns) {
const column = table.columns.find(c => c.name === columnName);
if (column && !column.isNullable) {
rules.push({
id: `${table.name}_${columnName}_not_null`,
name: `${columnName} NOT NULL`,
description: `Column ${columnName} cannot be null`,
table: table.name,
column: columnName,
type: 'not_null',
severity: 'error',
sqlCheck: `${columnName} IS NOT NULL`,
parameters: [columnName],
errorMessage: `${columnName} is required and cannot be null`,
autoFix: {
possible: true,
strategy: 'provide_default',
fixValue: this.getDefaultValueForColumn(column)
}
});
}
}
return rules;
}
/**
* Build UNIQUE constraint rules
*/
buildUniqueRules(table, constraint) {
const rules = [];
rules.push({
id: `${table.name}_${constraint.name}_unique`,
name: constraint.name,
description: `Unique constraint on ${constraint.columns.join(', ')}`,
table: table.name,
type: 'unique',
severity: 'error',
sqlCheck: `SELECT COUNT(*) FROM ${table.name} WHERE ${constraint.columns.map(col => `${col} = $${col}`).join(' AND ')}`,
parameters: constraint.columns,
errorMessage: `Duplicate value found for unique constraint: ${constraint.columns.join(', ')}`,
autoFix: {
possible: false,
strategy: 'modify_value'
}
});
return rules;
}
/**
* Build FOREIGN KEY constraint rules
*/
buildForeignKeyRules(table, constraint) {
const rules = [];
if (constraint.referencedTable && constraint.referencedColumns) {
rules.push({
id: `${table.name}_${constraint.name}_fk`,
name: constraint.name,
description: `Foreign key constraint referencing ${constraint.referencedTable}`,
table: table.name,
column: constraint.columns[0],
type: 'foreign_key',
severity: 'error',
sqlCheck: `SELECT COUNT(*) FROM ${constraint.referencedTable} WHERE ${constraint.referencedColumns[0]} = $${constraint.columns[0]}`,
parameters: constraint.columns,
errorMessage: `Referenced record not found in ${constraint.referencedTable}`,
autoFix: {
possible: true,
strategy: 'create_dependency'
}
});
}
return rules;
}
/**
* Build CHECK constraint rules
*/
buildCheckConstraintRules(table, constraint) {
const rules = [];
if (constraint.checkDefinition) {
rules.push({
id: `${table.name}_${constraint.name}_check`,
name: constraint.name,
description: `Check constraint: ${constraint.checkDefinition}`,
table: table.name,
type: 'check',
severity: 'error',
sqlCheck: constraint.checkDefinition,
parameters: [],
errorMessage: `Check constraint violation: ${constraint.checkDefinition}`,
autoFix: {
possible: false,
strategy: 'modify_value'
}
});
}
return rules;
}
/**
* Build business logic constraint rules
*/
async buildBusinessLogicRules(table) {
const rules = [];
if (!this.schemaInfo)
return rules;
// Find business logic constraints for this table
const businessConstraints = this.schemaInfo.constraints.businessLogicConstraints.filter(c => c.table === table.name);
for (const constraint of businessConstraints) {
rules.push({
id: `${table.name}_${constraint.rule}_business`,
name: constraint.rule,
description: constraint.description,
table: table.name,
type: 'business_logic',
severity: constraint.type === 'business_rule' ? 'warning' : 'error',
sqlCheck: constraint.sqlCondition,
parameters: [],
errorMessage: `Business logic constraint violation: ${constraint.description}`,
autoFix: {
possible: false,
strategy: 'skip_field'
}
});
}
return rules;
}
/**
* Build pattern-based validation rules
*/
async buildPatternRules(table) {
const rules = [];
if (!this.schemaInfo)
return rules;
// Find table pattern
const pattern = this.schemaInfo.patterns.find(p => p.name === table.name);
if (!pattern)
return rules;
// Add pattern-specific rules
if (pattern.suggestedRole === 'user') {
// Add user table specific rules
rules.push(...this.buildUserTableRules(table, pattern));
}
return rules;
}
/**
* Build user table specific rules
*/
buildUserTableRules(table, pattern) {
const rules = [];
// Email validation rule
if (pattern.columnMappings.email && pattern.columnMappings.email.length > 0) {
const emailColumn = pattern.columnMappings.email[0];
rules.push({
id: `${table.name}_${emailColumn}_email_format`,
name: 'Email Format',
description: 'Email must be in valid format',
table: table.name,
column: emailColumn,
type: 'custom',
severity: 'warning',
sqlCheck: `${emailColumn} ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'`,
parameters: [emailColumn],
errorMessage: 'Invalid email format',
autoFix: {
possible: false,
strategy: 'modify_value'
}
});
}
return rules;
}
/**
* Execute a single validation rule
*/
async executeValidationRule(rule, context) {
try {
switch (rule.type) {
case 'not_null':
return await this.validateNotNull(rule, context);
case 'unique':
return await this.validateUnique(rule, context);
case 'foreign_key':
return await this.validateForeignKey(rule, context);
case 'check':
return await this.validateCheckConstraint(rule, context);
case 'business_logic':
return await this.validateBusinessLogic(rule, context);
case 'custom':
return await this.validateCustomRule(rule, context);
default:
return { valid: true, message: 'Unknown rule type' };
}
}
catch (error) {
return {
valid: false,
message: `Rule execution failed: ${error.message}`
};
}
}
/**
* Validate NOT NULL constraint
*/
async validateNotNull(rule, context) {
if (!rule.column) {
return { valid: true, message: 'No column specified' };
}
const value = context.data[rule.column];
const isNull = value === null || value === undefined || value === '';
if (isNull) {
const autoFix = {
rule: rule.id,
field: rule.column,
originalValue: value,
fixedValue: rule.autoFix?.fixValue,
strategy: 'provide_default',
applied: false
};
return {
valid: false,
message: rule.errorMessage,
suggestedFix: `Provide a value for ${rule.column}`,
autoFix
};
}
return { valid: true, message: 'Value is not null' };
}
/**
* Validate UNIQUE constraint
*/
async validateUnique(rule, context) {
// Build WHERE conditions for unique check
const conditions = {};
for (const param of rule.parameters) {
conditions[param] = context.data[param];
}
try {
const { data, error } = await this.client
.from(context.table)
.select('id')
.match(conditions)
.limit(1);
if (error) {
return {
valid: false,
message: `Unique constraint check failed: ${error.message}`
};
}
if (data && data.length > 0) {
// Check if this is the same record (for updates)
if (context.operation === 'update' && context.existingData?.id === data[0].id) {
return { valid: true, message: 'Unique constraint satisfied (same record)' };
}
return {
valid: false,
message: rule.errorMessage,
suggestedFix: `Modify ${rule.parameters.join(', ')} to ensure uniqueness`
};
}
return { valid: true, message: 'Unique constraint satisfied' };
}
catch (error) {
return {
valid: false,
message: `Unique constraint validation error: ${error.message}`
};
}
}
/**
* Validate FOREIGN KEY constraint
*/
async validateForeignKey(rule, context) {
if (!rule.column) {
return { valid: true, message: 'No column specified' };
}
const foreignKeyValue = context.data[rule.column];
// If foreign key is null and column is nullable, it's valid
if (foreignKeyValue === null || foreignKeyValue === undefined) {
const column = this.schemaInfo?.tables
.find(t => t.name === context.table)
?.columns.find(c => c.name === rule.column);
if (column?.isNullable) {
return { valid: true, message: 'Foreign key is null but nullable' };
}
}
// Extract referenced table from rule description or SQL
const referencedTableMatch = rule.description.match(/referencing (\w+)/);
if (!referencedTableMatch) {
return { valid: false, message: 'Cannot determine referenced table' };
}
const referencedTable = referencedTableMatch[1];
try {
const { data, error } = await this.client
.from(referencedTable)
.select('id')
.eq('id', foreignKeyValue)
.limit(1);
if (error) {
return {
valid: false,
message: `Foreign key constraint check failed: ${error.message}`
};
}
if (!data || data.length === 0) {
return {
valid: false,
message: rule.errorMessage,
suggestedFix: `Create referenced record in ${referencedTable} or use valid foreign key value`
};
}
return { valid: true, message: 'Foreign key constraint satisfied' };
}
catch (error) {
return {
valid: false,
message: `Foreign key validation error: ${error.message}`
};
}
}
/**
* Validate CHECK constraint
*/
async validateCheckConstraint(rule, context) {
// For check constraints, we'd need to evaluate the SQL condition
// This is a simplified implementation
logger_1.Logger.debug(`Validating check constraint: ${rule.sqlCheck}`);
// For now, assume check constraints pass
// Full implementation would safely evaluate the SQL condition
return { valid: true, message: 'Check constraint assumed valid' };
}
/**
* Validate business logic constraint
*/
async validateBusinessLogic(rule, context) {
// Business logic validation would be specific to each rule
logger_1.Logger.debug(`Validating business logic: ${rule.description}`);
// For now, assume business logic passes with warning
return {
valid: true,
message: 'Business logic constraint assumed valid'
};
}
/**
* Validate custom rule
*/
async validateCustomRule(rule, context) {
// Custom rule validation would be implemented based on specific rule type
logger_1.Logger.debug(`Validating custom rule: ${rule.name}`);
return { valid: true, message: 'Custom rule assumed valid' };
}
/**
* Utility methods
*/
getDefaultValueForColumn(column) {
if (column.defaultValue) {
return column.defaultValue;
}
switch (column.type) {
case 'uuid':
return crypto.randomUUID();
case 'timestamp':
case 'timestamptz':
return new Date().toISOString();
case 'boolean':
return false;
case 'integer':
case 'bigint':
return 0;
case 'text':
case 'varchar':
return '';
default:
return null;
}
}
getTotalRuleCount() {
let total = 0;
for (const rules of this.validationRules.values()) {
total += rules.length;
}
return total;
}
/**
* Get validation rules for a specific table
*/
getTableRules(tableName) {
return this.validationRules.get(tableName) || [];
}
/**
* Get all validation rules
*/
getAllRules() {
return new Map(this.validationRules);
}
/**
* Clear validation cache
*/
clearCache() {
this.validationRules.clear();
this.schemaInfo = null;
}
}
exports.ConstraintValidator = ConstraintValidator;
//# sourceMappingURL=constraint-validator.js.map