feat: infered types should be avaialble from both client and server

This commit is contained in:
Bereket Engida
2024-09-12 10:15:26 +03:00
parent a1fab3a54c
commit 6916d33243
12 changed files with 124 additions and 70 deletions

View File

@@ -1,20 +0,0 @@
import Section from "@/components/landing/section";
import Hero from "@/components/landing/hero";
import Features from "@/components/features";
export default function HomePage() {
return (
<main className="h-min">
<Section
className="-z-1 mb-1"
crosses
crossesOffset="lg:translate-y-[5.25rem]"
customPaddings
id="hero"
>
<Hero />
<Features />
</Section>
</main>
);
}

View File

@@ -19,7 +19,7 @@ export const metadata = createMetadata({
metadataBase: baseUrl, metadataBase: baseUrl,
}); });
const hideNavbar = true
export default function Layout({ children }: { children: ReactNode }) { export default function Layout({ children }: { children: ReactNode }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
@@ -28,19 +28,10 @@ export default function Layout({ children }: { children: ReactNode }) {
</head> </head>
<body className={`${GeistSans.variable} ${GeistMono.variable} font-sans`}> <body className={`${GeistSans.variable} ${GeistMono.variable} font-sans`}>
<RootProvider> <RootProvider>
<div className="min-h-screen w-full dark:bg-black bg-white dark:bg-grid-small-white/[0.2] bg-grid-small-black/[0.2] relative flex justify-center ">
{/* Radial gradient for the container to give a faded look */}
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
<div className=" bg-white dark:bg-black border-b py-2 flex justify-between items-center px-4 border-border absolute z-50 w-1/2">
</div>
<NavbarProvider> <NavbarProvider>
{ <Navbar />
hideNavbar ? null : <Navbar />
}
{children} {children}
</NavbarProvider> </NavbarProvider>
</div>
</RootProvider> </RootProvider>
</body> </body>
</html> </html>

View File

@@ -1,21 +1,20 @@
import Section from "@/components/landing/section";
import Hero from "@/components/landing/hero"; import Hero from "@/components/landing/hero";
import { Button } from "@/components/ui/button"; import Features from "@/components/features";
import { BookOpen } from "lucide-react"; export default function HomePage() {
import Image from "next/image";
import Link from "next/link";
export default function Home() {
return ( return (
<div className="min-h-[90vh] flex items-center justify-center"> <main className="h-min">
<main className="flex flex-col gap-8 row-start-2 items-center justify-center"> <Section
<div className="flex flex-col gap-1"> className="-z-1 mb-1"
crosses
crossesOffset="lg:translate-y-[5.25rem]"
customPaddings
id="hero"
>
<Hero /> <Hero />
<Features />
</Section>
</div>
</main> </main>
</div>
); );
} }

View File

@@ -5,6 +5,9 @@ import { NavbarMobileBtn } from "./nav-mobile";
import { NavLink } from "./nav-link"; import { NavLink } from "./nav-link";
import { Logo } from "./logo"; import { Logo } from "./logo";
const hideNavbar = process.env.NODE_ENV === "production"
export const Navbar = () => { export const Navbar = () => {
return ( return (
<nav className="md:grid grid-cols-12 border-b sticky top-0 flex items-center justify-end bg-background backdrop-blur-md z-50"> <nav className="md:grid grid-cols-12 border-b sticky top-0 flex items-center justify-end bg-background backdrop-blur-md z-50">
@@ -19,11 +22,13 @@ export const Navbar = () => {
</Link> </Link>
<div className="md:col-span-9 lg:col-span-10 flex items-center justify-end "> <div className="md:col-span-9 lg:col-span-10 flex items-center justify-end ">
<ul className="md:flex items-center divide-x w-max border-r hidden shrink-0"> <ul className="md:flex items-center divide-x w-max border-r hidden shrink-0">
{navMenu.map((menu, i) => ( {
hideNavbar ? null : navMenu.map((menu, i) => (
<NavLink key={menu.name} href={menu.path}> <NavLink key={menu.name} href={menu.path}>
{menu.name} {menu.name}
</NavLink> </NavLink>
))} ))
}
</ul> </ul>
<ThemeToggle /> <ThemeToggle />
<NavbarMobileBtn /> <NavbarMobileBtn />

View File

@@ -14,6 +14,7 @@ export const getCSRFToken = createAuthEndpoint(
ctx.context.authCookies.csrfToken.name, ctx.context.authCookies.csrfToken.name,
ctx.context.secret, ctx.context.secret,
); );
if (csrfToken) { if (csrfToken) {
return { return {
csrfToken, csrfToken,

View File

@@ -1,7 +1,8 @@
import type { Endpoint } from "better-call"; import type { Endpoint, Prettify } from "better-call";
import { getEndpoints, router } from "./api"; import { getEndpoints, router } from "./api";
import { init } from "./init"; import { init } from "./init";
import type { BetterAuthOptions } from "./types/options"; import type { BetterAuthOptions } from "./types/options";
import type { InferSession, InferUser } from "./types";
type InferAPI<API> = Omit< type InferAPI<API> = Omit<
API, API,
@@ -38,6 +39,10 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
}, },
api: api as InferAPI<typeof api>, api: api as InferAPI<typeof api>,
options: authContext.options as O, options: authContext.options as O,
$infer: {} as {
session: Prettify<InferSession<O>>;
user: Prettify<InferUser<O>>;
},
}; };
}; };

View File

@@ -165,4 +165,32 @@ describe("type", () => {
expectTypeOf(client.setTestAtom).toEqualTypeOf<(value: boolean) => void>(); expectTypeOf(client.setTestAtom).toEqualTypeOf<(value: boolean) => void>();
expectTypeOf(client.test.signOut).toEqualTypeOf<() => Promise<void>>(); expectTypeOf(client.test.signOut).toEqualTypeOf<() => Promise<void>>();
}); });
it("should infer session", () => {
const client = createSolidClient({
plugins: [testClientPlugin(), testClientPlugin2()],
baseURL: "http://localhost:3000",
});
client.$infer.s;
const $infer = client.$infer;
expectTypeOf($infer.session).toEqualTypeOf<{
id: string;
userId: string;
expiresAt: Date;
ipAddress?: string | undefined;
userAgent?: string | undefined;
}>();
expectTypeOf($infer.user).toEqualTypeOf<{
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: Date;
updatedAt: Date;
image?: string | undefined;
testField?: string | undefined;
testField2?: number | undefined;
testField4: string;
}>();
});
}); });

View File

@@ -4,6 +4,7 @@ import { type Atom } from "nanostores";
import type { AtomListener, ClientOptions } from "./types"; import type { AtomListener, ClientOptions } from "./types";
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./fetch-plugins"; import { addCurrentURL, csrfPlugin, redirectPlugin } from "./fetch-plugins";
import type { InferSession } from "../types";
export const getClientConfig = <O extends ClientOptions>(options?: O) => { export const getClientConfig = <O extends ClientOptions>(options?: O) => {
const $fetch = createFetch({ const $fetch = createFetch({

View File

@@ -1,17 +1,28 @@
import type { BetterFetch } from "@better-fetch/fetch"; import type { BetterFetch } from "@better-fetch/fetch";
import { atom } from "nanostores"; import { atom } from "nanostores";
import type { Auth as BetterAuth } from "../auth"; import type { Auth as BetterAuth } from "../auth";
import type { Prettify } from "../types/helper"; import type { Prettify, UnionToIntersection } from "../types/helper";
import type { InferSession, InferUser } from "../types/models"; import type { InferSession, InferUser } from "../types/models";
import type { AuthClientPlugin, ClientOptions } from "./types"; import type { AuthClientPlugin, ClientOptions } from "./types";
import { useAuthQuery } from "./query"; import { useAuthQuery } from "./query";
import type { BetterAuthPlugin } from "../plugins";
export function getSessionAtom<Option extends ClientOptions>( export function getSessionAtom<Option extends ClientOptions>(
client: BetterFetch, client: BetterFetch,
) { ) {
type Plugins = Option["plugins"] extends Array<AuthClientPlugin> type Plugins = Option["plugins"] extends Array<AuthClientPlugin>
? Array<Option["plugins"][number]["$InferServerPlugin"]> ? Array<
: undefined; UnionToIntersection<
Option["plugins"] extends Array<infer Pl>
? Pl extends AuthClientPlugin
? Pl["$InferServerPlugin"] extends BetterAuthPlugin
? Pl["$InferServerPlugin"]
: never
: never
: never
>
>
: never;
type Auth = { type Auth = {
handler: any; handler: any;
@@ -22,10 +33,8 @@ export function getSessionAtom<Option extends ClientOptions>(
}; };
}; };
type UserWithAdditionalFields = InferUser< //@ts-expect-error
Auth extends BetterAuth ? Auth : never type UserWithAdditionalFields = InferUser<Auth["options"]>;
>;
//@ts-expect-error //@ts-expect-error
type SessionWithAdditionalFields = InferSession<Auth["options"]>; type SessionWithAdditionalFields = InferSession<Auth["options"]>;
const $signal = atom<boolean>(false); const $signal = atom<boolean>(false);
@@ -35,5 +44,13 @@ export function getSessionAtom<Option extends ClientOptions>(
}>($signal, "/session", client, { }>($signal, "/session", client, {
method: "GET", method: "GET",
}); });
return { $session: session, _sessionSignal: $signal }; return {
$session: session,
_sessionSignal: $signal,
$infer: {} as {
session: Prettify<SessionWithAdditionalFields>;
user: Prettify<UserWithAdditionalFields>;
s: Plugins;
},
};
} }

View File

@@ -49,7 +49,7 @@ export function createAuthClient<Option extends ClientOptions>(
for (const [key, value] of Object.entries(pluginsAtoms)) { for (const [key, value] of Object.entries(pluginsAtoms)) {
resolvedHooks[getAtomKey(key)] = () => useStore(value); resolvedHooks[getAtomKey(key)] = () => useStore(value);
} }
const { $session, _sessionSignal } = getSessionAtom<Option>($fetch); const { $session, _sessionSignal, $infer } = getSessionAtom<Option>($fetch);
function useSession() { function useSession() {
return useStore($session); return useStore($session);
@@ -73,5 +73,7 @@ export function createAuthClient<Option extends ClientOptions>(
InferClientAPI<Option> & InferClientAPI<Option> &
InferActions<Option> & { InferActions<Option> & {
useSession: typeof useSession; useSession: typeof useSession;
} & {
$infer: typeof $infer;
}; };
} }

View File

@@ -8,6 +8,7 @@ import type { Atom } from "nanostores";
import type { LiteralString, UnionToIntersection } from "../types/helper"; import type { LiteralString, UnionToIntersection } from "../types/helper";
import type { Auth } from "../auth"; import type { Auth } from "../auth";
import type { InferRoutes } from "./path-to-object"; import type { InferRoutes } from "./path-to-object";
import type { InferSession, InferUser } from "../types";
export type AtomListener = { export type AtomListener = {
matcher: (path: string) => boolean; matcher: (path: string) => boolean;
@@ -86,3 +87,25 @@ export type InferActions<O extends ClientOptions> = O["plugins"] extends Array<
* convention they start with "_" * convention they start with "_"
*/ */
export type IsSignal<T> = T extends `_${infer _}` ? true : false; export type IsSignal<T> = T extends `_${infer _}` ? true : false;
export type InferPluginsFromClient<O extends ClientOptions> =
O["plugins"] extends Array<AuthClientPlugin>
? Array<O["plugins"][number]["$InferServerPlugin"]>
: undefined;
type InferAuthFromClient<O extends ClientOptions> = {
handler: any;
api: any;
options: {
database: any;
plugins: InferPluginsFromClient<O>;
};
};
type InferSessionFromClient<O extends ClientOptions> = InferSession<
InferAuthFromClient<O> extends Auth ? InferAuthFromClient<O> : never
>;
type InferUserFromClient<O extends ClientOptions> = InferUser<
InferAuthFromClient<O> extends Auth ? InferAuthFromClient<O> : never
>;

View File

@@ -1,16 +1,18 @@
## TODO ## TODO
[ ] handle migration when the config removes existing schema [x] handle migration when the config removes existing schema
[x] refresh oauth tokens [x] refresh oauth tokens
[x] remember me functionality [x] remember me functionality
[x] add all oauth providers [x] add all oauth providers
[x] providers should only be oauth [x] providers should only be oauth
[x] add tests [x] add tests
[ ] add callback url on otp and backup code verification
[x] implement the ac check on the client to for organization [x] implement the ac check on the client to for organization
[x] add delete organization endpoint [x] add delete organization endpoint
[ ] add callback url on otp and backup code verification
[ ] fix bun problem [ ] fix bun problem
[ ] allow enabling two factor automatically for users [ ] allow enabling two factor automatically for users
[ ] change the pg driver to https://www.npmjs.com/package/postgres (maybe)
## Docs ## Docs
[ ] specify everywhere `auth` should be exported [x] specify everywhere `auth` should be exported
[ ] add a note about better-sqlite3 requiring to be added to webpack externals or find alternative that doesn't require it