UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

396 lines โ€ข 20.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 __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StrategyRegistry = exports.MediaSeeder = exports.GearSeeder = exports.SetupSeeder = exports.UserSeeder = exports.BaseDataSeeder = exports.AuthSeeder = exports.SchemaAdapter = exports.ConfigManager = exports.SupaSeedFramework = void 0; exports.createDefaultConfig = createDefaultConfig; const faker_1 = require("@faker-js/faker"); const auth_seeder_1 = require("./features/generation/seeders/auth-seeder"); Object.defineProperty(exports, "AuthSeeder", { enumerable: true, get: function () { return auth_seeder_1.AuthSeeder; } }); const base_data_seeder_1 = require("./features/generation/seeders/base-data-seeder"); Object.defineProperty(exports, "BaseDataSeeder", { enumerable: true, get: function () { return base_data_seeder_1.BaseDataSeeder; } }); const user_seeder_1 = require("./features/generation/seeders/user-seeder"); Object.defineProperty(exports, "UserSeeder", { enumerable: true, get: function () { return user_seeder_1.UserSeeder; } }); const setup_seeder_1 = require("./features/generation/seeders/setup-seeder"); Object.defineProperty(exports, "SetupSeeder", { enumerable: true, get: function () { return setup_seeder_1.SetupSeeder; } }); const gear_seeder_1 = require("./features/generation/seeders/gear-seeder"); Object.defineProperty(exports, "GearSeeder", { enumerable: true, get: function () { return gear_seeder_1.GearSeeder; } }); const media_seeder_1 = require("./features/generation/seeders/media-seeder"); Object.defineProperty(exports, "MediaSeeder", { enumerable: true, get: function () { return media_seeder_1.MediaSeeder; } }); const schema_adapter_1 = require("./core/schema-adapter"); const logger_1 = require("./core/utils/logger"); const schema_validator_1 = require("./features/analysis/schema-validator"); const enhanced_supabase_client_1 = require("./core/utils/enhanced-supabase-client"); class SupaSeedFramework { constructor(config) { this.config = config; this.validateConfig(config); this.client = (0, enhanced_supabase_client_1.createEnhancedSupabaseClient)(config.supabaseUrl, config.supabaseServiceKey); this.context = { client: this.client, config, faker: faker_1.faker, cache: new Map(), stats: { usersCreated: 0, setupsCreated: 0, imagesUploaded: 0, startTime: new Date(), } }; } async seed() { console.log('๐ŸŒฑ Starting database seeding...'); try { // First, check database connectivity and schema const schemaAdapter = await this.validateDatabaseAndSchema(); // Validate schema compatibility const validator = new schema_validator_1.SchemaValidator(this.client, schemaAdapter); const validationResult = await validator.validateSchema(); schema_validator_1.SchemaValidator.printResults(validationResult); if (!validationResult.valid) { const continueAnyway = process.env.FORCE_SEED === 'true'; if (!continueAnyway) { throw new Error('Schema validation failed. Set FORCE_SEED=true to continue anyway.'); } logger_1.Logger.warn('Continuing despite validation errors (FORCE_SEED=true)'); } // Define seeding order (dependency-aware) const seeders = [ new auth_seeder_1.AuthSeeder(this.context), new user_seeder_1.UserSeeder(this.context), new base_data_seeder_1.BaseDataSeeder(this.context), new setup_seeder_1.SetupSeeder(this.context), new gear_seeder_1.GearSeeder(this.context), new media_seeder_1.MediaSeeder(this.context), ]; for (const seeder of seeders) { try { console.log(`๐Ÿ”„ Running ${seeder.constructor.name}...`); await seeder.seed(); console.log(`โœ… ${seeder.constructor.name} completed`); } catch (error) { console.warn(`โš ๏ธ ${seeder.constructor.name} failed but seeding continues:`, error.message); // Continue with next seeder rather than failing completely } } await this.printSummary(); } catch (error) { console.error('โŒ Seeding failed:', error); throw error; } } async validateDatabaseAndSchema() { logger_1.Logger.step('Validating database connection and schema...'); try { // Test basic connection using multiple methods with clear feedback let connectionValid = false; let connectionMethod = ''; // Method 1: Test with auth.users query (most reliable for service role) try { logger_1.Logger.debug('Testing connection with auth.admin.listUsers...'); const { error: authError } = await this.client.auth.admin.listUsers({ page: 1, perPage: 1 }); if (!authError) { connectionValid = true; connectionMethod = 'auth.admin'; logger_1.Logger.debug('Connection successful via auth.admin'); } else { logger_1.Logger.debug('Auth test failed:', authError); } } catch (error) { logger_1.Logger.debug('Auth test threw error:', error); } // Method 2: Try a simple table query if (!connectionValid) { try { logger_1.Logger.debug('Testing connection with simple table query...'); const { error } = await this.client .from('_dummy_table_test_connection') .select('*') .limit(1); // PGRST116 = table doesn't exist (which is expected, but means connection works) if (!error || error.code === 'PGRST116') { connectionValid = true; connectionMethod = 'table query'; logger_1.Logger.debug('Connection successful via table query'); } } catch (error) { logger_1.Logger.debug('Table query test failed:', error); } } // Method 3: Try RPC call if (!connectionValid) { try { logger_1.Logger.debug('Testing connection with RPC call...'); const { error } = await this.client.rpc('version'); if (!error) { connectionValid = true; connectionMethod = 'rpc'; logger_1.Logger.debug('Connection successful via RPC'); } } catch (error) { // RPC might not exist, which is fine logger_1.Logger.debug('RPC test failed:', error); } } if (!connectionValid) { throw new Error('All connection test methods failed'); } logger_1.Logger.success(`Database connection validated (method: ${connectionMethod})`); // Initialize schema adapter to detect schema - pass config for overrides const schemaAdapter = new schema_adapter_1.SchemaAdapter(this.client, this.config); const schemaInfo = await schemaAdapter.detectSchema(); // Provide helpful guidance based on detected schema if (!schemaInfo.hasAccounts && !schemaInfo.hasProfiles) { logger_1.Logger.warn('No user tables detected. You may need to:'); logger_1.Logger.info(' 1. Run the schema.sql file to create required tables'); logger_1.Logger.info(' 2. Or ensure your custom schema is compatible'); logger_1.Logger.info(' 3. Check your database permissions'); } else { const strategy = schemaAdapter.getUserCreationStrategy(); logger_1.Logger.success(`Schema validated. Using ${strategy} user creation strategy.`); } return schemaAdapter; } catch (error) { logger_1.Logger.error('Connection validation failed:', error); // Provide detailed debugging information logger_1.Logger.debug('Connection Debug Info:', { URL: this.config.supabaseUrl, ServiceKey: this.config.supabaseServiceKey ? '***' + this.config.supabaseServiceKey.slice(-4) : 'Not provided', Environment: this.config.environment }); if (error.message.includes('permission denied') || error.message.includes('JWT')) { throw new Error(`โŒ Database permissions error. Please ensure your SUPABASE_SERVICE_ROLE_KEY has the necessary permissions.\n\n` + `๐Ÿ”ง Debug Info:\n` + ` โ€ข URL: ${this.config.supabaseUrl}\n` + ` โ€ข Key ends with: ${this.config.supabaseServiceKey ? '***' + this.config.supabaseServiceKey.slice(-4) : 'NOT_PROVIDED'}\n\n` + `โœ… Required permissions:\n` + ` โ€ข Create auth users (admin.createUser)\n` + ` โ€ข Insert into user tables (accounts/profiles)\n` + ` โ€ข Access table schemas\n\n` + `๐Ÿ’ก For local Supabase, make sure you're using the service_role key, not anon key.`); } else if (error.message.includes('connection') || error.message.includes('ECONNREFUSED') || error.message.includes('fetch')) { throw new Error(`โŒ Database connection failed. Connection details:\n\n` + `๐Ÿ”ง Debug Info:\n` + ` โ€ข URL: ${this.config.supabaseUrl}\n` + ` โ€ข Key: ${this.config.supabaseServiceKey ? 'PROVIDED' : 'MISSING'}\n` + ` โ€ข Error: ${error.message}\n\n` + `โœ… Please check:\n` + ` โ€ข SUPABASE_URL is correct and accessible\n` + ` โ€ข SUPABASE_SERVICE_ROLE_KEY is valid\n` + ` โ€ข Your network connection\n` + ` โ€ข Supabase instance is running\n\n` + `๐Ÿ’ก For local development, ensure Supabase is running on ${this.config.supabaseUrl}`); } else if (error.message.includes('Invalid JWT') || error.message.includes('jwt')) { throw new Error(`โŒ JWT/Authentication error:\n\n` + `๐Ÿ”ง Debug Info:\n` + ` โ€ข URL: ${this.config.supabaseUrl}\n` + ` โ€ข Error: ${error.message}\n\n` + `๐Ÿ’ก This usually means:\n` + ` โ€ข You're using the wrong API key (use service_role, not anon)\n` + ` โ€ข The API key has expired or is malformed\n` + ` โ€ข Local Supabase JWT_SECRET doesn't match`); } throw new Error(`โŒ Connection validation failed: ${error.message}`); } } async cleanup() { console.log('๐Ÿงน Cleaning up existing seed data...'); try { // Get test accounts first - look for any test domains const { data: testAccounts } = await this.client .from('accounts') .select('id, email') .or('email.like.%.test,email.like.%supaseed%,email.like.%wildernest%,email.like.%campfire%,email.like.%makerkit%,email.like.%demo,email.like.%fresh-test%'); if (!testAccounts?.length) { console.log('โ„น๏ธ No test data found to clean up'); return; } console.log(`๐Ÿ” Found ${testAccounts.length} test accounts to clean up:`); testAccounts.forEach((acc) => console.log(` - ${acc.email} (${acc.id})`)); const testAccountIds = testAccounts.map((acc) => acc.id); // Clean setup_gear_items first (if table exists) try { await this.client .from('setup_gear_items') .delete() .in('setup_id', (await this.client .from('setups') .select('id') .in('account_id', testAccountIds)).data?.map((s) => s.id) || []); } catch { console.log('โ„น๏ธ setup_gear_items table not found, skipping'); } // Clean setups await this.client .from('setups') .delete() .in('account_id', testAccountIds); // Clean accounts await this.client .from('accounts') .delete() .in('id', testAccountIds); // Clean auth.users - this is critical for avoiding duplicate email errors for (const accountId of testAccountIds) { try { await this.client.auth.admin.deleteUser(accountId); } catch (error) { // Some users might not exist in auth.users, that's okay console.log(`โ„น๏ธ Auth user ${accountId} not found or already deleted`); } } console.log('โœ… Cleanup completed'); } catch (error) { console.error('โŒ Cleanup failed:', error); throw error; } } async status() { console.log('๐Ÿ“Š Checking seeding status...'); try { const { data: testAccounts } = await this.client .from('accounts') .select('id, email, created_at') .like('email', '%.test'); const { data: setups } = await this.client .from('setups') .select('id, title, account_id') .in('account_id', testAccounts?.map((acc) => acc.id) || []); console.log(`๐Ÿ‘ฅ Test accounts: ${testAccounts?.length || 0}`); console.log(`๐Ÿ•๏ธ Test setups: ${setups?.length || 0}`); if (testAccounts?.length) { console.log('\n๐Ÿ“‹ Test accounts:'); testAccounts.forEach((acc) => { console.log(` โ€ข ${acc.email}`); }); } } catch (error) { console.error('โŒ Status check failed:', error); throw error; } } validateConfig(config) { const requiredFields = [ 'supabaseUrl', 'supabaseServiceKey', ]; const missingFields = requiredFields.filter(field => !config[field] || config[field] === ''); if (missingFields.length > 0) { throw new Error(`Missing required configuration: ${missingFields.join(', ')}.\n` + 'Please ensure SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set in your environment or configuration.'); } if (!config.supabaseUrl.startsWith('http')) { throw new Error('supabaseUrl must be a valid HTTP/HTTPS URL'); } if (config.userCount <= 0 || config.setupsPerUser <= 0) { throw new Error('userCount and setupsPerUser must be greater than 0'); } // NEW: Validate userStrategy configuration (SUPASEED-001) if (config.userStrategy && !['use-existing', 'create-new', 'hybrid'].includes(config.userStrategy)) { throw new Error(`Invalid userStrategy: '${config.userStrategy}'. Must be 'use-existing', 'create-new', or 'hybrid'.`); } // Validate existingUsers configuration when userStrategy requires it if ((config.userStrategy === 'use-existing' || config.userStrategy === 'hybrid') && config.existingUsers) { if (config.existingUsers.table && typeof config.existingUsers.table !== 'string') { throw new Error('existingUsers.table must be a string'); } if (config.existingUsers.filter && typeof config.existingUsers.filter !== 'object') { throw new Error('existingUsers.filter must be an object'); } } // Validate additionalUsers configuration for hybrid mode if (config.userStrategy === 'hybrid' && config.additionalUsers) { if (config.additionalUsers.count && config.additionalUsers.count <= 0) { throw new Error('additionalUsers.count must be greater than 0'); } if (config.additionalUsers.authIntegration && !['makerkit', 'supabase', 'custom'].includes(config.additionalUsers.authIntegration)) { throw new Error(`Invalid additionalUsers.authIntegration: '${config.additionalUsers.authIntegration}'. Must be 'makerkit', 'supabase', or 'custom'.`); } } } async printSummary() { const { stats } = this.context; const duration = Date.now() - stats.startTime.getTime(); console.log('\n๐Ÿ“Š Seeding Summary:'); console.log(`โฑ๏ธ Duration: ${duration}ms`); console.log(`๐Ÿ‘ฅ Users created: ${stats.usersCreated}`); console.log(`๐Ÿ•๏ธ Setups created: ${stats.setupsCreated}`); console.log(`๐Ÿ–ผ๏ธ Images uploaded: ${stats.imagesUploaded}`); console.log('โœจ Seeding completed successfully!\n'); } } exports.SupaSeedFramework = SupaSeedFramework; /** * Create default configuration for seeding */ function createDefaultConfig(overrides = {}) { return { supabaseUrl: process.env.SUPABASE_URL || 'http://127.0.0.1:54321', supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '', environment: process.env.NODE_ENV || 'local', userCount: 10, setupsPerUser: 3, imagesPerSetup: 3, enableRealImages: false, seed: 'supa-seed-2025', emailDomain: 'supaseed.test', // NEW: Default values for MakerKit Integration (SUPASEED-001) // Maintains 100% backward compatibility with 'create-new' as default userStrategy: 'create-new', existingUsers: { preserve: true, table: 'accounts', filter: { is_personal_account: true }, idField: 'id' }, additionalUsers: { count: 7, personas: ['casual_user', 'expert_user', 'content_creator', 'admin_user', 'power_user'], authIntegration: 'supabase' }, ...overrides, }; } // Export types and classes for library usage __exportStar(require("./core/types/types"), exports); __exportStar(require("./core/types/config-types"), exports); var config_manager_1 = require("./core/config/config-manager"); Object.defineProperty(exports, "ConfigManager", { enumerable: true, get: function () { return config_manager_1.ConfigManager; } }); var schema_adapter_2 = require("./core/schema-adapter"); Object.defineProperty(exports, "SchemaAdapter", { enumerable: true, get: function () { return schema_adapter_2.SchemaAdapter; } }); // Export framework strategy system __exportStar(require("./features/integration/strategy-interface"), exports); var strategy_registry_1 = require("./features/integration/strategy-registry"); Object.defineProperty(exports, "StrategyRegistry", { enumerable: true, get: function () { return strategy_registry_1.StrategyRegistry; } }); //# sourceMappingURL=index.js.map