supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
494 lines • 19.5 kB
JavaScript
;
/**
* Configurable Workflow Execution Engine
* Executes dynamically generated workflows with constraint validation and error handling
* Replaces hardcoded business logic with configurable execution patterns
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowExecutor = void 0;
const faker_1 = require("@faker-js/faker");
const logger_1 = require("../../core/utils/logger");
class WorkflowExecutor {
constructor(client, config) {
this.client = client;
this.config = config;
}
/**
* Execute a complete user creation workflow
*/
async executeWorkflow(workflow, userData) {
const executionId = crypto.randomUUID();
const startTime = new Date();
logger_1.Logger.info(`🚀 Executing workflow: ${workflow.name} (${executionId})`);
const context = {
client: this.client,
userData,
stepResults: new Map(),
rollbackData: new Map(),
executionId,
startTime
};
const result = {
success: false,
executionId,
duration: 0,
completedSteps: [],
rollbackRequired: false,
rollbackCompleted: false,
stepResults: {}
};
try {
// Execute workflow steps in dependency order
const executionOrder = this.calculateExecutionOrder(workflow.steps);
for (const stepId of executionOrder) {
const step = workflow.steps.find(s => s.id === stepId);
if (!step) {
throw new Error(`Step ${stepId} not found in workflow`);
}
logger_1.Logger.debug(`Executing step: ${step.description}`);
const stepResult = await this.executeStep(step, context);
if (!stepResult.success) {
result.failedStep = step.id;
result.error = stepResult.error;
if (step.onError === 'fail') {
result.rollbackRequired = this.config.enableRollback;
break;
}
else if (step.onError === 'skip') {
logger_1.Logger.warn(`Skipping failed step: ${step.description}`);
continue;
}
else if (step.onError === 'retry' && step.retryCount) {
// Implement retry logic
const retryResult = await this.retryStep(step, context, step.retryCount);
if (!retryResult.success) {
result.failedStep = step.id;
result.error = retryResult.error;
break;
}
stepResult.success = true;
stepResult.data = retryResult.data;
}
}
// Store step result
context.stepResults.set(step.id, stepResult.data);
result.completedSteps.push(step.id);
// Special handling for auth user creation
if (step.type === 'create_auth_user' && stepResult.data?.userId) {
context.userId = stepResult.data.userId;
result.userId = stepResult.data.userId;
}
}
// If we completed all steps, mark as successful
if (result.completedSteps.length === workflow.steps.length) {
result.success = true;
logger_1.Logger.success(`✅ Workflow completed successfully: ${workflow.name}`);
}
}
catch (error) {
logger_1.Logger.error(`Workflow execution failed: ${error.message}`);
result.error = error.message;
result.rollbackRequired = this.config.enableRollback;
}
// Execute rollback if needed
if (result.rollbackRequired && workflow.rollbackSteps.length > 0) {
logger_1.Logger.info('🔄 Executing rollback steps...');
result.rollbackCompleted = await this.executeRollback(workflow.rollbackSteps, context);
}
// Calculate execution duration
result.duration = Date.now() - startTime.getTime();
// Convert step results to plain object
result.stepResults = Object.fromEntries(context.stepResults);
logger_1.Logger.info(`Workflow execution completed in ${result.duration}ms`);
return result;
}
/**
* Execute a single workflow step
*/
async executeStep(step, context) {
try {
// Check step conditions first
if (step.conditions && step.conditions.length > 0) {
const conditionsValid = await this.validateConditions(step.conditions, context);
if (!conditionsValid) {
return {
success: false,
error: `Step conditions not met: ${step.description}`
};
}
}
// Execute based on step type
switch (step.type) {
case 'create_auth_user':
return await this.executeCreateAuthUser(step, context);
case 'insert_record':
return await this.executeInsertRecord(step, context);
case 'validate_constraint':
return await this.executeValidateConstraint(step, context);
case 'execute_trigger':
return await this.executeExecuteTrigger(step, context);
case 'conditional_action':
return await this.executeConditionalAction(step, context);
default:
return {
success: false,
error: `Unknown step type: ${step.type}`
};
}
}
catch (error) {
logger_1.Logger.error(`Step execution failed: ${step.description}`, error);
return {
success: false,
error: error.message
};
}
}
/**
* Execute auth user creation step
*/
async executeCreateAuthUser(step, context) {
const fieldData = await this.resolveStepFields(step.fields, context);
try {
const { data, error } = await context.client.auth.admin.createUser({
id: fieldData.id,
email: fieldData.email,
password: fieldData.password || 'password123',
email_confirm: fieldData.email_confirm !== false,
user_metadata: {
full_name: context.userData.name,
username: context.userData.username,
...context.userData.metadata
}
});
if (error) {
return { success: false, error: `Auth user creation failed: ${error.message}` };
}
if (!data.user) {
return { success: false, error: 'Auth user creation returned no user data' };
}
logger_1.Logger.debug(`Created auth user: ${fieldData.email}`);
return {
success: true,
data: {
userId: data.user.id,
email: data.user.email,
authUserData: data.user
}
};
}
catch (error) {
return { success: false, error: `Auth user creation failed: ${error.message}` };
}
}
/**
* Execute record insertion step
*/
async executeInsertRecord(step, context) {
if (!step.table) {
return { success: false, error: 'No table specified for insert operation' };
}
const fieldData = await this.resolveStepFields(step.fields, context);
// Validate required fields
for (const field of step.fields) {
if (field.required && (fieldData[field.name] === undefined || fieldData[field.name] === null)) {
return {
success: false,
error: `Required field missing: ${field.name}`
};
}
}
try {
const { data, error } = await context.client
.from(step.table)
.insert(fieldData)
.select()
.single();
if (error) {
return {
success: false,
error: `Insert into ${step.table} failed: ${error.message}`
};
}
logger_1.Logger.debug(`Inserted record into ${step.table}`);
// Store rollback data
if (this.config.enableRollback && data) {
context.rollbackData.set(step.id, {
table: step.table,
id: data.id,
operation: 'delete'
});
}
return { success: true, data };
}
catch (error) {
return {
success: false,
error: `Insert into ${step.table} failed: ${error.message}`
};
}
}
/**
* Execute constraint validation step
*/
async executeValidateConstraint(step, context) {
if (!this.config.enableConstraintValidation) {
return { success: true, data: { skipped: 'Constraint validation disabled' } };
}
// Execute custom SQL conditions
for (const condition of step.conditions || []) {
if (condition.type === 'custom_sql' && condition.sqlCondition) {
try {
// For now, we'll implement basic constraint validation
// Full implementation would use actual SQL constraint checking
logger_1.Logger.debug(`Validating constraint: ${condition.description}`);
// This is a placeholder - real implementation would execute the SQL condition
const valid = await this.validateSqlCondition(condition.sqlCondition, context);
if (!valid) {
return {
success: false,
error: `Constraint validation failed: ${condition.description}`
};
}
}
catch (error) {
return {
success: false,
error: `Constraint validation error: ${error.message}`
};
}
}
}
return { success: true, data: { validated: true } };
}
/**
* Execute trigger execution step
*/
async executeExecuteTrigger(step, context) {
// Triggers are typically executed automatically by the database
// This step is mainly for validation that triggers executed correctly
logger_1.Logger.debug(`Trigger execution step: ${step.description}`);
// For now, just return success - full implementation would verify trigger execution
return { success: true, data: { triggerExecuted: true } };
}
/**
* Execute conditional action step
*/
async executeConditionalAction(step, context) {
// Check if conditions are met
if (step.conditions) {
const conditionsValid = await this.validateConditions(step.conditions, context);
if (!conditionsValid) {
return { success: true, data: { skipped: 'Conditions not met' } };
}
}
// Execute the conditional action (implementation depends on specific action)
logger_1.Logger.debug(`Conditional action: ${step.description}`);
return { success: true, data: { actionExecuted: true } };
}
/**
* Resolve field values for a step
*/
async resolveStepFields(fields, context) {
const resolvedData = {};
for (const field of fields) {
let value;
switch (field.source) {
case 'input':
value = context.userData[field.name];
break;
case 'reference':
if (field.sourceField) {
// Look up reference value from previous step or context
if (field.sourceField === 'id' && context.userId) {
value = context.userId;
}
else {
value = context.userData[field.sourceField];
}
}
break;
case 'generated':
value = await this.generateFieldValue(field);
break;
case 'computed':
value = await this.computeFieldValue(field, context);
break;
default:
value = context.userData[field.name];
}
// Apply field validation
if (field.validation && field.validation.length > 0) {
const validationResult = await this.validateFieldValue(field, value, context);
if (!validationResult.valid) {
throw new Error(`Field validation failed for ${field.name}: ${validationResult.error}`);
}
}
resolvedData[field.name] = value;
}
return resolvedData;
}
/**
* Generate a field value based on the field configuration
*/
async generateFieldValue(field) {
switch (field.generator) {
case 'uuid':
return crypto.randomUUID();
case 'timestamp':
return new Date().toISOString();
case 'sequence':
// Implementation would maintain sequence counters
return Date.now(); // Simple fallback
case 'faker':
if (field.fakerType) {
return this.generateFakerValue(field.fakerType);
}
return null;
default:
return null;
}
}
/**
* Generate faker value based on type
*/
generateFakerValue(fakerType) {
const parts = fakerType.split('.');
if (parts.length !== 2)
return null;
const [category, method] = parts;
try {
const fakerCategory = faker_1.faker[category];
if (fakerCategory && typeof fakerCategory[method] === 'function') {
return fakerCategory[method]();
}
}
catch (error) {
logger_1.Logger.warn(`Failed to generate faker value for ${fakerType}`);
}
return null;
}
/**
* Calculate execution order based on step dependencies
*/
calculateExecutionOrder(steps) {
const executed = new Set();
const order = [];
const maxIterations = steps.length * 2; // Prevent infinite loops
let iterations = 0;
while (order.length < steps.length && iterations < maxIterations) {
iterations++;
for (const step of steps) {
if (executed.has(step.id))
continue;
// Check if all dependencies are satisfied
const dependenciesSatisfied = step.dependencies.every(dep => executed.has(dep));
if (dependenciesSatisfied) {
order.push(step.id);
executed.add(step.id);
}
}
}
if (order.length < steps.length) {
throw new Error('Circular dependency detected in workflow steps');
}
return order;
}
/**
* Validate step conditions
*/
async validateConditions(conditions, context) {
for (const condition of conditions) {
const valid = await this.validateSingleCondition(condition, context);
if (!valid) {
return false;
}
}
return true;
}
/**
* Validate a single condition
*/
async validateSingleCondition(condition, context) {
switch (condition.type) {
case 'field_exists':
return condition.field ? (context.userData[condition.field] !== undefined) : false;
case 'constraint_check':
return await this.validateConstraintCheck(condition, context);
case 'relationship_exists':
return await this.validateRelationshipExists(condition, context);
case 'custom_sql':
return condition.sqlCondition ?
await this.validateSqlCondition(condition.sqlCondition, context) : false;
default:
return true;
}
}
/**
* Validate SQL condition (placeholder implementation)
*/
async validateSqlCondition(sqlCondition, context) {
// This is a placeholder implementation
// Real implementation would execute the SQL condition safely
logger_1.Logger.debug(`Validating SQL condition: ${sqlCondition}`);
return true;
}
/**
* Additional validation methods (placeholders)
*/
async validateConstraintCheck(condition, context) {
return true; // Placeholder
}
async validateRelationshipExists(condition, context) {
return true; // Placeholder
}
async validateFieldValue(field, value, context) {
// Placeholder implementation
return { valid: true };
}
async computeFieldValue(field, context) {
// Placeholder for computed field values
return null;
}
/**
* Retry a failed step
*/
async retryStep(step, context, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
logger_1.Logger.debug(`Retrying step ${step.id}, attempt ${attempt}/${maxRetries}`);
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
const result = await this.executeStep(step, context);
if (result.success) {
logger_1.Logger.debug(`Step ${step.id} succeeded on retry attempt ${attempt}`);
return result;
}
}
return {
success: false,
error: `Step failed after ${maxRetries} retry attempts`
};
}
/**
* Execute rollback steps
*/
async executeRollback(rollbackSteps, context) {
let rollbackSuccess = true;
for (const step of rollbackSteps) {
try {
const result = await this.executeStep(step, context);
if (!result.success) {
logger_1.Logger.warn(`Rollback step failed: ${step.description}`);
rollbackSuccess = false;
}
}
catch (error) {
logger_1.Logger.error(`Rollback step error: ${step.description}`, error);
rollbackSuccess = false;
}
}
return rollbackSuccess;
}
}
exports.WorkflowExecutor = WorkflowExecutor;
//# sourceMappingURL=workflow-executor.js.map