374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
// 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<number, string>) {
|
|
console.log('\n📋 Migrating Users...');
|
|
const result = await pgClient.query<PgUser>('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<number, string>) {
|
|
console.log('\n📋 Migrating Clients...');
|
|
const result = await pgClient.query<PgClient>('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<number, string>, projectMapping: Map<number, string>) {
|
|
console.log('\n📋 Migrating Projects...');
|
|
const result = await pgClient.query<PgProject>('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<number, string>, projectMapping: Map<number, string>) {
|
|
console.log('\n📋 Migrating Sessions...');
|
|
const result = await pgClient.query<PgSession>('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<number, string>) {
|
|
console.log('\n📋 Migrating Work Completed...');
|
|
const result = await pgClient.query<PgWorkCompleted>('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<number, string>();
|
|
const projectMapping = new Map<number, string>();
|
|
|
|
// 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);
|
|
|