UNPKG

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
"use strict"; 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