From 97c72454598bb361ad3df1704373dff60370b73a Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Sun, 13 Sep 2026 23:47:53 +0530 Subject: [PATCH] fix(auth): align drizzle schema and drop legacy account issuer Signed-off-by: Arnab Nandy --- db/migrations/0015_relax_account_issuer.sql | 2 + doc/setup.md | 3 + scripts/migrate-account-issuer.mjs | 106 ++++++++++++++++++++ src/lib/auth-schema.ts | 9 +- tests/lib/auth-schema.test.ts | 50 +++++++++ 5 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 db/migrations/0015_relax_account_issuer.sql create mode 100644 scripts/migrate-account-issuer.mjs create mode 100644 tests/lib/auth-schema.test.ts diff --git a/db/migrations/0015_relax_account_issuer.sql b/db/migrations/0015_relax_account_issuer.sql new file mode 100644 index 0000000..bcd4a49 --- /dev/null +++ b/db/migrations/0015_relax_account_issuer.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS "account_issuer_accountId_uidx"; +ALTER TABLE "account" DROP COLUMN "issuer"; diff --git a/doc/setup.md b/doc/setup.md index b2dd9d0..4e11dc0 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -54,6 +54,7 @@ Run the SQL migrations in filename order against the Turso database: 12. `db/migrations/0012_opportunity_workflow.sql` 13. `db/migrations/0013_contribution_readiness.sql` 14. `db/migrations/0014_user_moderation.sql` +15. `db/migrations/0015_relax_account_issuer.sql` The first migration creates Better Auth's user, session, account, and verification tables. The second creates user-owned saved searches. Migration files intentionally contain structure only—never credentials or production data. @@ -84,6 +85,8 @@ The thirteenth adds the contribution-readiness preference to cloud saved searche existing records continue to include every readiness status. The fourteenth adds role, moderation, and impersonation fields to users and sessions for the Better Auth admin plugin, and promotes existing administrator records. +The fifteenth drops the legacy `account.issuer` column and unique index to align with +Better Auth 1.7.3+ runtime schema expectations while preserving provider credentials. ## GitHub OAuth diff --git a/scripts/migrate-account-issuer.mjs b/scripts/migrate-account-issuer.mjs new file mode 100644 index 0000000..bfc02cd --- /dev/null +++ b/scripts/migrate-account-issuer.mjs @@ -0,0 +1,106 @@ +import { createClient } from "@libsql/client"; +import * as fs from "fs"; + +function parseEnv(filePath) { + if (!fs.existsSync(filePath)) return {}; + const content = fs.readFileSync(filePath, "utf-8"); + const env = {}; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eqIdx = trimmed.indexOf("="); + if (eqIdx !== -1) { + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1); + } + env[key] = val; + } + } + return env; +} + +async function applyMigration(name, envFile) { + console.log(`\n========================================`); + console.log(`Applying migration 0015 to: ${name} (${envFile})`); + console.log(`========================================`); + + const env = parseEnv(envFile); + const url = env.TURSO_DATABASE_URL; + const authToken = env.TURSO_AUTH_TOKEN; + + if (!url || !authToken) { + console.error(`[SKIP] Missing credentials in ${envFile}`); + return false; + } + + const client = createClient({ url, authToken }); + + try { + const tableInfo = await client.execute("PRAGMA table_info(account)"); + const colNames = new Set(tableInfo.rows.map((r) => r.name)); + + const indexInfo = await client.execute("PRAGMA index_list(account)"); + const indexNames = new Set(indexInfo.rows.map((r) => r.name)); + + if (indexNames.has("account_issuer_accountId_uidx")) { + console.log(`Dropping index "account_issuer_accountId_uidx"...`); + await client.execute('DROP INDEX IF EXISTS "account_issuer_accountId_uidx";'); + console.log(` -> Index dropped`); + } else { + console.log(`Index "account_issuer_accountId_uidx" does not exist, skipping drop.`); + } + + if (colNames.has("issuer")) { + console.log(`Dropping column "issuer" from "account"...`); + await client.execute('ALTER TABLE "account" DROP COLUMN "issuer";'); + console.log(` -> Column dropped`); + } else { + console.log(`Column "issuer" does not exist on "account", skipping drop.`); + } + + // Verification + const updatedTableInfo = await client.execute("PRAGMA table_info(account)"); + const remainingCols = updatedTableInfo.rows.map((r) => r.name); + console.log(`Updated account columns:`, remainingCols.join(", ")); + + const updatedIndexInfo = await client.execute("PRAGMA index_list(account)"); + console.log(`Updated account indexes:`, updatedIndexInfo.rows.map((r) => r.name).join(", ")); + + const countResult = await client.execute("SELECT count(*) as count FROM account"); + console.log(`Account records preserved: ${countResult.rows[0].count}`); + + if (remainingCols.includes("issuer")) { + throw new Error(`Column "issuer" is still present on table "account"!`); + } + + return true; + } catch (err) { + console.error(`[ERROR] Failed migrating ${name}:`, err); + return false; + } +} + +async function main() { + const previewSuccess = await applyMigration("Preview Database", ".env.preview.local"); + const prodSuccess = await applyMigration("Production/Local Database", ".env.local"); + + console.log(`\n========================================`); + console.log(`Migration 0015 Summary:`); + console.log(`Preview: ${previewSuccess ? "SUCCESS" : "FAILED"}`); + console.log(`Production: ${prodSuccess ? "SUCCESS" : "FAILED"}`); + console.log(`========================================\n`); + + if (!previewSuccess || !prodSuccess) { + process.exit(1); + } +} + +main().catch((err) => { + console.error("Migration failed:", err); + process.exit(1); +}); diff --git a/src/lib/auth-schema.ts b/src/lib/auth-schema.ts index 7b1d8ad..3aec359 100644 --- a/src/lib/auth-schema.ts +++ b/src/lib/auth-schema.ts @@ -61,7 +61,6 @@ export const account = sqliteTable( "account", { id: text("id").primaryKey(), - issuer: text("issuer").notNull(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") @@ -85,13 +84,7 @@ export const account = sqliteTable( .$onUpdate(() => new Date()) .notNull(), }, - (table) => [ - uniqueIndex("account_issuer_accountId_uidx").on( - table.issuer, - table.accountId, - ), - index("account_userId_idx").on(table.userId), - ], + (table) => [index("account_userId_idx").on(table.userId)], ); export const verification = sqliteTable( diff --git a/tests/lib/auth-schema.test.ts b/tests/lib/auth-schema.test.ts new file mode 100644 index 0000000..f939eb7 --- /dev/null +++ b/tests/lib/auth-schema.test.ts @@ -0,0 +1,50 @@ +import { diffSchema, getExpectedSchema } from "@better-auth/core/db/internal"; +import { getTableColumns, is, Table } from "drizzle-orm"; +import { describe, expect, it } from "vitest"; +import * as schema from "@/lib/auth-schema"; + +function introspectDrizzleSchema(drizzleSchema: Record) { + const tables: Array<{ + name: string; + columns: Array<{ name: string; nullable: boolean; hasDefault: boolean }>; + }> = []; + + for (const [name, table] of Object.entries(drizzleSchema)) { + if (!is(table, Table)) continue; + const columns = Object.entries(getTableColumns(table)).map( + ([key, column]) => ({ + name: key, + nullable: !column.notNull, + hasDefault: + column.hasDefault || + (column as { generated?: unknown }).generated !== undefined, + }), + ); + tables.push({ name, columns }); + } + + return tables; +} + +describe("auth schema compatibility", () => { + it("does not have schema mismatch findings against Better Auth expected schema", () => { + const introspected = introspectDrizzleSchema( + schema as unknown as Record, + ); + const expected = getExpectedSchema({}); + const findings = diffSchema(expected, introspected); + + expect(findings).toEqual([]); + }); + + it("account table definition matches Better Auth without legacy issuer column", () => { + const columns = getTableColumns(schema.account); + const columnKeys = Object.keys(columns); + + expect(columnKeys).not.toContain("issuer"); + expect(columnKeys).toContain("id"); + expect(columnKeys).toContain("accountId"); + expect(columnKeys).toContain("providerId"); + expect(columnKeys).toContain("userId"); + }); +});