Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/module/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,86 @@ interface BuildDatabaseCodeInput {

export function buildDatabaseCode(input: BuildDatabaseCodeInput): string {
if (input.provider === 'nuxthub') {
if (input.hubDialect === 'postgresql') {
return `import { db } from '@nuxthub/db'
import * as schema from './schema.${input.hubDialect}.mjs'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/postgres-js'
import { useNitroApp } from 'nitropack/runtime'
import postgres from 'postgres'

const dialect = 'pg'
const requestDatabaseKey = Symbol.for('nuxt-better-auth.requestDatabase')
const fallbackRequestDatabaseContext = new WeakMap()

function getRequestDatabaseContext(event) {
const eventWithContext = event
if (eventWithContext?.context && typeof eventWithContext.context === 'object')
return eventWithContext.context

let context = fallbackRequestDatabaseContext.get(event)
if (!context) {
context = {}
fallbackRequestDatabaseContext.set(event, context)
}
return context
}

function createHyperdriveAdapter(client) {
return drizzleAdapter(drizzle({ client, schema }), { provider: dialect, schema, usePlural: ${input.usePlural}, camelCase: ${input.camelCase} })
}

function registerClientCleanup(event, client) {
const nitroApp = useNitroApp()
let unregister
unregister = nitroApp.hooks.hook('afterResponse', (responseEvent) => {
if (responseEvent !== event)
return

unregister?.()

const close = client.end({ timeout: 0 }).catch(() => {})
if (responseEvent.waitUntil)
responseEvent.waitUntil(close)
else
void close
})
}

export function createDatabase(event) {
const hyperdrive = process.env.POSTGRES || globalThis.__env__?.POSTGRES || globalThis.POSTGRES
if (!hyperdrive?.connectionString)
return drizzleAdapter(db, { provider: dialect, schema, usePlural: ${input.usePlural}, camelCase: ${input.camelCase} })

if (event) {
const context = getRequestDatabaseContext(event)
const cached = context[requestDatabaseKey]
if (cached)
return cached

const client = postgres(hyperdrive.connectionString, {
prepare: false,
onnotice: () => {},
max: 1,
})
const database = createHyperdriveAdapter(client)

context[requestDatabaseKey] = database
registerClientCleanup(event, client)
return database
}

const client = postgres(hyperdrive.connectionString, {
prepare: false,
onnotice: () => {},
max: 1,
})

return createHyperdriveAdapter(client)
}
export { db }`
}

return `import { db } from '@nuxthub/db'
import * as schema from './schema.${input.hubDialect}.mjs'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
Expand Down
2 changes: 1 addition & 1 deletion src/module/type-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ declare module '#auth/secondary-storage' {
getContents: () => `
declare module '#auth/database' {
import type { BetterAuthOptions } from 'better-auth'
export function createDatabase(): BetterAuthOptions['database']
export function createDatabase(event?: import('h3').H3Event): BetterAuthOptions['database']
export const db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'undefined'}
}
`,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/api/_better-auth/config.get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRuntimeConfig } from '#imports'
import { defineEventHandler } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
import { serverAuth } from '../../utils/auth'

export default defineEventHandler(async (event) => {
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/middleware/route-access.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AuthMeta, AuthMode, AuthRouteRules } from '../../types'
import { getRouteRules } from '#imports'
import { createError, defineEventHandler, getRequestURL } from 'h3'
import { getRouteRules } from 'nitropack/runtime'
import { matchesUser } from '../../utils/match-user'
import { getUserSession, requireUserSession } from '../utils/session'

Expand Down
73 changes: 57 additions & 16 deletions src/runtime/server/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
import type { BetterAuthOptions } from 'better-auth'
import type { H3Event } from 'h3'
// @ts-expect-error Nuxt generates this virtual module in app builds.
import { createDatabase, db } from '#auth/database'
// @ts-expect-error Nuxt generates this virtual module in app builds.
import { createSecondaryStorage } from '#auth/secondary-storage'
import createServerAuth from '#auth/server'
import { useRuntimeConfig } from '#imports'
import { betterAuth } from 'better-auth'
import { getRequestHost, getRequestProtocol } from 'h3'
import { useRuntimeConfig } from 'nitropack/runtime'
import { withoutProtocol } from 'ufo'
import { resolveCustomSecondaryStorageRequirement } from './custom-secondary-storage'

type AuthOptions = ReturnType<typeof createServerAuth>
type AuthInstance = ReturnType<typeof betterAuth<AuthOptions>>
type UserAuthConfig = AuthOptions & {
trustedOrigins?: BetterAuthOptions['trustedOrigins']
secondaryStorage?: BetterAuthOptions['secondaryStorage']
}
type ResolvedAuthOptions = UserAuthConfig & {
secret: string
baseURL: string
trustedOrigins?: BetterAuthOptions['trustedOrigins']
database?: BetterAuthOptions['database']
}
type AuthInstance = ReturnType<typeof betterAuth<ResolvedAuthOptions>>

const _authCache = new Map<string, AuthInstance>()
const requestAuthKey = Symbol.for('nuxt-better-auth.requestAuth')
let _baseURLInferenceLogged = false
let _customSecondaryStorageMisconfigWarned = false

interface RequestAuthContext {
[requestAuthKey]?: AuthInstance
}

const fallbackRequestAuthContext = new WeakMap<object, RequestAuthContext>()

function getRequestAuthContext(event: H3Event): RequestAuthContext {
const eventWithContext = event as H3Event & { context?: unknown }
if (eventWithContext.context && typeof eventWithContext.context === 'object')
return eventWithContext.context as RequestAuthContext

let context = fallbackRequestAuthContext.get(event as object)
if (!context) {
context = {}
fallbackRequestAuthContext.set(event as object, context)
}
return context
}

function normalizeLoopbackOrigin(origin: string): string {
if (!import.meta.dev)
return origin
Expand Down Expand Up @@ -251,15 +279,13 @@ export function serverAuth(event?: H3Event): AuthInstance {
const siteUrl = getBaseURL(event)
const hasExplicitSiteUrl = runtimeConfig.public.siteUrl && typeof runtimeConfig.public.siteUrl === 'string'
const cacheKey = hasExplicitSiteUrl ? '__explicit__' : siteUrl
const requestContext = event ? getRequestAuthContext(event) : undefined

const cached = _authCache.get(cacheKey)
if (cached)
return cached
if (requestContext?.[requestAuthKey])
return requestContext[requestAuthKey]

const database = createDatabase()
const userConfig = createServerAuth({ runtimeConfig, db }) as BetterAuthOptions & {
secondaryStorage?: BetterAuthOptions['secondaryStorage']
}
const database = createDatabase(event)
const userConfig = createServerAuth({ runtimeConfig, db }) as UserAuthConfig
const trustedOrigins = withDevTrustedOrigins(userConfig.trustedOrigins, Boolean(hasExplicitSiteUrl))

const hubSecondaryStorage = (runtimeConfig.auth as { hubSecondaryStorage?: boolean | 'custom' })?.hubSecondaryStorage
Expand All @@ -271,15 +297,30 @@ export function serverAuth(event?: H3Event): AuthInstance {
console.warn(customSecondaryStorage.message)
}

const auth = betterAuth({
if (!database) {
const cached = _authCache.get(cacheKey)
if (cached) {
if (requestContext)
requestContext[requestAuthKey] = cached
return cached
}
}

const authOptions: ResolvedAuthOptions = {
...userConfig,
...(database && { database }),
...(hubSecondaryStorage === true && { secondaryStorage: createSecondaryStorage() }),
...(database ? { database } : {}),
...(hubSecondaryStorage === true ? { secondaryStorage: createSecondaryStorage() } : {}),
secret: runtimeConfig.betterAuthSecret,
baseURL: siteUrl,
trustedOrigins,
})
}
const auth = betterAuth(authOptions)

if (requestContext)
requestContext[requestAuthKey] = auth

if (!database)
_authCache.set(cacheKey, auth)

_authCache.set(cacheKey, auth)
return auth
}
25 changes: 5 additions & 20 deletions test/cases/plugins-type-inference/tsconfig.type-check.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,11 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext",
"DOM"
],
"baseUrl": ".",
"module": "preserve",
"moduleResolution": "bundler",
"paths": {
"#auth/client": ["./app/auth.config"],
"#auth/server": ["./server/auth.config"],
"#nuxt-better-auth": ["../../../src/runtime/types/augment"]
},
"types": [],
"strict": true,
"noEmit": true,
"skipLibCheck": true
"noEmit": true
},
"files": [
"./.nuxt/types/nuxt-better-auth-infer.d.ts",
"./.nuxt/types/nuxt-better-auth-nitro.d.ts",
"include": [
"./.nuxt/nuxt.d.ts",
"./virtual-modules.d.ts",
"./typecheck-target.ts"
]
}
8 changes: 8 additions & 0 deletions test/cases/plugins-type-inference/virtual-modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare module '#auth/database' {
export const db: undefined
export function createDatabase(...args: any[]): undefined
}

declare module '#auth/secondary-storage' {
export function createSecondaryStorage(...args: any[]): undefined
}
36 changes: 36 additions & 0 deletions test/nuxthub-hyperdrive-database-template.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest'
import { buildDatabaseCode } from '../src/module/templates'

describe('buildDatabaseCode', () => {
it('uses request-scoped hyperdrive clients with cleanup for nuxthub postgresql', () => {
const code = buildDatabaseCode({
provider: 'nuxthub',
hubDialect: 'postgresql',
usePlural: false,
camelCase: true,
})

expect(code).toContain('import { useNitroApp } from \'nitropack/runtime\'')
expect(code).toContain('const requestDatabaseKey = Symbol.for(\'nuxt-better-auth.requestDatabase\')')
expect(code).toContain('hook(\'afterResponse\'')
expect(code).toContain('client.end({ timeout: 0 })')
expect(code).toContain('prepare: false')
expect(code).toContain('max: 1')
expect(code).toContain('export function createDatabase(event)')
expect(code).not.toContain('function resolveBetterAuthDb()')
})

it('keeps the existing generated adapter path for non-postgresql nuxthub databases', () => {
const code = buildDatabaseCode({
provider: 'nuxthub',
hubDialect: 'sqlite',
usePlural: false,
camelCase: true,
})

expect(code).toContain('import { db } from \'@nuxthub/db\'')
expect(code).toContain('drizzleAdapter(db, { provider: dialect')
expect(code).not.toContain('import postgres from \'postgres\'')
expect(code).not.toContain('hook(\'afterResponse\'')
})
})
Loading
Loading