supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
446 lines • 17.9 kB
JavaScript
;
/**
* Schema-Driven Workflow Builder
* Builds dynamic workflows based on actual database schema instead of hardcoded assumptions
* Replaces framework-specific business logic with configurable execution patterns
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowBuilder = void 0;
const logger_1 = require("../../core/utils/logger");
class WorkflowBuilder {
constructor(introspector) {
this.schemaResult = null;
this.introspector = introspector;
}
/**
* Build a complete user creation workflow based on actual schema
*/
async buildUserCreationWorkflow(config) {
logger_1.Logger.info('🔨 Building schema-driven user creation workflow...');
// Get fresh schema introspection
this.schemaResult = await this.introspector.introspectSchema();
if (!this.schemaResult) {
throw new Error('Schema introspection failed - cannot build workflow');
}
// Find the primary user table
const userTable = this.findUserTable(config.primaryUserTable);
if (!userTable) {
throw new Error(`Primary user table '${config.primaryUserTable}' not found in schema`);
}
// Build workflow steps
const steps = await this.buildWorkflowSteps(userTable, config);
const rollbackSteps = config.enableRollback ? await this.buildRollbackSteps(steps) : [];
const workflow = {
id: `user_creation_${Date.now()}`,
name: `Dynamic User Creation - ${userTable.name}`,
description: `Schema-driven workflow for creating users in ${userTable.name} table`,
steps,
rollbackSteps,
metadata: {
framework: this.schemaResult.framework.type,
version: this.schemaResult.framework.version,
createdAt: new Date().toISOString(),
schemaHash: this.generateSchemaHash(this.schemaResult)
}
};
logger_1.Logger.success(`✅ Built workflow with ${steps.length} steps`);
return workflow;
}
/**
* Find the appropriate user table pattern
*/
findUserTable(preferredTableName) {
if (!this.schemaResult)
return null;
// First try to find the exact table name
let userTable = this.schemaResult.patterns.find(p => p.name === preferredTableName && p.suggestedRole === 'user');
// If not found, find any user table
if (!userTable) {
userTable = this.schemaResult.patterns.find(p => p.suggestedRole === 'user');
}
// If still not found, check if the preferred table exists but wasn't classified as user
if (!userTable) {
const tableExists = this.schemaResult.tables.find(t => t.name === preferredTableName);
if (tableExists) {
// Create a basic pattern for the table
return {
name: preferredTableName,
confidence: 0.5,
evidence: ['Specified as primary user table in config'],
suggestedRole: 'user',
columnMappings: this.generateBasicColumnMappings(tableExists.columns.map(c => c.name))
};
}
}
return userTable || null;
}
/**
* Build the complete workflow steps
*/
async buildWorkflowSteps(userTable, config) {
const steps = [];
// Step 1: Always create auth user first (Supabase requirement)
steps.push(await this.buildAuthUserStep());
// Step 2: Validate constraints before user table insertion
if (config.enableConstraintValidation) {
const constraintSteps = await this.buildConstraintValidationSteps(userTable);
steps.push(...constraintSteps);
}
// Step 3: Create the main user record
const userRecordStep = await this.buildUserRecordStep(userTable, config);
steps.push(userRecordStep);
// Step 4: Handle related tables (accounts, profiles, etc.)
const relatedSteps = await this.buildRelatedTableSteps(userTable, config);
steps.push(...relatedSteps);
// Step 5: Execute any post-creation triggers or business logic
const postCreationSteps = await this.buildPostCreationSteps(userTable);
steps.push(...postCreationSteps);
return steps;
}
/**
* Build auth user creation step
*/
async buildAuthUserStep() {
return {
id: 'create_auth_user',
type: 'create_auth_user',
description: 'Create Supabase auth user',
dependencies: [],
fields: [
{
name: 'id',
source: 'generated',
generator: 'uuid',
required: true,
validation: [{ type: 'not_null', errorMessage: 'User ID is required' }]
},
{
name: 'email',
source: 'input',
required: true,
validation: [
{ type: 'not_null', errorMessage: 'Email is required' },
{ type: 'unique', errorMessage: 'Email must be unique' }
]
},
{
name: 'password',
source: 'input',
required: true,
validation: [{ type: 'not_null', errorMessage: 'Password is required' }]
},
{
name: 'email_confirm',
source: 'generated',
required: false
}
],
onError: 'fail'
};
}
/**
* Build constraint validation steps
*/
async buildConstraintValidationSteps(userTable) {
const steps = [];
if (!this.schemaResult)
return steps;
// Find constraints that affect the user table
const relevantConstraints = this.schemaResult.constraints.userCreationConstraints.filter(c => c.table === userTable.name);
for (const constraint of relevantConstraints) {
if (constraint.requiresValidation) {
steps.push({
id: `validate_${constraint.rule}`,
type: 'validate_constraint',
table: constraint.table,
description: `Validate constraint: ${constraint.description}`,
dependencies: ['create_auth_user'],
fields: [],
conditions: [{
type: 'custom_sql',
sqlCondition: constraint.sqlCondition,
description: constraint.description
}],
onError: 'fail'
});
}
}
return steps;
}
/**
* Build the main user record creation step
*/
async buildUserRecordStep(userTable, config) {
const tableInfo = this.schemaResult?.tables.find(t => t.name === userTable.name);
if (!tableInfo) {
throw new Error(`Table info not found for ${userTable.name}`);
}
const fields = [];
// Add primary key field
const pkColumn = tableInfo.columns.find(c => c.isPrimaryKey);
if (pkColumn) {
fields.push({
name: pkColumn.name,
source: 'reference',
sourceField: 'id',
required: true,
validation: [{ type: 'not_null', errorMessage: 'Primary key is required' }]
});
}
// Add mapped fields based on column mappings
for (const [semanticField, columnOptions] of Object.entries(userTable.columnMappings)) {
const actualColumn = columnOptions[0]; // Use first available column
if (actualColumn) {
const columnInfo = tableInfo.columns.find(c => c.name === actualColumn);
if (columnInfo) {
fields.push(this.buildFieldFromColumn(semanticField, columnInfo, config));
}
}
}
// Add required columns that weren't mapped
for (const column of tableInfo.columns) {
if (!column.isNullable && !column.isPrimaryKey && !fields.find(f => f.name === column.name)) {
fields.push(this.buildFieldFromColumn(column.name, column, config));
}
}
return {
id: `create_user_record`,
type: 'insert_record',
table: userTable.name,
description: `Create user record in ${userTable.name}`,
dependencies: ['create_auth_user'],
fields,
onError: 'fail'
};
}
/**
* Build a workflow field from a database column
*/
buildFieldFromColumn(semanticField, column, config) {
const field = {
name: column.name,
source: 'input',
required: !column.isNullable,
validation: []
};
// Add validations based on column constraints
if (!column.isNullable) {
field.validation.push({ type: 'not_null', errorMessage: `${column.name} is required` });
}
// Set appropriate source and generator based on semantic meaning
switch (semanticField) {
case 'email':
field.generator = 'faker';
field.fakerType = 'internet.email';
field.validation.push({ type: 'unique', errorMessage: 'Email must be unique' });
break;
case 'name':
case 'display_name':
case 'full_name':
field.generator = 'faker';
field.fakerType = 'person.fullName';
break;
case 'username':
field.generator = 'faker';
field.fakerType = 'internet.userName';
field.validation.push({ type: 'unique', errorMessage: 'Username must be unique' });
break;
case 'avatar':
case 'picture_url':
case 'avatar_url':
field.source = 'generated';
field.generator = 'faker';
field.fakerType = 'image.avatar';
field.required = false;
break;
case 'bio':
case 'about':
field.generator = 'faker';
field.fakerType = 'lorem.paragraph';
field.required = false;
break;
case 'created_at':
case 'updated_at':
field.source = 'generated';
field.generator = 'timestamp';
break;
}
return field;
}
/**
* Build steps for related tables
*/
async buildRelatedTableSteps(userTable, config) {
const steps = [];
if (!this.schemaResult)
return steps;
// Find tables that have foreign keys to the user table
const relatedTables = this.schemaResult.relationships.filter(rel => rel.toTable === userTable.name);
for (const relationship of relatedTables) {
// Check if this is a required relationship
if (relationship.isRequired) {
const relatedTable = this.schemaResult.patterns.find(p => p.name === relationship.fromTable);
if (relatedTable) {
const step = await this.buildRelatedTableStep(relatedTable, relationship, userTable);
if (step) {
steps.push(step);
}
}
}
}
return steps;
}
/**
* Build a step for a related table
*/
async buildRelatedTableStep(relatedTable, relationship, userTable) {
const tableInfo = this.schemaResult?.tables.find(t => t.name === relatedTable.name);
if (!tableInfo)
return null;
// Only create steps for tables we understand
if (relatedTable.suggestedRole === 'system')
return null;
const fields = [];
// Add the foreign key field
fields.push({
name: relationship.fromColumn,
source: 'reference',
sourceField: 'id',
required: true,
validation: [
{ type: 'not_null', errorMessage: 'User reference is required' },
{ type: 'foreign_key', errorMessage: 'Must reference valid user' }
]
});
// Add other required fields for this table
for (const column of tableInfo.columns) {
if (!column.isNullable && !column.isPrimaryKey && column.name !== relationship.fromColumn) {
fields.push(this.buildFieldFromColumn(column.name, column, {}));
}
}
return {
id: `create_${relatedTable.name}_record`,
type: 'insert_record',
table: relatedTable.name,
description: `Create related record in ${relatedTable.name}`,
dependencies: ['create_user_record'],
fields,
conditions: [{
type: 'relationship_exists',
description: `Ensure ${userTable.name} record exists`
}],
onError: 'skip' // Related tables are usually optional
};
}
/**
* Build post-creation steps
*/
async buildPostCreationSteps(userTable) {
const steps = [];
if (!this.schemaResult)
return steps;
// Find triggers that might need to be executed
const tableInfo = this.schemaResult.tables.find(t => t.name === userTable.name);
if (tableInfo && tableInfo.triggers.length > 0) {
for (const trigger of tableInfo.triggers) {
if (trigger.events.includes('INSERT')) {
steps.push({
id: `execute_trigger_${trigger.name}`,
type: 'execute_trigger',
table: userTable.name,
description: `Execute trigger: ${trigger.name}`,
dependencies: ['create_user_record'],
fields: [],
onError: 'continue' // Triggers should not fail the entire process
});
}
}
}
return steps;
}
/**
* Build rollback steps
*/
async buildRollbackSteps(forwardSteps) {
const rollbackSteps = [];
// Create rollback steps in reverse order
for (let i = forwardSteps.length - 1; i >= 0; i--) {
const step = forwardSteps[i];
if (step.type === 'insert_record' && step.table) {
rollbackSteps.push({
id: `rollback_${step.id}`,
type: 'conditional_action',
table: step.table,
description: `Rollback: Delete record from ${step.table}`,
dependencies: [],
fields: [],
conditions: [{
type: 'field_exists',
field: 'id',
description: 'Only delete if record was created'
}],
onError: 'continue' // Continue rollback even if individual steps fail
});
}
}
return rollbackSteps;
}
/**
* Utility methods
*/
generateBasicColumnMappings(columnNames) {
const mappings = {};
// Common mappings
const commonMappings = {
email: ['email', 'email_address', 'user_email'],
name: ['name', 'display_name', 'full_name', 'username'],
avatar: ['avatar_url', 'picture_url', 'profile_image_url', 'image_url'],
bio: ['bio', 'about', 'description']
};
for (const [key, variants] of Object.entries(commonMappings)) {
const found = variants.filter(variant => columnNames.includes(variant));
if (found.length > 0) {
mappings[key] = found;
}
}
return mappings;
}
generateSchemaHash(schema) {
// Generate a hash of the schema structure for change detection
const schemaString = JSON.stringify({
tables: schema.tables.map(t => ({ name: t.name, columns: t.columns.length })),
relationships: schema.relationships.length,
framework: schema.framework
});
return btoa(schemaString).slice(0, 16); // Simple hash for demo
}
/**
* Validate a workflow against the current schema
*/
async validateWorkflow(workflow) {
const errors = [];
const warnings = [];
// Check if schema has changed
const currentSchema = await this.introspector.introspectSchema();
const currentHash = this.generateSchemaHash(currentSchema);
if (workflow.metadata.schemaHash !== currentHash) {
warnings.push('Schema has changed since workflow was created. Consider regenerating.');
}
// Validate each step
for (const step of workflow.steps) {
if (step.table) {
const tableExists = currentSchema.tables.find(t => t.name === step.table);
if (!tableExists) {
errors.push(`Table ${step.table} referenced in step ${step.id} does not exist`);
}
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
}
exports.WorkflowBuilder = WorkflowBuilder;
//# sourceMappingURL=workflow-builder.js.map