fix(drizzle-adapter): defer relation metadata access (#11263)

This commit is contained in:
Taesu
2026-09-12 04:02:01 +00:00
committed by GitHub
parent 5d91f5b12f
commit e18bc83172
4 changed files with 107 additions and 6 deletions
+5
View File
@@ -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.
@@ -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,
@@ -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) {
@@ -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) {