supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
632 lines โข 31.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserSeeder = void 0;
const types_1 = require("../../../core/types/types");
const schema_adapter_1 = require("../../../core/schema-adapter");
const domains_1 = require("../../../domains");
const makerkit_compatibility_1 = require("../../integration/makerkit-compatibility");
const crypto = __importStar(require("crypto"));
class UserSeeder extends types_1.SeedModule {
constructor() {
super(...arguments);
this.userCounter = 0; // Counter for truly unique user generation
}
async seed() {
try {
// Initialize schema adapter with config override
this.schemaAdapter = new schema_adapter_1.SchemaAdapter(this.context.client, this.context.config);
await this.schemaAdapter.detectSchema();
// Initialize MakerKit compatibility layer
await this.initializeMakerKitCompatibility();
const users = [];
let successfulUsers = 0;
let failedUsers = 0;
// NEW: Handle different user strategies (SUPASEED-001)
const userStrategy = this.context.config.userStrategy || 'create-new';
switch (userStrategy) {
case 'use-existing':
console.log('๐ Using existing users only...');
const existingUsers = await this.useExistingUsers();
users.push(...existingUsers);
successfulUsers = existingUsers.length;
console.log(`โ
Found ${existingUsers.length} existing users`);
break;
case 'hybrid':
console.log('๐ Using hybrid user strategy (existing + new)...');
const hybridResult = await this.hybridUserStrategy();
users.push(...hybridResult.allUsers);
successfulUsers = hybridResult.totalCreated;
console.log(`โ
Hybrid strategy: ${hybridResult.existing} existing + ${hybridResult.created} new = ${hybridResult.allUsers.length} total`);
break;
case 'create-new':
default:
// Original behavior - create standard test emails if enabled
if (this.context.config.createStandardTestEmails) {
try {
const standardUsers = await this.createEnhancedStandardTestUsers();
users.push(...standardUsers);
successfulUsers += standardUsers.length;
}
catch (error) {
console.log(`โ ๏ธ Standard test user creation failed: ${error.message}`);
console.log(' Continuing with regular user creation...');
failedUsers++;
}
}
// Create diverse generated users with error recovery
console.log(`๐ Creating ${this.context.config.userCount} users...`);
for (let i = 0; i < this.context.config.userCount; i++) {
try {
const user = await this.createUser();
if (user) {
users.push(user);
this.context.stats.usersCreated++;
successfulUsers++;
}
else {
failedUsers++;
}
}
catch (error) {
console.log(`โ ๏ธ User ${i + 1} creation failed: ${error.message}`);
failedUsers++;
// Continue with next user instead of failing completely
}
}
break;
}
// Report results
console.log(`โ
User processing complete: ${successfulUsers} successful, ${failedUsers} failed`);
// Cache users and compatibility info for other seeders (even if some failed)
this.context.cache.set('users', users);
this.context.cache.set('schemaAdapter', this.schemaAdapter);
this.context.cache.set('makerkitCompatibility', this.makerkitCompatibility);
// If no users were found/created at all, provide helpful guidance
if (users.length === 0) {
if (userStrategy === 'use-existing') {
console.log('๐จ No existing users found matching criteria. This may cause cascade failures.');
console.log('๐ก Check your existingUsers configuration:');
console.log(' 1. Table name and filter criteria');
console.log(' 2. Database permissions');
console.log(' 3. Existing data in the specified table');
}
else {
console.log('๐จ No users were created successfully. This may cause cascade failures.');
console.log('๐ก Check your schema configuration and column mappings.');
console.log(' Other seeders will be skipped to prevent errors.');
}
// Set a flag to indicate no users were available
this.context.cache.set('noUsersCreated', true);
}
}
catch (error) {
console.error('๐จ Critical error in user seeding:', error.message);
console.log('๐ก This may be due to schema incompatibility. Please check:');
console.log(' 1. Database connection');
console.log(' 2. Table permissions');
console.log(' 3. Column name mappings in config');
// Set empty cache to prevent cascade failures
this.context.cache.set('users', []);
this.context.cache.set('noUsersCreated', true);
throw error;
}
}
/**
* NEW: Use existing users only (SUPASEED-001)
* Queries the database for existing users based on configuration
*/
async useExistingUsers() {
const existingConfig = this.context.config.existingUsers || {};
const table = existingConfig.table || 'accounts';
const filter = existingConfig.filter || { is_personal_account: true };
const idField = existingConfig.idField || 'id';
console.log(`๐ Querying existing users from '${table}' table...`);
console.log(`๐ Filter criteria:`, filter);
try {
// Query existing users with the specified filter
const query = this.context.client.from(table).select('*');
// Apply filter criteria
Object.entries(filter).forEach(([key, value]) => {
query.eq(key, value);
});
const { data: existingUsers, error } = await query;
if (error) {
console.error(`โ Error querying existing users:`, error);
throw new Error(`Failed to query existing users from '${table}': ${error.message}`);
}
if (!existingUsers || existingUsers.length === 0) {
console.log(`โ ๏ธ No existing users found in '${table}' matching criteria`);
return [];
}
// Convert database users to CachedUser format
const cachedUsers = existingUsers.map((user) => {
// Try common field mappings for email, name, username
const email = user.email || user.email_address || user.user_email || `user-${user[idField]}@existing.test`;
const name = user.name || user.display_name || user.full_name || user.username || `User ${user[idField]}`;
const username = user.username || user.display_name || user.name || `user_${user[idField]}`;
return {
id: user[idField],
email,
username,
name,
};
});
console.log(`โ
Successfully loaded ${cachedUsers.length} existing users`);
// Log sample of found users for verification
if (cachedUsers.length > 0) {
console.log('๐ Sample existing users:');
cachedUsers.slice(0, 3).forEach(user => {
console.log(` โข ${user.email} (${user.name})`);
});
if (cachedUsers.length > 3) {
console.log(` ... and ${cachedUsers.length - 3} more`);
}
}
return cachedUsers;
}
catch (error) {
console.error(`โ Failed to load existing users:`, error.message);
// Provide helpful debugging information
if (error.message.includes('relation') && error.message.includes('does not exist')) {
console.log(`๐ก Table '${table}' does not exist. Please check your configuration.`);
}
else if (error.message.includes('permission denied')) {
console.log(`๐ก Permission denied accessing '${table}'. Check your service role key permissions.`);
}
else if (error.message.includes('column') && error.message.includes('does not exist')) {
console.log(`๐ก One of the filter columns doesn't exist in '${table}'. Check your filter configuration.`);
}
throw error;
}
}
/**
* NEW: Hybrid user strategy - combine existing users with newly created ones (SUPASEED-001)
*/
async hybridUserStrategy() {
console.log('๐ Starting hybrid user strategy...');
// Step 1: Load existing users
const existingUsers = await this.useExistingUsers();
console.log(`๐ Found ${existingUsers.length} existing users`);
// Step 2: Create additional users based on configuration
const additionalConfig = this.context.config.additionalUsers || {};
const additionalCount = additionalConfig.count || 7;
const personas = additionalConfig.personas || ['casual_user', 'expert_user', 'content_creator', 'admin_user', 'power_user'];
// NEW: Pre-check constraints to determine realistic user creation limits
const constraintLimits = await this.checkUserCreationLimits();
const safeAdditionalCount = Math.min(additionalCount, constraintLimits.maxAdditional);
if (safeAdditionalCount < additionalCount) {
console.log(`โ ๏ธ MakerKit constraint limits additional users to ${safeAdditionalCount} (requested: ${additionalCount})`);
console.log(` Existing personal accounts: ${constraintLimits.existingPersonalAccounts}`);
console.log(` Maximum allowed: ${constraintLimits.maxPersonalAccounts}`);
console.log(` Adapting to create ${safeAdditionalCount} users within constraints`);
}
else {
console.log(`๐ Creating ${additionalCount} additional users with personas...`);
}
const newUsers = [];
let createdCount = 0;
let failedCount = 0;
for (let i = 0; i < safeAdditionalCount; i++) {
try {
// Create user with persona-based profile
const persona = personas[i % personas.length];
const user = await this.createPersonaUser(persona, i);
if (user) {
newUsers.push(user);
createdCount++;
this.context.stats.usersCreated++;
}
else {
failedCount++;
}
}
catch (error) {
console.log(`โ ๏ธ Persona user ${i + 1} (${personas[i % personas.length]}) creation failed: ${error.message}`);
failedCount++;
}
}
// Enhanced logging with constraint information
if (safeAdditionalCount < additionalCount) {
const skippedCount = additionalCount - safeAdditionalCount;
console.log(`โ
Additional user creation: ${createdCount} successful, ${failedCount} failed, ${skippedCount} skipped (constraint limits)`);
console.log(`๐ก To create more users, consider using existing users or team accounts instead of personal accounts`);
}
else {
console.log(`โ
Additional user creation: ${createdCount} successful, ${failedCount} failed`);
}
// Step 3: Combine existing and new users
const allUsers = [...existingUsers, ...newUsers];
// Step 4: Log summary for user visibility
console.log('๐ Hybrid Strategy Summary:');
console.log(` โข Existing users: ${existingUsers.length}`);
console.log(` โข Newly created: ${createdCount}`);
console.log(` โข Total available: ${allUsers.length}`);
console.log(` โข Failed creations: ${failedCount}`);
return {
existing: existingUsers.length,
created: createdCount,
totalCreated: existingUsers.length + createdCount, // Total successful users
allUsers
};
}
/**
* NEW: Create a user with persona-based characteristics (SUPASEED-001)
*/
async createPersonaUser(persona, index) {
const { faker } = this.context;
const domainConfig = (0, domains_1.getDomainConfig)(this.context.config.domain);
// Generate persona-specific characteristics
const personaConfig = this.getPersonaConfig(persona);
// Generate realistic profile based on persona
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
// Create truly unique email to prevent conflicts (faker is seeded so names repeat)
this.userCounter++;
const uuid = crypto.randomUUID().replace(/-/g, '').substring(0, 12);
const timestamp = Date.now();
// Use timestamp + UUID instead of faker names to ensure absolute uniqueness
const username = `user_${this.userCounter}_${timestamp}_${uuid}`;
const email = `${username}@${this.context.config.emailDomain || 'supaseed-personas.test'}`;
const bio = this.generatePersonaBio(persona, personaConfig, domainConfig);
const name = `${firstName} ${lastName}`;
console.log(` ๐ญ Creating ${persona}: ${email}`);
try {
// Use schema adapter to create user with appropriate strategy
const result = await this.schemaAdapter.createUserForSchema({
email,
name,
username,
bio,
picture_url: this.generateProfileImage(firstName, lastName),
});
if (!result.success) {
console.log(` โ ๏ธ ${persona} user creation failed: ${result.error}`);
return null;
}
console.log(` โ
Created ${persona}: ${email}`);
return {
id: result.id,
email,
username,
name,
};
}
catch (error) {
console.log(` โ ๏ธ Unexpected error creating ${persona} user ${email}: ${error.message}`);
return null;
}
}
/**
* NEW: Get persona-specific configuration (SUPASEED-001)
*/
getPersonaConfig(persona) {
const personaConfigs = {
casual_user: {
usernamePrefix: 'casual',
activities: ['browsing', 'sharing', 'commenting'],
traits: ['friendly', 'curious', 'social'],
experience: 'beginner'
},
expert_user: {
usernamePrefix: 'expert',
activities: ['analyzing', 'teaching', 'mentoring', 'reviewing'],
traits: ['knowledgeable', 'detailed', 'helpful'],
experience: 'expert'
},
content_creator: {
usernamePrefix: 'creator',
activities: ['creating', 'publishing', 'sharing', 'storytelling'],
traits: ['creative', 'engaging', 'prolific'],
experience: 'intermediate'
},
admin_user: {
usernamePrefix: 'admin',
activities: ['managing', 'moderating', 'organizing'],
traits: ['responsible', 'organized', 'fair'],
experience: 'advanced'
},
power_user: {
usernamePrefix: 'power',
activities: ['optimizing', 'customizing', 'automating'],
traits: ['efficient', 'technical', 'innovative'],
experience: 'advanced'
}
};
return personaConfigs[persona] || personaConfigs.casual_user;
}
/**
* NEW: Generate persona-specific bio (SUPASEED-001)
*/
generatePersonaBio(persona, personaConfig, domainConfig) {
const { faker } = this.context;
const activity = faker.helpers.arrayElement(personaConfig.activities);
const trait = faker.helpers.arrayElement(personaConfig.traits);
const location = faker.helpers.arrayElement(domainConfig.locations || ['the community', 'online', 'worldwide']);
const templates = [
`${personaConfig.experience.charAt(0).toUpperCase() + personaConfig.experience.slice(1)} user passionate about ${activity}. Known for being ${trait}. Active in ${location}.`,
`${trait.charAt(0).toUpperCase() + trait.slice(1)} ${persona.replace('_', ' ')} focused on ${activity}. Based in ${location}.`,
`${personaConfig.experience.charAt(0).toUpperCase() + personaConfig.experience.slice(1)} level user specializing in ${activity}. ${trait.charAt(0).toUpperCase() + trait.slice(1)} and engaged.`
];
return faker.helpers.arrayElement(templates);
}
/**
* Initialize enhanced MakerKit compatibility layer
*/
async initializeMakerKitCompatibility() {
// Build compatibility config from context
const compatibilityConfig = {
standardTestUsers: this.context.config.createStandardTestEmails || false,
customTestEmails: this.context.config.customTestEmails || [],
preserveAuthFlow: true,
preserveRLS: true,
makerkitVersion: 'auto', // Auto-detect from schema
teamAccountCreation: this.context.config.createTeamAccounts ?? true,
personalAccountCreation: true,
roleHierarchy: true,
subscriptionSupport: true,
notificationSystem: true,
overrides: {
primaryUserTable: this.context.config.schema?.primaryUserTable,
testUserPasswords: {
default: this.context.config.testUserPassword || 'password123'
}
}
};
this.makerkitCompatibility = new makerkit_compatibility_1.MakerKitCompatibilityLayer(this.context.client, compatibilityConfig, this.schemaAdapter);
// Initialize and validate compatibility
const validation = await this.makerkitCompatibility.initialize();
// Log compatibility status
console.log(`๐ง MakerKit compatibility: ${validation.compatibility}`);
console.log(`๐ Detected version: ${validation.detectedVersion}`);
if (validation.issues.length > 0) {
console.log('โ ๏ธ Compatibility issues:');
validation.issues.forEach(issue => console.log(` โข ${issue}`));
}
if (validation.recommendations.length > 0) {
console.log('๐ก Recommendations:');
validation.recommendations.forEach(rec => console.log(` โข ${rec}`));
}
}
/**
* Check user creation limits based on constraints
*/
async checkUserCreationLimits() {
try {
// Check if we have MakerKit pattern with personal account constraints
const strategy = this.schemaAdapter.getUserCreationStrategy();
if (strategy !== 'makerkit-profiles') {
// Not MakerKit, no constraints expected
return {
maxAdditional: Infinity,
existingPersonalAccounts: 0,
maxPersonalAccounts: Infinity,
constraintDetected: false
};
}
// Count existing personal accounts
const { data: existingAccounts, error } = await this.context.client
.from('accounts')
.select('id')
.eq('is_personal_account', true);
if (error) {
console.log('โ ๏ธ Could not check existing personal accounts, proceeding without constraints');
return {
maxAdditional: Infinity,
existingPersonalAccounts: 0,
maxPersonalAccounts: -1,
constraintDetected: false
};
}
const existingCount = existingAccounts?.length || 0;
// MakerKit typically allows 1 personal account per workspace
// This is based on the unique_personal_account constraint pattern
const maxAllowed = 1;
const maxAdditional = Math.max(0, maxAllowed - existingCount);
return {
maxAdditional,
existingPersonalAccounts: existingCount,
maxPersonalAccounts: maxAllowed,
constraintDetected: true
};
}
catch (error) {
console.log('โ ๏ธ Error checking user creation limits, proceeding without constraints:', error.message);
return {
maxAdditional: Infinity,
existingPersonalAccounts: -1,
maxPersonalAccounts: -1,
constraintDetected: false
};
}
}
/**
* Create enhanced standard test users using MakerKit compatibility layer
*/
async createEnhancedStandardTestUsers() {
console.log('๐งช Creating enhanced MakerKit test users...');
// Use the compatibility layer to create users with proper MakerKit integration
const result = await this.makerkitCompatibility.createStandardTestUsers();
// Convert StandardTestUser[] to CachedUser[] format
const cachedUsers = result.created.map(user => ({
id: crypto.randomUUID(), // This will be updated with actual ID from database
email: user.email,
username: user.username,
name: user.name,
}));
// Update stats
this.context.stats.usersCreated += result.created.length;
// Log any failures
if (result.failed.length > 0) {
console.log('โ ๏ธ Some users failed to create:');
result.failed.forEach(failure => {
console.log(` โข ${failure.user.email}: ${failure.error}`);
});
}
console.log(`โ
Enhanced MakerKit users created: ${result.created.length}/${result.created.length + result.failed.length}`);
return cachedUsers;
}
/**
* Legacy method for backward compatibility
* @deprecated Use createEnhancedStandardTestUsers instead
*/
async createStandardTestUsers() {
console.log('๐งช Creating standard MakerKit test users (legacy mode)...');
const standardTestEmails = [
{ email: 'test@makerkit.dev', name: 'Test User', username: 'test_user', role: 'admin' },
{ email: 'custom@makerkit.dev', name: 'Custom User', username: 'custom_user', role: 'custom' },
{ email: 'owner@makerkit.dev', name: 'Owner User', username: 'owner_user', role: 'owner' },
{ email: 'member@makerkit.dev', name: 'Member User', username: 'member_user', role: 'member' },
{ email: 'super-admin@makerkit.dev', name: 'Super Admin', username: 'super_admin', role: 'super-admin' },
];
const createdUsers = [];
for (const testUser of standardTestEmails) {
console.log(` Creating: ${testUser.email}`);
const bio = `Standard MakerKit test user with ${testUser.role} role for testing purposes.`;
const result = await this.schemaAdapter.createUserForSchema({
email: testUser.email,
name: testUser.name,
username: testUser.username,
bio,
picture_url: this.generateProfileImage(testUser.name.split(' ')[0], testUser.name.split(' ')[1] || 'User'),
});
if (result.success) {
createdUsers.push({
id: result.id,
email: testUser.email,
username: testUser.username,
name: testUser.name,
});
this.context.stats.usersCreated++;
console.log(` โ
Created: ${testUser.email}`);
}
else {
console.log(` โ ๏ธ Failed to create ${testUser.email}: ${result.error}`);
}
}
console.log(`โ
Created ${createdUsers.length} standard test users`);
return createdUsers;
}
async createUser() {
const { faker } = this.context;
const domainConfig = (0, domains_1.getDomainConfig)(this.context.config.domain);
// Generate realistic profile based on domain
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
// Use truly unique approach with UUID to prevent any collision
this.userCounter++;
const uuid = crypto.randomUUID().replace(/-/g, '').substring(0, 12);
const timestamp = Date.now();
const username = `user_${this.userCounter}_${timestamp}_${uuid}`;
const email = `${username}@${this.context.config.emailDomain || 'supaseed.test'}`;
const bio = this.generateUserBio(domainConfig);
const name = `${firstName} ${lastName}`;
try {
// Use schema adapter to create user with appropriate strategy
const result = await this.schemaAdapter.createUserForSchema({
email,
name,
username,
bio,
picture_url: this.generateProfileImage(firstName, lastName),
});
if (!result.success) {
// Provide detailed error information for debugging
const errorInfo = result.error || 'Unknown error';
console.log(` โ ๏ธ User creation failed for ${email}: ${errorInfo}`);
// Check if it's a column mapping issue
if (errorInfo.includes('column') && errorInfo.includes('does not exist')) {
console.log(` ๐ก This appears to be a column mapping issue. Check your schema configuration.`);
}
return null;
}
return {
id: result.id,
email,
username,
name,
};
}
catch (error) {
console.log(` โ ๏ธ Unexpected error creating user ${email}: ${error.message}`);
return null;
}
}
generateUserBio(domainConfig) {
const { faker } = this.context;
const activities = domainConfig.activities || ['working', 'creating', 'building', 'learning'];
const locations = domainConfig.locations || ['the city', 'remote', 'downtown', 'worldwide'];
const templates = domainConfig.bioTemplates || [
'Passionate about {activity}. Based in {location}.',
'Professional focusing on {activity}. Located in {location}.'
];
// Select random template and fill in placeholders
let bio = faker.helpers.arrayElement(templates);
bio = bio.replace('{activity}', faker.helpers.arrayElement(activities));
bio = bio.replace('{location}', faker.helpers.arrayElement(locations));
return bio;
}
/**
* Legacy function for backward compatibility
* @deprecated Use generateUserBio instead
*/
generateOutdoorBio() {
const { faker } = this.context;
const activities = [
'hiking', 'backpacking', 'camping', 'overlanding', 'rock climbing',
'mountaineering', 'van life', 'car camping', 'ultralight backpacking',
'bushcraft', 'photography', 'wildlife watching'
];
const locations = [
'Pacific Northwest', 'Colorado Rockies', 'California Sierra',
'Appalachian Mountains', 'Utah desert', 'Alaska wilderness',
'Canadian Rockies', 'Cascade Range', 'Great Smoky Mountains'
];
const templates = [
`Passionate ${faker.helpers.arrayElements(activities, 2).join(' and ')} enthusiast. Love exploring the ${faker.helpers.arrayElement(locations)}.`,
`Weekend warrior with a passion for ${faker.helpers.arrayElement(activities)}. Always planning the next adventure!`,
`${faker.helpers.arrayElement(activities)} addict from the ${faker.helpers.arrayElement(locations)}. Sharing my gear setups and trail stories.`,
`Outdoor photographer and ${faker.helpers.arrayElement(activities)} enthusiast. ${faker.helpers.arrayElement(['Based in', 'Exploring', 'Living in'])} the ${faker.helpers.arrayElement(locations)}.`,
];
return faker.helpers.arrayElement(templates);
}
generateProfileImage(firstName, lastName) {
// Use a service like RoboHash or UI Avatars for consistent profile images
const initial = `${firstName[0]}${lastName[0]}`.toUpperCase();
return `https://ui-avatars.com/api/?name=${encodeURIComponent(firstName + '+' + lastName)}&background=random&bold=true&size=200`;
}
}
exports.UserSeeder = UserSeeder;
//# sourceMappingURL=user-seeder.js.map