-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Better Auth - Backfill scripts #4285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2ecf48c
b6eb86a
2d66bbc
ca7fd6a
6b1d3c0
4270536
ab22b9f
c1cc03f
ff49b73
5960997
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { qstash } from "@/lib/cron"; | ||
| import { withCron } from "@/lib/cron/with-cron"; | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; | ||
| import { logAndRespond } from "../../utils"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const maxDuration = 300; | ||
|
|
||
| const BATCH_SIZE = 1000; | ||
| const ITERATIONS = 10; | ||
|
|
||
| // POST /api/cron/better-auth/migrate-accounts | ||
| export const POST = withCron(async () => { | ||
| for (let i = 0; i < ITERATIONS; i++) { | ||
| const count = await prisma.$executeRaw` | ||
| UPDATE Account | ||
| SET | ||
| accountId = COALESCE(accountId, providerAccountId), | ||
| providerId = COALESCE(providerId, provider), | ||
| accessToken = COALESCE(accessToken, access_token), | ||
| refreshToken = COALESCE(refreshToken, refresh_token), | ||
| idToken = COALESCE(idToken, id_token) | ||
| WHERE | ||
| accountId IS NULL | ||
| AND providerAccountId IS NOT NULL | ||
| ORDER BY id | ||
| LIMIT ${BATCH_SIZE} | ||
| `; | ||
|
|
||
| console.log(`Migrated ${count} accounts.`); | ||
|
|
||
| if (count === 0) { | ||
| return logAndRespond("Finished migrating accounts."); | ||
| } | ||
| } | ||
|
|
||
| const qstashResponse = await qstash.publishJSON({ | ||
| method: "POST", | ||
| url: `${APP_DOMAIN_WITH_NGROK}/api/cron/better-auth/migrate-accounts`, | ||
| delay: "10s", | ||
| retries: 0, | ||
| flowControl: { | ||
| key: "better-auth-migrate-accounts", | ||
| parallelism: 1, | ||
| }, | ||
| }); | ||
|
|
||
| return logAndRespond( | ||
| `Scheduled next batch of accounts to migrate ${qstashResponse.messageId}`, | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { qstash } from "@/lib/cron"; | ||
| import { withCron } from "@/lib/cron/with-cron"; | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; | ||
| import { logAndRespond } from "../../utils"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const maxDuration = 300; | ||
|
|
||
| const BATCH_SIZE = 1000; | ||
| const ITERATIONS = 10; | ||
|
|
||
| // POST /api/cron/better-auth/migrate-credentials | ||
| export const POST = withCron(async () => { | ||
| for (let i = 0; i < ITERATIONS; i++) { | ||
| const users = await prisma.user.findMany({ | ||
| where: { | ||
| passwordHash: { | ||
| not: null, | ||
| }, | ||
| accounts: { | ||
| none: { | ||
| providerId: "credential", | ||
| }, | ||
| }, | ||
| }, | ||
| select: { | ||
| id: true, | ||
| passwordHash: true, | ||
| }, | ||
| orderBy: { | ||
| id: "asc", | ||
| }, | ||
| take: BATCH_SIZE, | ||
| }); | ||
|
|
||
| if (users.length === 0) { | ||
| return logAndRespond("Finished migrating credentials."); | ||
| } | ||
|
|
||
| const { count } = await prisma.account.createMany({ | ||
| skipDuplicates: true, | ||
| data: users.map((user) => ({ | ||
| userId: user.id, | ||
| accountId: user.id, | ||
| providerId: "credential", | ||
| password: user.passwordHash, | ||
| })), | ||
| }); | ||
|
|
||
| console.log(`Migrated ${count} credentials.`); | ||
|
|
||
| // No rows inserted while candidates remain means the batch cannot progress. | ||
| if (count === 0) { | ||
| return logAndRespond( | ||
| `Stopped migrating credentials: ${users.length} users matched but no accounts were created.`, | ||
| { logLevel: "error" }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const qstashResponse = await qstash.publishJSON({ | ||
| method: "POST", | ||
| url: `${APP_DOMAIN_WITH_NGROK}/api/cron/better-auth/migrate-credentials`, | ||
| delay: "10s", | ||
| retries: 0, | ||
| flowControl: { | ||
| key: "better-auth-migrate-credentials", | ||
| parallelism: 1, | ||
| }, | ||
| }); | ||
|
|
||
| return logAndRespond( | ||
| `Scheduled next batch of credentials to migrate ${qstashResponse.messageId}`, | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { qstash } from "@/lib/cron"; | ||
| import { withCron } from "@/lib/cron/with-cron"; | ||
| import { prisma } from "@/lib/prisma"; | ||
| import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; | ||
| import { logAndRespond } from "../../utils"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const maxDuration = 300; | ||
|
|
||
| const BATCH_SIZE = 1000; | ||
| const ITERATIONS = 10; | ||
|
|
||
| // POST /api/cron/better-auth/migrate-users | ||
| export const POST = withCron(async () => { | ||
|
Comment on lines
+7
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win No migration route declares
Match the value to the plan limit for this deployment. 📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| for (let i = 0; i < ITERATIONS; i++) { | ||
| const { count } = await prisma.user.updateMany({ | ||
| where: { | ||
| emailVerified: { | ||
| not: null, | ||
| }, | ||
| emailVerifiedBa: false, | ||
| }, | ||
| data: { | ||
| emailVerifiedBa: true, | ||
| }, | ||
| limit: BATCH_SIZE, | ||
| }); | ||
|
|
||
| console.log(`Migrated ${count} users.`); | ||
|
|
||
| if (count === 0) { | ||
| return logAndRespond("Finished migrating users."); | ||
| } | ||
| } | ||
|
|
||
| const qstashResponse = await qstash.publishJSON({ | ||
| method: "POST", | ||
| url: `${APP_DOMAIN_WITH_NGROK}/api/cron/better-auth/migrate-users`, | ||
| delay: "10s", | ||
| retries: 0, | ||
| flowControl: { | ||
| key: "better-auth-migrate-users", | ||
| parallelism: 1, | ||
| }, | ||
| }); | ||
|
|
||
| return logAndRespond( | ||
| `Scheduled next batch of users to migrate ${qstashResponse.messageId}`, | ||
| ); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Add a progress guard. The batch can repeat forever.
createManyusesskipDuplicates: true. The returnedcountis logged but never checked. Ifcountis 0 whileusers.lengthis greater than 0, thefindManyfilter still matches the same users on the next iteration. The loop then processes the same 1000 rows 10 times and schedules another QStash message.retries: 0does not stop this, because each run publishes a new message. The chain repeats every 10 seconds without end.migrate-usersandmigrate-accountsboth exit oncount === 0. This route needs the same guard.🐛 Proposed fix
const { count } = await prisma.account.createMany({ skipDuplicates: true, data: users.map((user) => ({ userId: user.id, accountId: user.id, providerId: "credential", password: user.passwordHash, })), }); console.log(`Migrated ${count} credentials.`); + + // No rows inserted while candidates remain means the batch cannot progress. + if (count === 0) { + return logAndRespond( + `Stopped migrating credentials: ${users.length} users matched but no accounts were created.`, + true, + ); + } }Confirm the second argument of
logAndRespondmarks an error response before you apply it.🤖 Prompt for AI Agents