mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
refactor(vue): make Nuxt session fetching type-safe (#11085)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Provide type-safe Nuxt `useFetch` integration for the Vue client's `useSession` hook.
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { UseFetchOptions } from "nuxt/app";
|
||||
import type { authClient } from "../../lib/auth-client";
|
||||
|
||||
type SessionFetch = Parameters<typeof authClient.useSession>[0];
|
||||
type SessionFetchOptions = Parameters<SessionFetch>[1];
|
||||
|
||||
type NuxtCompatibleOptions<Options extends UseFetchOptions<unknown>> = Options;
|
||||
type NuxtCompatibleFetch<Fetch extends SessionFetch> = Fetch;
|
||||
|
||||
export type SessionFetchOptionsContract =
|
||||
NuxtCompatibleOptions<SessionFetchOptions>;
|
||||
export type NuxtUseFetchContract = NuxtCompatibleFetch<typeof useFetch>;
|
||||
@@ -26,23 +26,64 @@ it("should call '/api/auth' for vue client", async () => {
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/5358
|
||||
*/
|
||||
it("uses stable Nuxt options for a session fetch", async () => {
|
||||
const headers = { cookie: "better-auth.session_token=session" };
|
||||
const useFetch = vi.fn(async () => ({
|
||||
data: { value: null },
|
||||
error: { value: null },
|
||||
}));
|
||||
it("uses stable Nuxt options for a session fetch", () => {
|
||||
const headers = {
|
||||
cookie: "better-auth.session_token=session",
|
||||
"x-optional": undefined,
|
||||
};
|
||||
const pendingFetch = new Promise<never>(() => undefined);
|
||||
const useFetch = vi.fn(() => pendingFetch);
|
||||
const client = createVueClient({
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: { headers },
|
||||
});
|
||||
|
||||
await client.useSession(useFetch);
|
||||
void client.useSession(useFetch);
|
||||
|
||||
expect(useFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:3000/api/auth/get-session",
|
||||
{
|
||||
headers,
|
||||
headers: { cookie: "better-auth.session_token=session" },
|
||||
key: "better-auth:session:http://localhost:3000:/api/auth",
|
||||
watch: [expect.anything()],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/5358
|
||||
*/
|
||||
it.each([
|
||||
[
|
||||
"Headers",
|
||||
new Headers({ cookie: "better-auth.session_token=session" }),
|
||||
new Headers({ cookie: "better-auth.session_token=session" }),
|
||||
],
|
||||
[
|
||||
"tuple array",
|
||||
[["cookie", "better-auth.session_token=session"]] satisfies [
|
||||
string,
|
||||
string,
|
||||
][],
|
||||
[["cookie", "better-auth.session_token=session"]] satisfies [
|
||||
string,
|
||||
string,
|
||||
][],
|
||||
],
|
||||
])("preserves %s for a Nuxt session fetch", (_name, headers, expectedHeaders) => {
|
||||
const pendingFetch = new Promise<never>(() => undefined);
|
||||
const useFetch = vi.fn(() => pendingFetch);
|
||||
const client = createVueClient({
|
||||
baseURL: "http://localhost:3000",
|
||||
fetchOptions: { headers },
|
||||
});
|
||||
|
||||
void client.useSession(useFetch);
|
||||
|
||||
expect(useFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:3000/api/auth/get-session",
|
||||
{
|
||||
headers: expectedHeaders,
|
||||
key: "better-auth:session:http://localhost:3000:/api/auth",
|
||||
watch: [expect.anything()],
|
||||
},
|
||||
|
||||
@@ -191,6 +191,7 @@ export const getClientConfig = (
|
||||
pluginPathMethods,
|
||||
atomListeners,
|
||||
hydrateSession,
|
||||
$sessionSignal,
|
||||
$fetch,
|
||||
$store,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { BetterAuthClientOptions } from "@better-auth/core";
|
||||
import type {
|
||||
BetterAuthClientOptions,
|
||||
ClientFetchOption,
|
||||
} from "@better-auth/core";
|
||||
import type { BASE_ERROR_CODES } from "@better-auth/core/error";
|
||||
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import type { DeepReadonly, Ref } from "vue";
|
||||
import type { DeepReadonly, Ref, WatchSource } from "vue";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import { getClientConfig } from "../config";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
@@ -22,6 +25,20 @@ function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
}
|
||||
|
||||
/** Preserves standard `HeadersInit` values and removes undefined record entries. */
|
||||
function toHeadersInit(
|
||||
headers: ClientFetchOption["headers"],
|
||||
): HeadersInit | undefined {
|
||||
if (!headers) return undefined;
|
||||
if (headers instanceof Headers || Array.isArray(headers)) return headers;
|
||||
|
||||
const normalizedHeaders: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (value !== undefined) normalizedHeaders[name] = value;
|
||||
}
|
||||
return normalizedHeaders;
|
||||
}
|
||||
|
||||
type InferResolvedHooks<O extends BetterAuthClientOptions> = O extends {
|
||||
plugins: Array<infer Plugin>;
|
||||
}
|
||||
@@ -56,28 +73,41 @@ type ClientSession<Option extends BetterAuthClientOptions> =
|
||||
: Res
|
||||
: never;
|
||||
|
||||
type VueUseSession<Option extends BetterAuthClientOptions> = {
|
||||
(): DeepReadonly<
|
||||
Ref<{
|
||||
data: ClientSession<Option>;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
error: BetterFetchError | null;
|
||||
refetch: (
|
||||
queryParams?: { query?: SessionQueryParams } | undefined,
|
||||
) => Promise<void>;
|
||||
}>
|
||||
>;
|
||||
<F extends (...args: any) => any>(
|
||||
useFetch: F,
|
||||
): Promise<{
|
||||
data: Ref<ClientSession<Option>>;
|
||||
isPending: false;
|
||||
error: Ref<{
|
||||
message?: string | undefined;
|
||||
status: number;
|
||||
statusText: string;
|
||||
}>;
|
||||
/**
|
||||
* Minimal Nuxt-compatible fetch contract for `useSession(useFetch)`.
|
||||
* Compatibility with Nuxt's `useFetch` and `UseFetchOptions` is checked by the Nuxt fixture type test.
|
||||
*/
|
||||
type SessionFetch = (
|
||||
url: string,
|
||||
options: {
|
||||
headers?: HeadersInit;
|
||||
key: string;
|
||||
watch: WatchSource<unknown>[];
|
||||
},
|
||||
) => Promise<{
|
||||
data: Ref<unknown>;
|
||||
error: Ref<unknown>;
|
||||
}>;
|
||||
|
||||
type VueSessionState<Option extends BetterAuthClientOptions> = DeepReadonly<
|
||||
Ref<{
|
||||
data: ClientSession<Option>;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
error: BetterFetchError | null;
|
||||
refetch: (
|
||||
queryParams?: { query?: SessionQueryParams } | undefined,
|
||||
) => Promise<void>;
|
||||
}>
|
||||
>;
|
||||
|
||||
type SessionFetchResult<Option extends BetterAuthClientOptions> = {
|
||||
data: Ref<ClientSession<Option>>;
|
||||
isPending: false;
|
||||
error: Ref<{
|
||||
message?: string | undefined;
|
||||
status: number;
|
||||
statusText: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -89,7 +119,8 @@ export type VueAuthClient<Option extends BetterAuthClientOptions> =
|
||||
InferClientAPI<Option> &
|
||||
InferActions<Option> & {
|
||||
hydrateSession(session: NonNullable<ClientSession<Option>> | null): void;
|
||||
useSession: VueUseSession<Option>;
|
||||
useSession(): VueSessionState<Option>;
|
||||
useSession(useFetch: SessionFetch): Promise<SessionFetchResult<Option>>;
|
||||
$Infer: {
|
||||
Session: NonNullable<ClientSession<Option>>;
|
||||
};
|
||||
@@ -109,6 +140,7 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
|
||||
pluginsActions,
|
||||
pluginsAtoms,
|
||||
hydrateSession,
|
||||
$sessionSignal,
|
||||
$fetch,
|
||||
$store,
|
||||
atomListeners,
|
||||
@@ -126,45 +158,29 @@ export function createAuthClient<Option extends BetterAuthClientOptions>(
|
||||
resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
||||
}
|
||||
|
||||
function useSession(): DeepReadonly<
|
||||
Ref<{
|
||||
data: ClientSession<Option>;
|
||||
isPending: boolean;
|
||||
isRefetching: boolean;
|
||||
error: BetterFetchError | null;
|
||||
refetch: (
|
||||
queryParams?: { query?: SessionQueryParams } | undefined,
|
||||
) => Promise<void>;
|
||||
}>
|
||||
>;
|
||||
function useSession<F extends (...args: any) => any>(
|
||||
useFetch: F,
|
||||
): Promise<{
|
||||
data: Ref<ClientSession<Option>>;
|
||||
isPending: false; //this is just to be consistent with the default hook
|
||||
error: Ref<{
|
||||
message?: string | undefined;
|
||||
status: number;
|
||||
statusText: string;
|
||||
}>;
|
||||
}>;
|
||||
function useSession<UseFetch extends <_T>(...args: any) => any>(
|
||||
useFetch?: UseFetch | undefined,
|
||||
) {
|
||||
function useSession(): VueSessionState<Option>;
|
||||
function useSession(
|
||||
useFetch: SessionFetch,
|
||||
): Promise<SessionFetchResult<Option>>;
|
||||
function useSession(useFetch?: SessionFetch | undefined) {
|
||||
// Passing `useFetch` opts into Nuxt-managed session fetching and hydration.
|
||||
if (useFetch) {
|
||||
const ref = useStore(pluginsAtoms.$sessionSignal!);
|
||||
const sessionSignal = useStore($sessionSignal);
|
||||
return useFetch(`${baseURL}/get-session`, {
|
||||
headers: options?.fetchOptions?.headers,
|
||||
headers: toHeadersInit(options?.fetchOptions?.headers),
|
||||
key: sessionCacheKey,
|
||||
watch: [ref],
|
||||
}).then((res: any) => {
|
||||
watch: [sessionSignal],
|
||||
}).then((result) => {
|
||||
const data = result.data as Ref<ClientSession<Option>>;
|
||||
const error = result.error as SessionFetchResult<Option>["error"];
|
||||
return {
|
||||
data: res.data,
|
||||
data,
|
||||
isPending: false,
|
||||
error: res.error,
|
||||
error,
|
||||
};
|
||||
});
|
||||
}
|
||||
// Otherwise, return Better Auth's session state as a reactive Vue ref.
|
||||
return resolvedHooks.useSession();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user