Skip to content

Commit e752578

Browse files
committed
fix(auth): rebuild nuxthub postgres adapters per request
1 parent f344c73 commit e752578

File tree

4 files changed

+147
-6
lines changed

4 files changed

+147
-6
lines changed

src/module/templates.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ interface BuildDatabaseCodeInput {
2323

2424
export function buildDatabaseCode(input: BuildDatabaseCodeInput): string {
2525
if (input.provider === 'nuxthub') {
26+
if (input.hubDialect === 'postgresql') {
27+
return `import { db } from '@nuxthub/db'
28+
import * as schema from './schema.${input.hubDialect}.mjs'
29+
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
30+
import { drizzle } from 'drizzle-orm/postgres-js'
31+
import postgres from 'postgres'
32+
33+
const dialect = 'pg'
34+
35+
function resolveBetterAuthDb() {
36+
const hyperdrive = process.env.POSTGRES || globalThis.__env__?.POSTGRES || globalThis.POSTGRES
37+
if (!hyperdrive?.connectionString)
38+
return db
39+
40+
const client = postgres(hyperdrive.connectionString, {
41+
prepare: false,
42+
onnotice: () => {},
43+
})
44+
45+
return drizzle({ client, schema })
46+
}
47+
48+
export function createDatabase() { return drizzleAdapter(resolveBetterAuthDb(), { provider: dialect, schema, usePlural: ${input.usePlural}, camelCase: ${input.camelCase} }) }
49+
export { db }`
50+
}
51+
2652
return `import { db } from '@nuxthub/db'
2753
import * as schema from './schema.${input.hubDialect}.mjs'
2854
import { drizzleAdapter } from 'better-auth/adapters/drizzle'

src/runtime/server/utils/auth.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,8 @@ export function serverAuth(event?: H3Event): AuthInstance {
251251
const siteUrl = getBaseURL(event)
252252
const hasExplicitSiteUrl = runtimeConfig.public.siteUrl && typeof runtimeConfig.public.siteUrl === 'string'
253253
const cacheKey = hasExplicitSiteUrl ? '__explicit__' : siteUrl
254-
255-
const cached = _authCache.get(cacheKey)
256-
if (cached)
257-
return cached
258-
259254
const database = createDatabase()
255+
260256
const userConfig = createServerAuth({ runtimeConfig, db }) as BetterAuthOptions & {
261257
secondaryStorage?: BetterAuthOptions['secondaryStorage']
262258
}
@@ -271,6 +267,12 @@ export function serverAuth(event?: H3Event): AuthInstance {
271267
console.warn(customSecondaryStorage.message)
272268
}
273269

270+
if (!database) {
271+
const cached = _authCache.get(cacheKey)
272+
if (cached)
273+
return cached
274+
}
275+
274276
const auth = betterAuth({
275277
...userConfig,
276278
...(database && { database }),
@@ -280,6 +282,8 @@ export function serverAuth(event?: H3Event): AuthInstance {
280282
trustedOrigins,
281283
})
282284

283-
_authCache.set(cacheKey, auth)
285+
if (!database)
286+
_authCache.set(cacheKey, auth)
287+
284288
return auth
285289
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { buildDatabaseCode } from '../src/module/templates'
3+
4+
describe('buildDatabaseCode', () => {
5+
it('uses a dedicated postgres-js client with prepare disabled for nuxthub postgresql', () => {
6+
const code = buildDatabaseCode({
7+
provider: 'nuxthub',
8+
hubDialect: 'postgresql',
9+
usePlural: false,
10+
camelCase: true,
11+
})
12+
13+
expect(code).toContain("import postgres from 'postgres'")
14+
expect(code).toContain('hyperdrive.connectionString')
15+
expect(code).toContain('prepare: false')
16+
expect(code).toContain('return db')
17+
expect(code).toContain('drizzleAdapter(resolveBetterAuthDb()')
18+
expect(code).not.toContain('_betterAuthDb')
19+
})
20+
21+
it('keeps the existing generated adapter path for non-postgresql nuxthub databases', () => {
22+
const code = buildDatabaseCode({
23+
provider: 'nuxthub',
24+
hubDialect: 'sqlite',
25+
usePlural: false,
26+
camelCase: true,
27+
})
28+
29+
expect(code).toContain("import { db } from '@nuxthub/db'")
30+
expect(code).toContain('drizzleAdapter(db, { provider: dialect')
31+
expect(code).not.toContain("import postgres from 'postgres'")
32+
expect(code).not.toContain('prepare: false')
33+
})
34+
})
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
const betterAuthMock = vi.fn()
4+
const createDatabaseMock = vi.fn()
5+
const createServerAuthMock = vi.fn()
6+
const useRuntimeConfigMock = vi.fn()
7+
8+
vi.mock('#auth/database', () => ({
9+
createDatabase: createDatabaseMock,
10+
db: { query: {} },
11+
}))
12+
13+
vi.mock('#auth/secondary-storage', () => ({
14+
createSecondaryStorage: vi.fn(() => undefined),
15+
}))
16+
17+
vi.mock('#auth/server', () => ({
18+
default: createServerAuthMock,
19+
}))
20+
21+
vi.mock('better-auth', () => ({
22+
betterAuth: betterAuthMock,
23+
}))
24+
25+
vi.mock('nitropack/runtime', () => ({
26+
useRuntimeConfig: useRuntimeConfigMock,
27+
}))
28+
29+
describe('serverAuth database cache behavior', () => {
30+
beforeEach(() => {
31+
vi.resetModules()
32+
vi.clearAllMocks()
33+
34+
useRuntimeConfigMock.mockReturnValue({
35+
public: {
36+
siteUrl: 'https://example.com',
37+
},
38+
auth: {},
39+
betterAuthSecret: 'test-secret',
40+
})
41+
42+
createServerAuthMock.mockReturnValue({
43+
trustedOrigins: undefined,
44+
})
45+
46+
betterAuthMock.mockImplementation((options: Record<string, unknown>) => ({
47+
options,
48+
marker: Symbol('auth-instance'),
49+
}))
50+
})
51+
52+
it('creates a fresh auth instance on each call when a database adapter is active', async () => {
53+
createDatabaseMock.mockReturnValue({ kind: 'database-adapter' })
54+
55+
const { serverAuth } = await import('../src/runtime/server/utils/auth')
56+
57+
const first = serverAuth()
58+
const second = serverAuth()
59+
60+
expect(first).not.toBe(second)
61+
expect(createDatabaseMock).toHaveBeenCalledTimes(2)
62+
expect(betterAuthMock).toHaveBeenCalledTimes(2)
63+
})
64+
65+
it('keeps caching auth instances when no database adapter is configured', async () => {
66+
createDatabaseMock.mockReturnValue(undefined)
67+
68+
const { serverAuth } = await import('../src/runtime/server/utils/auth')
69+
70+
const first = serverAuth()
71+
const second = serverAuth()
72+
73+
expect(first).toBe(second)
74+
expect(createDatabaseMock).toHaveBeenCalledTimes(2)
75+
expect(betterAuthMock).toHaveBeenCalledTimes(1)
76+
})
77+
})

0 commit comments

Comments
 (0)