The FK conversion (teams.owner_id, commissioners.user_id → UUIDs) and email_verified update were inside runClerkMigration() which only ran when CLERK_SECRET_KEY was set. If the key wasn't in the environment at deploy time, all Clerk IDs were left unconverted, causing UUID parse errors at runtime. Split into two functions: - convertForeignKeys(): always runs, idempotent WHERE clauses - importClerkOAuthAccounts(): only runs when CLERK_SECRET_KEY is set Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
207 lines
6.6 KiB
JavaScript
207 lines
6.6 KiB
JavaScript
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`);
|
|
}
|
|
}
|