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
9 changes: 0 additions & 9 deletions playground/app/types/auth.d.ts

This file was deleted.

44 changes: 35 additions & 9 deletions src/module/type-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,57 @@ declare module '#auth/database' {
addTypeTemplate({
filename: 'types/nuxt-better-auth-infer.d.ts',
getContents: () => `
import type { InferUser, InferSession, InferPluginTypes } from 'better-auth'
import type { BetterAuthOptions, BetterAuthPlugin, InferPluginTypes, UnionToIntersection } from 'better-auth'
import type { InferFieldsOutput } from 'better-auth/db'
import type { RuntimeConfig } from 'nuxt/schema'
import type createServerAuth from '${serverConfigPath}'

type _Config = ReturnType<typeof createServerAuth>
type _RawConfig = ReturnType<typeof createServerAuth>
type _RawPlugins = _RawConfig extends { plugins: infer P } ? P : _RawConfig extends { plugins?: infer P } ? P : []
type _NormalizedPlugins = _RawPlugins extends readonly (infer T)[]
? Array<T & BetterAuthPlugin>
: _RawPlugins extends (infer T)[]
? Array<T & BetterAuthPlugin>
: BetterAuthPlugin[]
type _Config = Omit<BetterAuthOptions, 'plugins'> & Omit<_RawConfig, 'plugins'> & {
plugins?: _NormalizedPlugins
}

type _InferModelFieldsFromPlugins<P, M extends string> = P extends readonly (infer Plugin)[]
? UnionToIntersection<Plugin extends { schema: { [K in M]: { fields: infer F } } } ? InferFieldsOutput<F> : {}>
: P extends (infer Plugin)[]
? UnionToIntersection<Plugin extends { schema: { [K in M]: { fields: infer F } } } ? InferFieldsOutput<F> : {}>
: {}

type _InferModelFieldsFromOptions<C, M extends 'user' | 'session'> = C extends { [K in M]: { additionalFields: infer F } }
? InferFieldsOutput<F>
: {}

type _UserFallback = _InferModelFieldsFromPlugins<_RawPlugins, 'user'> & _InferModelFieldsFromOptions<_RawConfig, 'user'>
type _SessionFallback = _InferModelFieldsFromPlugins<_RawPlugins, 'session'> & _InferModelFieldsFromOptions<_RawConfig, 'session'>

declare module '#nuxt-better-auth' {
interface AuthUser extends InferUser<_Config> {}
interface AuthSession extends InferSession<_Config> {}
interface AuthUser extends _UserFallback {}
interface AuthSession extends _SessionFallback {}
interface ServerAuthContext {
runtimeConfig: RuntimeConfig
${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : ''}
db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'unknown'}
}
type PluginTypes = InferPluginTypes<_Config>
}

interface _AugmentedServerAuthContext {
runtimeConfig: RuntimeConfig
${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : 'db: unknown'}
db: ${hasHubDb ? `typeof import('@nuxthub/db')['db']` : 'unknown'}
}

declare module '@onmax/nuxt-better-auth/config' {
import type { BetterAuthOptions } from 'better-auth'
type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>
export function defineServerAuth<T extends ServerAuthConfig>(config: T | ((ctx: _AugmentedServerAuthContext) => T)): (ctx: _AugmentedServerAuthContext) => T
import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'
type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'> & {
plugins?: readonly BetterAuthPlugin[]
}
export function defineServerAuth<const R extends ServerAuthConfig>(config: R): (ctx: _AugmentedServerAuthContext) => R
export function defineServerAuth<const R extends ServerAuthConfig>(config: (ctx: _AugmentedServerAuthContext) => R): (ctx: _AugmentedServerAuthContext) => R
}
`,
}, { nuxt: true, nitro: true, node: true })
Expand Down
8 changes: 6 additions & 2 deletions src/runtime/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BetterAuthOptions } from 'better-auth'
import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'
import type { BetterAuthClientOptions } from 'better-auth/client'
import type { DatabaseProvider } from '../database-provider'
import type { CasingOption } from '../schema-generator'
Expand All @@ -12,7 +12,9 @@ export interface ClientAuthContext {
siteUrl: string
}

export type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>
export type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'> & {
plugins?: readonly BetterAuthPlugin[]
}
export type ClientAuthConfig = Omit<BetterAuthClientOptions, 'baseURL'> & { baseURL?: string }

export type ServerAuthConfigFn = (ctx: ServerAuthContext) => ServerAuthConfig
Expand Down Expand Up @@ -72,6 +74,8 @@ export interface AuthPrivateRuntimeConfig {
secondaryStorage: boolean
}

export function defineServerAuth<const R extends ServerAuthConfig>(config: R): (ctx: ServerAuthContext) => R
export function defineServerAuth<const R extends ServerAuthConfig>(config: (ctx: ServerAuthContext) => R): (ctx: ServerAuthContext) => R
export function defineServerAuth<T extends ServerAuthConfig>(config: T | ((ctx: ServerAuthContext) => T)): (ctx: ServerAuthContext) => T {
return typeof config === 'function' ? config : () => config
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions test/cases/plugins-type-inference/app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>type infer 107</div>
</template>
3 changes: 3 additions & 0 deletions test/cases/plugins-type-inference/app/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineClientAuth } from '../../../../src/runtime/config'

export default defineClientAuth({})
10 changes: 10 additions & 0 deletions test/cases/plugins-type-inference/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default defineNuxtConfig({
modules: ['../../../src/module'],
runtimeConfig: {
betterAuthSecret: 'test-secret-for-testing-only-32chars!',
public: { siteUrl: 'http://localhost:3000' },
},
routeRules: {
'/admin/**': { auth: { user: { role: 'admin', internalCode: 'x' } } },
},
})
37 changes: 37 additions & 0 deletions test/cases/plugins-type-inference/server/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineServerAuth } from '../../../../src/runtime/config'

function customAdminLikePlugin() {
return {
id: 'custom-admin-like',
$ERROR_CODES: {
BROKEN: {
code: 'BROKEN',
message: 'Broken',
},
},
schema: {
user: {
fields: {
role: {
type: 'string',
required: false,
input: false,
},
},
},
},
} as const
}

export default defineServerAuth({
emailAndPassword: { enabled: true },
plugins: [customAdminLikePlugin()],
user: {
additionalFields: {
internalCode: {
type: 'string',
required: false,
},
},
},
})
3 changes: 3 additions & 0 deletions test/cases/plugins-type-inference/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}
25 changes: 25 additions & 0 deletions test/cases/plugins-type-inference/tsconfig.type-check.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"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
},
"files": [
"./.nuxt/types/nuxt-better-auth-infer.d.ts",
"./typecheck-target.ts"
]
}
14 changes: 14 additions & 0 deletions test/cases/plugins-type-inference/typecheck-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { AuthUser } from '#nuxt-better-auth'

const user: AuthUser = {
id: '1',
createdAt: new Date(),
updatedAt: new Date(),
email: 'a@b.c',
emailVerified: false,
name: 'n',
role: 'admin',
internalCode: 'x',
}

void user
1 change: 1 addition & 0 deletions test/cases/without-nuxthub/.nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
setups.@onmax/nuxt-better-auth="0.0.2-alpha.21"
32 changes: 32 additions & 0 deletions test/infer-plugins-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

const fixtureDir = fileURLToPath(new URL('./cases/plugins-type-inference', import.meta.url))
const env = {
...process.env,
BETTER_AUTH_SECRET: 'test-secret-for-testing-only-32chars',
}

describe('type inference regression #107', () => {
it('typechecks routeRules user fields inferred from plugins/additionalFields', () => {
const prepare = spawnSync('npx', ['nuxi', 'prepare'], {
cwd: fixtureDir,
env,
encoding: 'utf8',
})
expect(prepare.status, `nuxi prepare failed:\n${prepare.stdout}\n${prepare.stderr}`).toBe(0)

const typecheck = spawnSync('npx', ['vue-tsc', '--noEmit', '--pretty', 'false', '-p', 'tsconfig.type-check.json'], {
cwd: fixtureDir,
env,
encoding: 'utf8',
})
expect(typecheck.status, `vue-tsc failed:\n${typecheck.stdout}\n${typecheck.stderr}`).toBe(0)
const output = `${typecheck.stdout}\n${typecheck.stderr}`

expect(output).not.toContain(`is not assignable to type 'BetterAuthPlugin'`)
expect(output).not.toContain(`'role' does not exist in type`)
expect(output).not.toContain(`'internalCode' does not exist in type`)
}, 60_000)
})
2 changes: 1 addition & 1 deletion test/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'

describe('nuxt-better-auth module', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
rootDir: fileURLToPath(new URL('./cases/core-auth', import.meta.url)),
})

describe('page rendering', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/no-db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'

describe('no-db mode (NuxtHub without database)', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/no-db', import.meta.url)),
rootDir: fileURLToPath(new URL('./cases/database-less', import.meta.url)),
})

it('renders home page without database', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/no-hub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'

describe('no-hub mode (without NuxtHub)', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/no-hub', import.meta.url)),
rootDir: fileURLToPath(new URL('./cases/without-nuxthub', import.meta.url)),
})

it('renders home page without NuxtHub', async () => {
Expand Down
7 changes: 7 additions & 0 deletions test/schema-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ describe('loadUserAuthConfig', () => {
const result = await loadUserAuthConfig(configPath, false)
expect(result).toEqual({ appName: 'Test', plugins: [] })
})

it('accepts readonly plugin tuples in object syntax defineServerAuth', async () => {
const configPath = join(TEST_DIR, 'readonly-object-config.ts')
writeFileSync(configPath, `const plugin = { id: 'test-plugin', schema: { user: { fields: {} } } } as const\nexport default defineServerAuth({ appName: 'Readonly', plugins: [plugin] as const })`)
const result = await loadUserAuthConfig(configPath, false)
expect(result).toEqual({ appName: 'Readonly', plugins: [{ id: 'test-plugin', schema: { user: { fields: {} } } }] })
})
})

describe('defineServerAuth', () => {
Expand Down
Loading