import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { fileURLToPath } from "url"; import path from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const migrationUrl = process.env.DATABASE_DIRECT_URL || process.env.DATABASE_URL; if (!migrationUrl) { console.error("ERROR: DATABASE_URL (or DATABASE_DIRECT_URL) is required"); process.exit(1); } console.log("Running database migrations..."); const client = postgres(migrationUrl, { max: 1, onnotice: () => {}, // suppress NOTICE messages (schema/table already exists, etc.) }); try { const db = drizzle(client); await migrate(db, { migrationsFolder: path.resolve(__dirname, "../drizzle") }); console.log("Migrations completed successfully"); } catch (err) { console.error("Migration failed:", err); await client.end().catch(() => {}); process.exit(1); } // ── Clerk → BetterAuth one-time data migration ──────────────────────────────── // FK conversion and email_verified always run (idempotent WHERE clauses). // OAuth account import only runs when CLERK_SECRET_KEY is set. console.log("Running FK conversion..."); try { await convertForeignKeys(client); } catch (err) { console.error("FK conversion failed:", err); await client.end().catch(() => {}); process.exit(1); } const CLERK_SECRET_KEY = process.env.CLERK_SECRET_KEY; if (!CLERK_SECRET_KEY) { console.log("CLERK_SECRET_KEY not set — skipping Clerk OAuth account import"); } else { console.log("Running Clerk OAuth account import..."); try { await importClerkOAuthAccounts(client, CLERK_SECRET_KEY); console.log("Clerk OAuth account import complete"); } catch (err) { console.error("Clerk OAuth account import failed:", err); await client.end().catch(() => {}); process.exit(1); } } await client.end().catch(() => {}); process.exit(0); // ───────────────────────────────────────────────────────────────────────────── async function convertForeignKeys(sql) { // Convert teams.owner_id from Clerk IDs to users.id UUIDs. // Safe to run repeatedly — WHERE clause only matches unconverted rows. const teamsResult = await sql` UPDATE teams t SET owner_id = u.id::text FROM users u WHERE t.owner_id = u.clerk_id AND u.clerk_id IS NOT NULL `; console.log(` teams.owner_id: ${teamsResult.count} rows converted`); // Convert commissioners.user_id from Clerk IDs to users.id UUIDs const commissResult = await sql` UPDATE commissioners c SET user_id = u.id::text FROM users u WHERE c.user_id = u.clerk_id AND u.clerk_id IS NOT NULL `; console.log(` commissioners.user_id: ${commissResult.count} rows converted`); // Mark all Clerk users as email-verified (they already verified via Clerk) await sql` UPDATE users SET email_verified = true WHERE clerk_id IS NOT NULL AND email_verified = false `; } async function importClerkOAuthAccounts(sql, secretKey) { // Migrate OAuth accounts from Clerk into BetterAuth's accounts table. // Skip entirely if any OAuth accounts already exist to avoid duplicates. // We check only OAuth providers (not 'credential') so running migrate-clerk-passwords.mjs // first doesn't prevent OAuth accounts from being created. const [{ count: existingCount }] = await sql` SELECT COUNT(*)::int AS count FROM accounts WHERE provider_id IN ('google', 'discord') `; if (existingCount > 0) { console.log(` ${existingCount} OAuth accounts already exist — skipping Clerk account import`); return; } // Fetch all users with Clerk IDs from our DB const dbUsers = await sql` SELECT id, clerk_id FROM users WHERE clerk_id IS NOT NULL `; if (dbUsers.length === 0) { console.log(" No Clerk users found in DB — nothing to migrate"); return; } console.log(` Found ${dbUsers.length} Clerk users in DB, fetching from Clerk API...`); const dbUserByClerkId = new Map(dbUsers.map((u) => [u.clerk_id, u.id])); // Fetch all users from Clerk API (paginated) const clerkUsers = []; let offset = 0; const limit = 100; while (true) { const res = await fetch( `https://api.clerk.com/v1/users?limit=${limit}&offset=${offset}`, { headers: { Authorization: `Bearer ${secretKey}` } } ); if (!res.ok) { throw new Error(`Clerk API error ${res.status}: ${await res.text()}`); } const batch = await res.json(); clerkUsers.push(...batch); if (batch.length < limit) break; offset += limit; } console.log(` Fetched ${clerkUsers.length} users from Clerk API`); let googleCount = 0; let discordCount = 0; let emailPasswordCount = 0; let skippedCount = 0; for (const clerkUser of clerkUsers) { const userId = dbUserByClerkId.get(clerkUser.id); if (!userId) { skippedCount++; continue; } for (const ext of clerkUser.external_accounts ?? []) { if (ext.provider === "google") { await sql` INSERT INTO accounts (id, account_id, provider_id, user_id, created_at, updated_at) SELECT gen_random_uuid()::text, ${ext.provider_user_id}, 'google', ${userId}::uuid, now(), now() WHERE NOT EXISTS ( SELECT 1 FROM accounts WHERE user_id = ${userId}::uuid AND provider_id = 'google' ) `; googleCount++; } if (ext.provider === "discord") { await sql` INSERT INTO accounts (id, account_id, provider_id, user_id, created_at, updated_at) SELECT gen_random_uuid()::text, ${ext.provider_user_id}, 'discord', ${userId}::uuid, now(), now() WHERE NOT EXISTS ( SELECT 1 FROM accounts WHERE user_id = ${userId}::uuid AND provider_id = 'discord' ) `; discordCount++; } } if (clerkUser.password_enabled) { emailPasswordCount++; } } console.log( ` Accounts created: ${googleCount} Google, ${discordCount} Discord` ); if (emailPasswordCount > 0) { console.log( ` ${emailPasswordCount} email/password users — they must use "Forgot Password" on first login` ); } if (skippedCount > 0) { console.log(` Skipped ${skippedCount} Clerk users with no matching DB row`); } }