Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/content/4.integrations/1.nuxthub.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ The module analyzes your `server/auth.config.ts` at build time:
**Restart Required**: After adding/removing plugins that affect database structure, restart the dev server to regenerate the schema.
::

::note
When Nuxt resolves `buildDir` to `node_modules/.cache/nuxt/.nuxt`, the module mirrors `schema.<dialect>.ts` to root `.nuxt/better-auth/` and uses that mirrored path for NuxtHub schema extension. This avoids Node type-stripping failures for `.ts` files under `node_modules`. In normal buildDir layouts, the hook prefers `schema.<dialect>.ts` and falls back to `schema.<dialect>.mjs`.
::

## Accessing the Database

With NuxtHub, you get access to the Drizzle database instance in your server config:
Expand Down
45 changes: 35 additions & 10 deletions src/module/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ interface SchemaContext {
serverConfigPath: string
}

function isInsideNodeModules(path: string): boolean {
return path.split(/[\\/]/).includes('node_modules')
}

export function resolveHubSchemaPath(
buildDir: string,
rootDir: string,
dialect: string,
exists: (path: string) => boolean = existsSync,
): string | null {
const rootTsPath = join(rootDir, '.nuxt', 'better-auth', `schema.${dialect}.ts`)
if (isInsideNodeModules(buildDir) && exists(rootTsPath))
return rootTsPath

const tsPath = join(buildDir, 'better-auth', `schema.${dialect}.ts`)
if (exists(tsPath))
return tsPath

const mjsPath = join(buildDir, 'better-auth', `schema.${dialect}.mjs`)
if (exists(mjsPath))
return mjsPath

return null
}

async function loadAuthOptions(context: SchemaContext) {
const isProduction = !context.nuxt.options.dev
const configFile = `${context.serverConfigPath}.ts`
Expand Down Expand Up @@ -64,23 +89,23 @@ export async function setupBetterAuthSchema(
await writeFile(schemaPathTs, schemaCode)
await writeFile(schemaPathMjs, schemaCode)

if (isInsideNodeModules(nuxt.options.buildDir)) {
const rootSchemaDir = join(nuxt.options.rootDir, '.nuxt', 'better-auth')
const rootSchemaPathTs = join(rootSchemaDir, `schema.${dialect}.ts`)
await mkdir(rootSchemaDir, { recursive: true })
await writeFile(rootSchemaPathTs, schemaCode)
}

addTemplate({ filename: `better-auth/schema.${dialect}.ts`, getContents: () => schemaCode, write: true })
addTemplate({ filename: `better-auth/schema.${dialect}.mjs`, getContents: () => schemaCode, write: true })

consola.info(`Generated ${dialect} schema (.ts + .mjs)`)

const nuxtWithHubHooks = nuxt as Nuxt & { hook: (name: string, cb: (arg: { paths: string[], dialect: string }) => void) => void }
nuxtWithHubHooks.hook('hub:db:schema:extend', ({ paths, dialect: hookDialect }) => {
const tsPath = join(nuxt.options.buildDir, 'better-auth', `schema.${hookDialect}.ts`)
const mjsPath = join(nuxt.options.buildDir, 'better-auth', `schema.${hookDialect}.mjs`)

if (existsSync(tsPath)) {
paths.unshift(tsPath)
return
}

if (existsSync(mjsPath))
paths.unshift(mjsPath)
const schemaPath = resolveHubSchemaPath(nuxt.options.buildDir, nuxt.options.rootDir, hookDialect)
if (schemaPath)
paths.unshift(schemaPath)
})
}
catch (error) {
Expand Down
85 changes: 85 additions & 0 deletions test/schema-hook-path-priority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'pathe'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveHubSchemaPath } from '../src/module/schema'

const tempDirs: string[] = []

function createBuildDir() {
const dir = mkdtempSync(join(tmpdir(), 'nuxt-better-auth-schema-'))
tempDirs.push(dir)
return dir
}

afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length))
rmSync(dir, { recursive: true, force: true })
})

describe('resolveHubSchemaPath', () => {
it('prefers .ts when both .ts and .mjs exist', () => {
const buildDir = createBuildDir()
const rootDir = createBuildDir()
const schemaDir = join(buildDir, 'better-auth')
const tsPath = join(schemaDir, 'schema.sqlite.ts')
const mjsPath = join(schemaDir, 'schema.sqlite.mjs')

mkdirSync(schemaDir, { recursive: true })
writeFileSync(tsPath, 'export {}', { flag: 'w' })
writeFileSync(mjsPath, 'export {}', { flag: 'w' })

const selected = resolveHubSchemaPath(buildDir, rootDir, 'sqlite')
expect(selected).toBe(tsPath)
})

it('prefers mirrored root .nuxt schema.ts when buildDir is under node_modules', () => {
const rootDir = createBuildDir()
const buildDir = join(rootDir, 'node_modules', '.cache', 'nuxt', '.nuxt')
const buildSchemaDir = join(buildDir, 'better-auth')
const rootSchemaDir = join(rootDir, '.nuxt', 'better-auth')
const rootTsPath = join(rootSchemaDir, 'schema.sqlite.ts')
const buildMjsPath = join(buildSchemaDir, 'schema.sqlite.mjs')

mkdirSync(buildSchemaDir, { recursive: true })
mkdirSync(rootSchemaDir, { recursive: true })
writeFileSync(buildMjsPath, 'export {}', { flag: 'w' })
writeFileSync(rootTsPath, 'export {}', { flag: 'w' })

const selected = resolveHubSchemaPath(buildDir, rootDir, 'sqlite')
expect(selected).toBe(rootTsPath)
})

it('falls back to .ts when .mjs does not exist', () => {
const buildDir = createBuildDir()
const rootDir = createBuildDir()
const schemaDir = join(buildDir, 'better-auth')
const tsPath = join(schemaDir, 'schema.sqlite.ts')

mkdirSync(schemaDir, { recursive: true })
writeFileSync(tsPath, 'export {}', { flag: 'w' })

const selected = resolveHubSchemaPath(buildDir, rootDir, 'sqlite')
expect(selected).toBe(tsPath)
})

it('falls back to .mjs when .ts does not exist', () => {
const buildDir = createBuildDir()
const rootDir = createBuildDir()
const schemaDir = join(buildDir, 'better-auth')
const mjsPath = join(schemaDir, 'schema.sqlite.mjs')

mkdirSync(schemaDir, { recursive: true })
writeFileSync(mjsPath, 'export {}', { flag: 'w' })

const selected = resolveHubSchemaPath(buildDir, rootDir, 'sqlite')
expect(selected).toBe(mjsPath)
})

it('returns null when no schema files exist', () => {
const buildDir = createBuildDir()
const rootDir = createBuildDir()
const selected = resolveHubSchemaPath(buildDir, rootDir, 'sqlite')
expect(selected).toBeNull()
})
})
Loading