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
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 __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