/** * Migration script to move data from Supabase to the new PostgreSQL database * * Prerequisites: * 1. Set SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables * 2. Set DATABASE_URL for the new PostgreSQL database * 3. Run migrations on the new database first: pnpm db:migrate * * Usage: * SUPABASE_URL=... SUPABASE_SERVICE_KEY=... DATABASE_URL=... tsx src/migrate-from-supabase.ts */ import { createClient as createSupabaseClient } from '@supabase/supabase-js'; import { getDb, closeDb } from './client.js'; import { decks, cards, studySessions, cardProgress, deckTemplates, aiGenerations, userStats, } from './schema/index.js'; // Initialize Supabase client const supabaseUrl = process.env.SUPABASE_URL; const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY; if (!supabaseUrl || !supabaseServiceKey) { console.error('Missing SUPABASE_URL or SUPABASE_SERVICE_KEY environment variables'); process.exit(1); } const supabase = createSupabaseClient(supabaseUrl, supabaseServiceKey); const db = getDb(); interface MigrationStats { table: string; migrated: number; errors: number; } const stats: MigrationStats[] = []; async function migrateTable( tableName: string, supabaseTableName: string, drizzleTable: any, transformer: (row: any) => T ) { console.log(`\nMigrating ${tableName}...`); let migrated = 0; let errors = 0; try { // Fetch all data from Supabase const { data, error } = await supabase.from(supabaseTableName).select('*'); if (error) { console.error(`Error fetching ${tableName}:`, error); stats.push({ table: tableName, migrated: 0, errors: 1 }); return; } if (!data || data.length === 0) { console.log(`No data found in ${tableName}`); stats.push({ table: tableName, migrated: 0, errors: 0 }); return; } console.log(`Found ${data.length} rows in ${tableName}`); // Process in batches of 100 const batchSize = 100; for (let i = 0; i < data.length; i += batchSize) { const batch = data.slice(i, i + batchSize); const transformed = batch.map(transformer); try { await db.insert(drizzleTable).values(transformed).onConflictDoNothing(); migrated += batch.length; process.stdout.write(`\r Migrated ${migrated}/${data.length} rows`); } catch (err) { console.error(`\n Error inserting batch:`, err); errors += batch.length; } } console.log(`\n Completed: ${migrated} migrated, ${errors} errors`); } catch (err) { console.error(`Error migrating ${tableName}:`, err); errors++; } stats.push({ table: tableName, migrated, errors }); } async function main() { console.log('=== Cards Data Migration ==='); console.log('From: Supabase'); console.log('To: PostgreSQL (Drizzle)'); console.log('==============================\n'); try { // 1. Migrate decks await migrateTable('decks', 'decks', decks, (row) => ({ id: row.id, userId: row.user_id, title: row.title, description: row.description, coverImageUrl: row.cover_image_url, isPublic: row.is_public ?? false, isFeatured: row.is_featured ?? false, featuredAt: row.featured_at ? new Date(row.featured_at) : null, settings: row.settings ?? {}, tags: row.tags ?? [], metadata: row.metadata ?? {}, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), })); // 2. Migrate cards await migrateTable('cards', 'cards', cards, (row) => ({ id: row.id, deckId: row.deck_id, position: row.position ?? 0, title: row.title, content: row.content, cardType: row.card_type, aiModel: row.ai_model, aiPrompt: row.ai_prompt, version: row.version ?? 1, isFavorite: row.is_favorite ?? false, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), })); // 3. Migrate study sessions await migrateTable('study_sessions', 'study_sessions', studySessions, (row) => ({ id: row.id, deckId: row.deck_id, userId: row.user_id, mode: row.mode, totalCards: row.total_cards ?? 0, completedCards: row.completed_cards ?? 0, correctCards: row.correct_cards ?? 0, startedAt: new Date(row.started_at), completedAt: row.completed_at ? new Date(row.completed_at) : null, timeSpentSeconds: row.time_spent_seconds ?? 0, })); // 4. Migrate card progress await migrateTable('card_progress', 'card_progress', cardProgress, (row) => ({ id: row.id, userId: row.user_id, cardId: row.card_id, easeFactor: row.ease_factor?.toString() ?? '2.5', interval: row.interval ?? 0, repetitions: row.repetitions ?? 0, lastReviewed: row.last_reviewed ? new Date(row.last_reviewed) : null, nextReview: row.next_review ? new Date(row.next_review) : null, status: row.status ?? 'new', createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), })); // 5. Migrate deck templates await migrateTable('deck_templates', 'deck_templates', deckTemplates, (row) => ({ id: row.id, title: row.title, description: row.description, category: row.category, templateData: row.template_data ?? { cards: [] }, isActive: row.is_active ?? true, isPublic: row.is_public ?? true, popularity: row.popularity ?? 0, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), })); // 6. Migrate AI generations await migrateTable('ai_generations', 'ai_generations', aiGenerations, (row) => ({ id: row.id, userId: row.user_id, deckId: row.deck_id, functionName: row.function_name, prompt: row.prompt, model: row.model, status: row.status ?? 'pending', metadata: row.metadata ?? {}, completedAt: row.completed_at ? new Date(row.completed_at) : null, createdAt: new Date(row.created_at), })); // 7. Migrate user stats await migrateTable('user_stats', 'user_stats', userStats, (row) => ({ userId: row.user_id, totalWins: row.total_wins ?? 0, totalSessions: row.total_sessions ?? 0, totalCardsStudied: row.total_cards_studied ?? 0, totalTimeSeconds: row.total_time_seconds ?? 0, averageAccuracy: row.average_accuracy?.toString() ?? '0', streakDays: row.streak_days ?? 0, longestStreak: row.longest_streak ?? 0, lastStudyDate: row.last_study_date, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), })); // Print summary console.log('\n\n=== Migration Summary ==='); console.log('-------------------------'); let totalMigrated = 0; let totalErrors = 0; for (const stat of stats) { console.log(`${stat.table}: ${stat.migrated} migrated, ${stat.errors} errors`); totalMigrated += stat.migrated; totalErrors += stat.errors; } console.log('-------------------------'); console.log(`Total: ${totalMigrated} rows migrated, ${totalErrors} errors`); if (totalErrors === 0) { console.log('\n✅ Migration completed successfully!'); } else { console.log('\n⚠️ Migration completed with some errors. Please review.'); } } catch (error) { console.error('Migration failed:', error); process.exit(1); } finally { await closeDb(); } } main();