// MUST load environment variables BEFORE any other imports require('dotenv').config({ path: require('path').resolve(__dirname, '../.env.local') }); import { Client } from 'pg'; import admin from 'firebase-admin'; import { FieldValue } from 'firebase-admin/firestore'; const PG_CONNECTION_STRING = 'postgresql://postgres:jhsRNOIyjjVfrdvDXnUVcXXXsuzjvcFc@metro.proxy.rlwy.net:30866/railway'; // Initialize Firebase Admin directly if (!admin.apps.length) { const privateKey = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'); if (!process.env.FIREBASE_PROJECT_ID || !process.env.FIREBASE_CLIENT_EMAIL || !privateKey) { throw new Error('Missing Firebase Admin credentials. Check your .env.local file.'); } admin.initializeApp({ credential: admin.credential.cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: privateKey, }), }); console.log('āœ… Firebase Admin initialized successfully'); } const adminDb = admin.firestore(); const adminAuth = admin.auth(); interface PgUser { id: number; email: string; name: string; created_at: Date; settings: any; } interface PgProject { id: number; client_id: number; name: string; workspace_path: string; status: string; created_at: Date; updated_at: Date; metadata: any; } interface PgSession { id: number; session_id: string; project_id: number; user_id: number; started_at: Date; last_updated: Date; ended_at: Date | null; status: string; conversation: any[]; file_changes: any[]; message_count: number; user_message_count: number; assistant_message_count: number; file_change_count: number; duration_minutes: number; summary: string | null; tasks_identified: any[]; decisions_made: any[]; technologies_used: any[]; metadata: any; total_tokens: number; prompt_tokens: number; completion_tokens: number; estimated_cost_usd: number; model: string; } interface PgWorkCompleted { id: number; project_id: number; session_id: number; title: string; description: string; completed_at: Date; files_modified: any[]; lines_added: number; lines_removed: number; metadata: any; } interface PgClient { id: number; owner_user_id: number; name: string; email: string | null; created_at: Date; metadata: any; } async function migrateUsers(pgClient: Client, userMapping: Map) { console.log('\nšŸ“‹ Migrating Users...'); const result = await pgClient.query('SELECT * FROM users'); for (const pgUser of result.rows) { try { // Create Firebase Auth user let firebaseUser; try { firebaseUser = await adminAuth.getUserByEmail(pgUser.email); console.log(` āœ… User already exists: ${pgUser.email}`); } catch { // User doesn't exist, create them firebaseUser = await adminAuth.createUser({ email: pgUser.email, displayName: pgUser.name, emailVerified: true, }); console.log(` ✨ Created Firebase Auth user: ${pgUser.email}`); } // Store mapping userMapping.set(pgUser.id, firebaseUser.uid); // Create user document in Firestore const workspace = pgUser.email.split('@')[0].replace(/[^a-z0-9]/gi, '-').toLowerCase(); await adminDb.collection('users').doc(firebaseUser.uid).set({ uid: firebaseUser.uid, email: pgUser.email, displayName: pgUser.name, workspace: workspace, settings: pgUser.settings || {}, createdAt: FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(), migratedFrom: 'postgresql', originalPgId: pgUser.id, }); console.log(` āœ… Migrated user: ${pgUser.email} → ${firebaseUser.uid}`); } catch (error) { console.error(` āŒ Error migrating user ${pgUser.email}:`, error); } } } async function migrateClients(pgClient: Client, userMapping: Map) { console.log('\nšŸ“‹ Migrating Clients...'); const result = await pgClient.query('SELECT * FROM clients'); for (const pgClient of result.rows) { const firebaseUserId = userMapping.get(pgClient.owner_user_id); if (!firebaseUserId) { console.log(` āš ļø Skipping client ${pgClient.name} - user not found`); continue; } try { const clientRef = adminDb.collection('clients').doc(); await clientRef.set({ id: clientRef.id, ownerId: firebaseUserId, name: pgClient.name, email: pgClient.email || null, createdAt: FieldValue.serverTimestamp(), metadata: pgClient.metadata || {}, migratedFrom: 'postgresql', originalPgId: pgClient.id, }); console.log(` āœ… Migrated client: ${pgClient.name}`); } catch (error) { console.error(` āŒ Error migrating client ${pgClient.name}:`, error); } } } async function migrateProjects(pgClient: Client, userMapping: Map, projectMapping: Map) { console.log('\nšŸ“‹ Migrating Projects...'); const result = await pgClient.query('SELECT * FROM projects'); for (const pgProject of result.rows) { try { // Get the client to find the owner const clientResult = await pgClient.query('SELECT owner_user_id FROM clients WHERE id = $1', [pgProject.client_id]); const firebaseUserId = userMapping.get(clientResult.rows[0]?.owner_user_id); if (!firebaseUserId) { console.log(` āš ļø Skipping project ${pgProject.name} - user not found`); continue; } // Get user's workspace const userDoc = await adminDb.collection('users').doc(firebaseUserId).get(); const workspace = userDoc.data()?.workspace || 'default-workspace'; const projectRef = adminDb.collection('projects').doc(); await projectRef.set({ id: projectRef.id, name: pgProject.name, slug: pgProject.name.toLowerCase().replace(/[^a-z0-9]/g, '-'), userId: firebaseUserId, workspace: workspace, productName: pgProject.name, productVision: pgProject.metadata?.vision || null, workspacePath: pgProject.workspace_path, status: pgProject.status, isForClient: true, hasLogo: false, hasDomain: false, hasWebsite: false, hasGithub: false, hasChatGPT: false, createdAt: FieldValue.serverTimestamp(), updatedAt: FieldValue.serverTimestamp(), metadata: pgProject.metadata || {}, migratedFrom: 'postgresql', originalPgId: pgProject.id, }); projectMapping.set(pgProject.id, projectRef.id); console.log(` āœ… Migrated project: ${pgProject.name} → ${projectRef.id}`); } catch (error) { console.error(` āŒ Error migrating project ${pgProject.name}:`, error); } } } async function migrateSessions(pgClient: Client, userMapping: Map, projectMapping: Map) { console.log('\nšŸ“‹ Migrating Sessions...'); const result = await pgClient.query('SELECT * FROM sessions ORDER BY started_at'); for (const pgSession of result.rows) { try { const firebaseUserId = userMapping.get(pgSession.user_id); const firebaseProjectId = projectMapping.get(pgSession.project_id); if (!firebaseUserId) { console.log(` āš ļø Skipping session ${pgSession.session_id} - user not found`); continue; } const sessionRef = adminDb.collection('sessions').doc(); await sessionRef.set({ id: sessionRef.id, userId: firebaseUserId, projectId: firebaseProjectId || null, // Session data startTime: pgSession.started_at, endTime: pgSession.ended_at || null, duration: pgSession.duration_minutes * 60, // Convert to seconds // Project context workspacePath: null, // Not in old schema workspaceName: null, // AI usage model: pgSession.model, tokensUsed: pgSession.total_tokens, promptTokens: pgSession.prompt_tokens, completionTokens: pgSession.completion_tokens, cost: parseFloat(String(pgSession.estimated_cost_usd)), // Context filesModified: pgSession.file_changes.map((fc: any) => fc.path || fc.file), conversationSummary: pgSession.summary || null, conversation: pgSession.conversation || [], // Additional data from old schema messageCount: pgSession.message_count, userMessageCount: pgSession.user_message_count, assistantMessageCount: pgSession.assistant_message_count, fileChangeCount: pgSession.file_change_count, tasksIdentified: pgSession.tasks_identified || [], decisionsMade: pgSession.decisions_made || [], technologiesUsed: pgSession.technologies_used || [], status: pgSession.status, metadata: pgSession.metadata || {}, createdAt: pgSession.started_at, updatedAt: pgSession.last_updated, migratedFrom: 'postgresql', originalPgId: pgSession.id, originalSessionId: pgSession.session_id, }); console.log(` āœ… Migrated session: ${pgSession.session_id}`); } catch (error) { console.error(` āŒ Error migrating session ${pgSession.session_id}:`, error); } } } async function migrateWorkCompleted(pgClient: Client, projectMapping: Map) { console.log('\nšŸ“‹ Migrating Work Completed...'); const result = await pgClient.query('SELECT * FROM work_completed ORDER BY completed_at'); for (const work of result.rows) { try { const firebaseProjectId = projectMapping.get(work.project_id); if (!firebaseProjectId) { console.log(` āš ļø Skipping work ${work.title} - project not found`); continue; } const workRef = adminDb.collection('workCompleted').doc(); await workRef.set({ id: workRef.id, projectId: firebaseProjectId, sessionId: work.session_id ? `pg-session-${work.session_id}` : null, title: work.title, description: work.description, completedAt: work.completed_at, filesModified: work.files_modified || [], linesAdded: work.lines_added || 0, linesRemoved: work.lines_removed || 0, metadata: work.metadata || {}, createdAt: work.completed_at, migratedFrom: 'postgresql', originalPgId: work.id, }); console.log(` āœ… Migrated work: ${work.title}`); } catch (error) { console.error(` āŒ Error migrating work ${work.title}:`, error); } } } async function main() { console.log('šŸš€ Starting PostgreSQL to Firebase migration...\n'); const pgClient = new Client({ connectionString: PG_CONNECTION_STRING, }); try { // Connect to PostgreSQL console.log('šŸ“” Connecting to PostgreSQL...'); await pgClient.connect(); console.log('āœ… Connected to PostgreSQL\n'); // Mappings to track old ID -> new ID const userMapping = new Map(); const projectMapping = new Map(); // Migrate in order (respecting foreign keys) await migrateUsers(pgClient, userMapping); await migrateClients(pgClient, userMapping); await migrateProjects(pgClient, userMapping, projectMapping); await migrateSessions(pgClient, userMapping, projectMapping); await migrateWorkCompleted(pgClient, projectMapping); console.log('\nāœ… Migration completed successfully!'); console.log('\nšŸ“Š Summary:'); console.log(` - Users migrated: ${userMapping.size}`); console.log(` - Projects migrated: ${projectMapping.size}`); } catch (error) { console.error('\nāŒ Migration failed:', error); throw error; } finally { await pgClient.end(); console.log('\nšŸ“” Disconnected from PostgreSQL'); } } // Run migration main().catch(console.error);