diff --git a/.changeset/lazy-drizzle-metadata.md b/.changeset/lazy-drizzle-metadata.md new file mode 100644 index 0000000000..8ee1a15703 --- /dev/null +++ b/.changeset/lazy-drizzle-metadata.md @@ -0,0 +1,5 @@ +--- +"@better-auth/drizzle-adapter": patch +--- + +Defer Drizzle relation metadata access until a relational query runs, preserving lazy database initialization during application builds. diff --git a/packages/drizzle-adapter/src/drizzle-adapter.test.ts b/packages/drizzle-adapter/src/drizzle-adapter.test.ts index 13ab40d4b5..33539b314f 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.test.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.test.ts @@ -7,7 +7,9 @@ import { timestamp, } from "drizzle-orm/pg-core"; import { describe, expect, it, vi } from "vitest"; +import type { DB } from "./drizzle-adapter"; import { drizzleAdapter } from "./drizzle-adapter"; +import { drizzleAdapter as drizzleRelationsV2Adapter } from "./relations-v2"; describe("drizzle-adapter", () => { it("should create drizzle adapter", () => { @@ -23,6 +25,94 @@ describe("drizzle-adapter", () => { expect(adapter).toBeDefined(); }); + /** + * @see https://github.com/better-auth/better-auth/issues/11258 + */ + describe("lazy database initialization", () => { + const adapterFactories = [ + { + relations: "Relations v1", + createAdapter: drizzleAdapter, + relationMetadata: { schema: {} }, + }, + { + relations: "Relations v2", + createAdapter: drizzleRelationsV2Adapter, + relationMetadata: { relations: {} }, + }, + ] as const; + + it.for( + adapterFactories, + )("should not initialize a lazy database during $relations adapter construction", ({ + createAdapter, + }) => { + const db = new Proxy({} as DB, { + get() { + throw new Error("DATABASE_URL is not set"); + }, + }); + + expect(() => + createAdapter(db, { provider: "pg" })({ + secret: "test-secret-that-is-at-least-32-chars-long!!", + }), + ).not.toThrow(); + }); + + it.for( + adapterFactories, + )("should support a first $relations joined read inside a transaction", async ({ + createAdapter, + relationMetadata, + }) => { + const findFirst = vi.fn().mockResolvedValue(null); + const transactionDatabase = new Proxy( + { + query: { user: { findFirst } }, + } as DB, + { + get(target, property, receiver) { + if (property === "_") { + throw new Error("Transaction metadata should not be read"); + } + return Reflect.get(target, property, receiver); + }, + }, + ); + const database = { + _: relationMetadata, + transaction: (callback: (transactionDatabase: DB) => unknown) => + callback(transactionDatabase), + } as DB; + const user = pgTable("user", { id: text("id") }); + const adapter = createAdapter(database, { + provider: "pg", + schema: { user }, + transaction: true, + })({ + advanced: { + database: { joins: true, validateSchema: false }, + }, + }); + + if (!adapter.transaction) { + throw new Error("Transaction support should be enabled"); + } + + await expect( + adapter.transaction((transactionAdapter) => + transactionAdapter.findOne({ + model: "user", + where: [], + join: { session: true }, + }), + ), + ).resolves.toBeNull(); + expect(findFirst).toHaveBeenCalledOnce(); + }); + }); + it("should use unique column fallback for MySQL creates without an id", async () => { const userRow = { id: 42, diff --git a/packages/drizzle-adapter/src/drizzle-adapter.ts b/packages/drizzle-adapter/src/drizzle-adapter.ts index f86446ba81..5e928371d4 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.ts @@ -36,6 +36,7 @@ import { or, sql, } from "drizzle-orm"; +import type { RelationKeysByModel } from "./join-relation-key"; import { buildRelationKeysByModel, getOneToOneRelationKey, @@ -184,7 +185,9 @@ export interface DrizzleAdapterConfig { export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { let lazyOptions: BetterAuthOptions | null = null; let mysqlNoIdWarned = false; - const relationKeysByModel = buildRelationKeysByModel(db._?.schema); + let relationKeysByModel: RelationKeysByModel | undefined; + const getRelationKeysByModel = () => + (relationKeysByModel ??= buildRelationKeysByModel(db._?.schema)); const createCustomAdapter = (db: DB, inTransaction = false): AdapterFactoryCustomizeAdapterCreator => ({ @@ -803,7 +806,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { | undefined; const renamedJoinResults: { key: string; target: string }[] = []; - const relationKeys = relationKeysByModel.get(queryModel); + const relationKeys = getRelationKeysByModel().get(queryModel); includes = {}; const joinEntries = Object.entries(join); for (const [joinModel, joinAttr] of joinEntries) { @@ -890,7 +893,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { | undefined; const renamedJoinResults: { key: string; target: string }[] = []; - const relationKeys = relationKeysByModel.get(queryModel); + const relationKeys = getRelationKeysByModel().get(queryModel); includes = {}; const joinEntries = Object.entries(join); for (const [joinModel, joinAttr] of joinEntries) { diff --git a/packages/drizzle-adapter/src/relations-v2/index.ts b/packages/drizzle-adapter/src/relations-v2/index.ts index f279a4cc5d..1c4a531cbb 100644 --- a/packages/drizzle-adapter/src/relations-v2/index.ts +++ b/packages/drizzle-adapter/src/relations-v2/index.ts @@ -33,6 +33,7 @@ import { or, sql, } from "drizzle-orm"; +import type { RelationKeysByModel } from "../join-relation-key"; import { buildRelationKeysByModel, getOneToOneRelationKey, @@ -274,7 +275,9 @@ export interface DrizzleAdapterConfig { export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { let lazyOptions: BetterAuthOptions | null = null; let mysqlNoIdWarned = false; - const relationKeysByModel = buildRelationKeysByModel(db._?.relations); + let relationKeysByModel: RelationKeysByModel | undefined; + const getRelationKeysByModel = () => + (relationKeysByModel ??= buildRelationKeysByModel(db._?.relations)); const createCustomAdapter = (db: DB, inTransaction = false): AdapterFactoryCustomizeAdapterCreator => ({ getFieldName, getDefaultModelName, options, schema: baSchema }) => { @@ -724,7 +727,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { | undefined; const renamedJoinResults: { key: string; target: string }[] = []; - const relationKeys = relationKeysByModel.get(queryModel); + const relationKeys = getRelationKeysByModel().get(queryModel); includes = {}; const joinEntries = Object.entries(join); for (const [joinModel, joinAttr] of joinEntries) { @@ -812,7 +815,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { | undefined; const renamedJoinResults: { key: string; target: string }[] = []; - const relationKeys = relationKeysByModel.get(queryModel); + const relationKeys = getRelationKeysByModel().get(queryModel); includes = {}; const joinEntries = Object.entries(join); for (const [joinModel, joinAttr] of joinEntries) {