diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-11 14:54:52 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-11 14:55:09 +0000 |
| commit | 2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d (patch) | |
| tree | c4738ba0bc011d60361f89aca9be3293474ab9e9 /packages | |
| parent | c2f1d6d8b8a0f09820153fc736806b147d46abfe (diff) | |
| download | karakeep-2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d.tar.zst | |
refactor: Migrating to trpc instead of next's route handers
Diffstat (limited to 'packages')
25 files changed, 289 insertions, 443 deletions
diff --git a/packages/db/index.ts b/packages/db/index.ts index 4a0f473f..e87b9515 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -1,14 +1,14 @@ import { PrismaClient } from "@prisma/client"; -const prisma = new PrismaClient({ - log: - process.env.NODE_ENV === "development" - ? ["query", "error", "warn"] - : ["error"], -}); - -// For some weird reason accessing @prisma/client from any package is causing problems (specially in error handling). -// Re export them here instead. -export * from "@prisma/client"; +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; -export default prisma; +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + log: + process.env.NODE_ENV === "development" + ? ["query", "error", "warn"] + : ["error"], + }); diff --git a/packages/web/app/api/auth/[...nextauth]/route.tsx b/packages/web/app/api/auth/[...nextauth]/route.tsx index e722926b..2f7f1cb0 100644 --- a/packages/web/app/api/auth/[...nextauth]/route.tsx +++ b/packages/web/app/api/auth/[...nextauth]/route.tsx @@ -1,3 +1,3 @@ -import { authHandler } from "@/lib/auth"; +import { authHandler } from "@/server/auth"; export { authHandler as GET, authHandler as POST }; diff --git a/packages/web/app/api/trpc/[trpc]/route.ts b/packages/web/app/api/trpc/[trpc]/route.ts index 872da79a..4d108604 100644 --- a/packages/web/app/api/trpc/[trpc]/route.ts +++ b/packages/web/app/api/trpc/[trpc]/route.ts @@ -1,10 +1,12 @@ -import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; -import { appRouter } from '@/server/routers/_app'; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { appRouter } from "@/server/api/routers/_app"; +import { createContext } from "@/server/api/client"; + const handler = (req: Request) => fetchRequestHandler({ - endpoint: '/api/trpc', + endpoint: "/api/trpc", req, router: appRouter, - createContext: () => ({}) + createContext, }); export { handler as GET, handler as POST }; diff --git a/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts b/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts deleted file mode 100644 index 3e57fa65..00000000 --- a/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { authOptions } from "@/lib/auth"; -import { deleteBookmark, updateBookmark } from "@/lib/services/bookmarks"; -import { - ZBookmark, - zUpdateBookmarksRequestSchema, -} from "@/lib/types/api/bookmarks"; -import { Prisma } from "@remember/db"; - -import { getServerSession } from "next-auth"; -import { NextRequest, NextResponse } from "next/server"; - -export async function PATCH( - request: NextRequest, - { params }: { params: { bookmarkId: string } }, -) { - const session = await getServerSession(authOptions); - if (!session) { - return new Response(null, { status: 401 }); - } - - const updateJson = await request.json(); - const update = zUpdateBookmarksRequestSchema.safeParse(updateJson); - if (!update.success) { - return new Response(null, { status: 400 }); - } - - try { - const bookmark: ZBookmark = await updateBookmark( - params.bookmarkId, - session.user.id, - update.data, - ); - return NextResponse.json(bookmark); - } catch (e: unknown) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2025" // RecordNotFound - ) { - return new Response(null, { status: 404 }); - } else { - throw e; - } - } -} - -export async function DELETE( - _request: NextRequest, - { params }: { params: { bookmarkId: string } }, -) { - // TODO: We probably should be using an API key here instead of the session; - const session = await getServerSession(authOptions); - if (!session) { - return new Response(null, { status: 401 }); - } - - try { - await deleteBookmark(params.bookmarkId, session.user.id); - } catch (e: unknown) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2025" // RecordNotFound - ) { - return new Response(null, { status: 404 }); - } else { - throw e; - } - } - - return new Response(null, { status: 204 }); -} diff --git a/packages/web/app/api/v1/bookmarks/route.ts b/packages/web/app/api/v1/bookmarks/route.ts deleted file mode 100644 index 98e01080..00000000 --- a/packages/web/app/api/v1/bookmarks/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { authOptions } from "@/lib/auth"; -import { bookmarkLink, getBookmarks } from "@/lib/services/bookmarks"; - -import { - zNewBookmarkRequestSchema, - ZGetBookmarksResponse, - ZBookmark, - zGetBookmarksRequestSchema, -} from "@/lib/types/api/bookmarks"; -import { getServerSession } from "next-auth"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(request: NextRequest) { - // TODO: We probably should be using an API key here instead of the session; - const session = await getServerSession(authOptions); - if (!session) { - return new Response(null, { status: 401 }); - } - - const linkRequest = zNewBookmarkRequestSchema.safeParse(await request.json()); - - if (!linkRequest.success) { - return NextResponse.json( - { - error: linkRequest.error.toString(), - }, - { status: 400 }, - ); - } - - const bookmark = await bookmarkLink(linkRequest.data.url, session.user.id); - - const response: ZBookmark = { ...bookmark }; - return NextResponse.json(response, { status: 201 }); -} - -export async function GET(request: NextRequest) { - // TODO: We probably should be using an API key here instead of the session; - const session = await getServerSession(authOptions); - if (!session) { - return new Response(null, { status: 401 }); - } - - const query = request.nextUrl.searchParams; - const params = zGetBookmarksRequestSchema.safeParse(query); - - if (!params.success) { - return new Response(null, { status: 400 }); - } - - const bookmarks = await getBookmarks(session.user.id, params.data); - - const response: ZGetBookmarksResponse = { bookmarks }; - return NextResponse.json(response); -} diff --git a/packages/web/app/dashboard/bookmarks/archive/page.tsx b/packages/web/app/dashboard/bookmarks/archive/page.tsx index 0d105fbd..954c298c 100644 --- a/packages/web/app/dashboard/bookmarks/archive/page.tsx +++ b/packages/web/app/dashboard/bookmarks/archive/page.tsx @@ -1,5 +1,5 @@ import Bookmarks from "../components/Bookmarks"; export default async function ArchivedBookmarkPage() { - return <Bookmarks title="Archive" archived={true} favourited={false} />; + return <Bookmarks title="Archive" archived={true} />; } diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx index f99c1655..e8ecec35 100644 --- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx +++ b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx @@ -3,13 +3,13 @@ import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import APIClient from "@/lib/api"; import { Plus } from "lucide-react"; import { useRouter } from "next/navigation"; import { useForm, SubmitErrorHandler } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; const formSchema = z.object({ url: z.string().url({ message: "The link must be a valid URL" }), @@ -23,9 +23,10 @@ export default function AddLink() { }); async function onSubmit(value: z.infer<typeof formSchema>) { - const [_resp, error] = await APIClient.bookmarkLink(value.url); - if (error) { - toast({ description: error.message, variant: "destructive" }); + try { + await api.bookmarks.bookmarkLink.mutate({ url: value.url, type: "link" }); + } catch (e) { + toast({ description: "Something went wrong", variant: "destructive" }); return; } router.refresh(); diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx index 15ce64c7..4496d820 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx @@ -1,7 +1,7 @@ "use client"; import { useToast } from "@/components/ui/use-toast"; -import APIClient from "@/lib/api"; +import { api } from "@/lib/trpc"; import { ZBookmark, ZUpdateBookmarksRequest } from "@/lib/types/api/bookmarks"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; @@ -19,36 +19,37 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const linkId = bookmark.id; const unbookmarkLink = async () => { - const [_, error] = await APIClient.deleteBookmark(linkId); + try { + await api.bookmarks.deleteBookmark.mutate({ + bookmarkId: linkId, + }); - if (error) { + toast({ + description: "The bookmark has been deleted!", + }); + } catch (e) { toast({ variant: "destructive", title: "Something went wrong", description: "There was a problem with your request.", }); - } else { - toast({ - description: "The bookmark has been deleted!", - }); } router.refresh(); }; const updateBookmark = async (req: ZUpdateBookmarksRequest) => { - const [_, error] = await APIClient.updateBookmark(linkId, req); - - if (error) { + try { + await api.bookmarks.updateBookmark.mutate(req); + toast({ + description: "The bookmark has been updated!", + }); + } catch (e) { toast({ variant: "destructive", title: "Something went wrong", description: "There was a problem with your request.", }); - } else { - toast({ - description: "The bookmark has been updated!", - }); } router.refresh(); @@ -63,13 +64,20 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </DropdownMenuTrigger> <DropdownMenuContent className="w-fit"> <DropdownMenuItem - onClick={() => updateBookmark({ favourited: !bookmark.favourited })} + onClick={() => + updateBookmark({ + bookmarkId: linkId, + favourited: !bookmark.favourited, + }) + } > <Star className="mr-2 size-4" /> <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span> </DropdownMenuItem> <DropdownMenuItem - onClick={() => updateBookmark({ archived: !bookmark.archived })} + onClick={() => + updateBookmark({ bookmarkId: linkId, archived: !bookmark.archived }) + } > <Archive className="mr-2 size-4" /> <span>{bookmark.archived ? "Un-archive" : "Archive"}</span> diff --git a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx index 6a9ffe1b..d7e3f1f3 100644 --- a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx +++ b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx @@ -1,25 +1,26 @@ import { redirect } from "next/navigation"; import BookmarksGrid from "./BookmarksGrid"; -import { authOptions } from "@/lib/auth"; -import { getServerSession } from "next-auth"; -import { getBookmarks } from "@/lib/services/bookmarks"; import { ZGetBookmarksRequest } from "@/lib/types/api/bookmarks"; +import { api } from "@/server/api/client"; +import { getServerAuthSession } from "@/server/auth"; export default async function Bookmarks({ favourited, archived, title, }: ZGetBookmarksRequest & { title: string }) { - const session = await getServerSession(authOptions); + const session = await getServerAuthSession(); if (!session) { redirect("/"); } - const bookmarks = await getBookmarks(session.user.id, { + + // TODO: Migrate to a server side call in trpc instead + const bookmarks = await api.bookmarks.getBookmarks({ favourited, archived, }); - if (bookmarks.length == 0) { + if (bookmarks.bookmarks.length == 0) { // TODO: This needs to be polished return ( <> @@ -32,7 +33,7 @@ export default async function Bookmarks({ return ( <> <div className="container pb-4 text-2xl">{title}</div> - <BookmarksGrid bookmarks={bookmarks} /> + <BookmarksGrid bookmarks={bookmarks.bookmarks} /> </> ); } diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx index 3b4e1649..44892e81 100644 --- a/packages/web/app/dashboard/components/Sidebar.tsx +++ b/packages/web/app/dashboard/components/Sidebar.tsx @@ -1,12 +1,11 @@ import { Button } from "@/components/ui/button"; -import { authOptions } from "@/lib/auth"; import { Archive, MoreHorizontal, Star, Tag, Home, Brain } from "lucide-react"; -import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import SidebarItem from "./SidebarItem"; +import { getServerAuthSession } from "@/server/auth"; export default async function Sidebar() { - const session = await getServerSession(authOptions); + const session = await getServerAuthSession(); if (!session) { redirect("/"); } diff --git a/packages/web/lib/api.ts b/packages/web/lib/api.ts deleted file mode 100644 index 3978dcb6..00000000 --- a/packages/web/lib/api.ts +++ /dev/null @@ -1,91 +0,0 @@ -"use client"; - -import { ZodTypeAny, z } from "zod"; -import { - ZNewBookmarkRequest, - ZUpdateBookmarksRequest, - zBookmarkSchema, - zGetBookmarksResponseSchema, -} from "./types/api/bookmarks"; - -import serverConfig from "./config"; - -const BASE_URL = `${serverConfig.api_url}/api/v1`; - -export type FetchError = { - status?: number; - message?: string; -}; - -type InputSchema<T> = T extends ZodTypeAny ? T : undefined; - -async function doRequest<T>( - path: string, - respSchema?: InputSchema<T>, - opts?: RequestInit, -): Promise< - | (InputSchema<T> extends ZodTypeAny - ? [z.infer<InputSchema<T>>, undefined] - : [undefined, undefined]) - | [undefined, FetchError] -> { - try { - const res = await fetch(`${BASE_URL}${path}`, opts); - if (!res.ok) { - return [ - undefined, - { status: res.status, message: await res.text() }, - ] as const; - } - if (!respSchema) { - return [undefined, undefined] as const; - } - - const parsed = respSchema.safeParse(await res.json()); - if (!parsed.success) { - return [ - undefined, - { message: `Failed to parse response: ${parsed.error.toString()}` }, - ] as const; - } - - return [parsed.data, undefined] as const; - } catch (error) { - return [ - undefined, - { message: `Failed to execute fetch request: ${error}` }, - ] as const; - } -} - -export default class APIClient { - static async getBookmarks() { - return await doRequest(`/bookmarks`, zGetBookmarksResponseSchema, { - next: { tags: ["links"] }, - }); - } - - static async bookmarkLink(url: string) { - const body: ZNewBookmarkRequest = { - type: "link", - url, - }; - return await doRequest(`/bookmarks`, undefined, { - method: "POST", - body: JSON.stringify(body), - }); - } - - static async deleteBookmark(id: string) { - return await doRequest(`/bookmarks/${id}`, undefined, { - method: "DELETE", - }); - } - - static async updateBookmark(id: string, update: ZUpdateBookmarksRequest) { - return await doRequest(`/bookmarks/${id}`, zBookmarkSchema, { - method: "PATCH", - body: JSON.stringify(update), - }); - } -} diff --git a/packages/web/lib/services/bookmarks.ts b/packages/web/lib/services/bookmarks.ts deleted file mode 100644 index 82bfec49..00000000 --- a/packages/web/lib/services/bookmarks.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { LinkCrawlerQueue } from "@remember/shared/queues"; -import prisma from "@remember/db"; -import { - ZBookmark, - ZBookmarkContent, - ZGetBookmarksRequest, - ZUpdateBookmarksRequest, -} from "@/lib/types/api/bookmarks"; - -const defaultBookmarkFields = { - id: true, - favourited: true, - archived: true, - createdAt: true, - link: { - select: { - url: true, - title: true, - description: true, - imageUrl: true, - favicon: true, - }, - }, - tags: { - include: { - tag: true, - }, - }, -}; - -async function dummyPrismaReturnType() { - const x = await prisma.bookmark.findFirstOrThrow({ - select: defaultBookmarkFields, - }); - return x; -} - -function toZodSchema( - bookmark: Awaited<ReturnType<typeof dummyPrismaReturnType>>, -): ZBookmark { - const { tags, link, ...rest } = bookmark; - - let content: ZBookmarkContent; - if (link) { - content = { type: "link", ...link }; - } else { - throw new Error("Unknown content type"); - } - - return { - tags: tags.map((t) => t.tag), - content, - ...rest, - }; -} - -export async function updateBookmark( - bookmarkId: string, - userId: string, - req: ZUpdateBookmarksRequest, -) { - const bookmark = await prisma.bookmark.update({ - where: { - id: bookmarkId, - userId, - }, - data: req, - select: defaultBookmarkFields, - }); - return toZodSchema(bookmark); -} - -export async function deleteBookmark(bookmarkId: string, userId: string) { - await prisma.bookmark.delete({ - where: { - id: bookmarkId, - userId, - }, - }); -} - -export async function bookmarkLink(url: string, userId: string) { - const bookmark = await prisma.bookmark.create({ - data: { - link: { - create: { - url, - }, - }, - userId, - }, - select: defaultBookmarkFields, - }); - - // Enqueue crawling request - await LinkCrawlerQueue.add("crawl", { - bookmarkId: bookmark.id, - url: url, - }); - - return toZodSchema(bookmark); -} - -export async function getBookmarks( - userId: string, - { favourited, archived }: ZGetBookmarksRequest, -) { - return ( - await prisma.bookmark.findMany({ - where: { - userId, - archived, - favourited, - }, - orderBy: { - createdAt: 'desc', - }, - select: defaultBookmarkFields, - }) - ).map(toZodSchema); -} diff --git a/packages/web/lib/trpc.tsx b/packages/web/lib/trpc.tsx new file mode 100644 index 00000000..540c6ab5 --- /dev/null +++ b/packages/web/lib/trpc.tsx @@ -0,0 +1,20 @@ +"use client"; +import { httpBatchLink } from "@trpc/client"; +import type { AppRouter } from "@/server/api/routers/_app"; + +import { loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; + +export const api = createTRPCClient<AppRouter>({ + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + httpBatchLink({ + // TODO: Change this to be a full URL exposed as a client side setting + url: `/api/trpc`, + }), + ], +}); diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/web/lib/types/api/bookmarks.ts index e37d14fb..3d70f868 100644 --- a/packages/web/lib/types/api/bookmarks.ts +++ b/packages/web/lib/types/api/bookmarks.ts @@ -45,6 +45,7 @@ export type ZGetBookmarksResponse = z.infer<typeof zGetBookmarksResponseSchema>; // PATCH /v1/bookmarks/[bookmarkId] export const zUpdateBookmarksRequestSchema = z.object({ + bookmarkId: z.string(), archived: z.boolean().optional(), favourited: z.boolean().optional(), }); diff --git a/packages/web/lib/types/next-auth.d.ts b/packages/web/lib/types/next-auth.d.ts deleted file mode 100644 index cd47dfce..00000000 --- a/packages/web/lib/types/next-auth.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DefaultSession } from "next-auth"; - -declare module "next-auth" { - /** - * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context - */ - export interface Session { - user: { - id: string; - } & DefaultSession["user"]; - } -} diff --git a/packages/web/package.json b/packages/web/package.json index a46e7344..0601e4f4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -18,10 +18,9 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", - "@tanstack/react-query": "^5.18.1", + "@remember/db": "0.1.0", "@trpc/client": "11.0.0-next-beta.274", "@trpc/next": "11.0.0-next-beta.274", - "@trpc/react-query": "11.0.0-next-beta.274", "@trpc/server": "11.0.0-next-beta.274", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -33,6 +32,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.50.1", + "server-only": "^0.0.1", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" diff --git a/packages/web/server/api/client.ts b/packages/web/server/api/client.ts new file mode 100644 index 00000000..7008e10d --- /dev/null +++ b/packages/web/server/api/client.ts @@ -0,0 +1,14 @@ +import { appRouter } from "./routers/_app"; +import { getServerAuthSession } from "@/server/auth"; +import { Context, createCallerFactory } from "./trpc"; + +export const createContext = async (): Promise<Context> => { + const session = await getServerAuthSession(); + return { + session, + }; +}; + +const createCaller = createCallerFactory(appRouter); + +export const api = createCaller(createContext); diff --git a/packages/web/server/api/routers/_app.ts b/packages/web/server/api/routers/_app.ts new file mode 100644 index 00000000..a4f9c629 --- /dev/null +++ b/packages/web/server/api/routers/_app.ts @@ -0,0 +1,7 @@ +import { router } from "../trpc"; +import { bookmarksAppRouter } from "./bookmarks"; +export const appRouter = router({ + bookmarks: bookmarksAppRouter, +}); +// export type definition of API +export type AppRouter = typeof appRouter; diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts new file mode 100644 index 00000000..0b97563f --- /dev/null +++ b/packages/web/server/api/routers/bookmarks.ts @@ -0,0 +1,139 @@ +import { z } from "zod"; +import { authedProcedure, router } from "../trpc"; +import { + ZBookmark, + ZBookmarkContent, + zBookmarkSchema, + zGetBookmarksRequestSchema, + zGetBookmarksResponseSchema, + zNewBookmarkRequestSchema, + zUpdateBookmarksRequestSchema, +} from "@/lib/types/api/bookmarks"; +import { prisma } from "@remember/db"; +import { LinkCrawlerQueue } from "@remember/shared/queues"; + +const defaultBookmarkFields = { + id: true, + favourited: true, + archived: true, + createdAt: true, + link: { + select: { + url: true, + title: true, + description: true, + imageUrl: true, + favicon: true, + }, + }, + tags: { + include: { + tag: true, + }, + }, +}; + +async function dummyPrismaReturnType() { + const x = await prisma.bookmark.findFirstOrThrow({ + select: defaultBookmarkFields, + }); + return x; +} + +function toZodSchema( + bookmark: Awaited<ReturnType<typeof dummyPrismaReturnType>>, +): ZBookmark { + const { tags, link, ...rest } = bookmark; + + let content: ZBookmarkContent; + if (link) { + content = { type: "link", ...link }; + } else { + throw new Error("Unknown content type"); + } + + return { + tags: tags.map((t) => t.tag), + content, + ...rest, + }; +} + +export const bookmarksAppRouter = router({ + bookmarkLink: authedProcedure + .input(zNewBookmarkRequestSchema) + .output(zBookmarkSchema) + .mutation(async ({ input, ctx }) => { + const { url } = input; + const userId = ctx.user.id; + + const bookmark = await prisma.bookmark.create({ + data: { + link: { + create: { + url, + }, + }, + userId, + }, + select: defaultBookmarkFields, + }); + + // Enqueue crawling request + await LinkCrawlerQueue.add("crawl", { + bookmarkId: bookmark.id, + url: url, + }); + + return toZodSchema(bookmark); + }), + + updateBookmark: authedProcedure + .input(zUpdateBookmarksRequestSchema) + .output(zBookmarkSchema) + .mutation(async ({ input, ctx }) => { + const bookmark = await prisma.bookmark.update({ + where: { + id: input.bookmarkId, + userId: ctx.user.id, + }, + data: { + archived: input.archived, + favourited: input.favourited, + }, + select: defaultBookmarkFields, + }); + return toZodSchema(bookmark); + }), + + deleteBookmark: authedProcedure + .input(z.object({ bookmarkId: z.string() })) + .mutation(async ({ input, ctx }) => { + await prisma.bookmark.delete({ + where: { + id: input.bookmarkId, + userId: ctx.user.id, + }, + }); + }), + getBookmarks: authedProcedure + .input(zGetBookmarksRequestSchema) + .output(zGetBookmarksResponseSchema) + .query(async ({ input, ctx }) => { + const bookmarks = ( + await prisma.bookmark.findMany({ + where: { + userId: ctx.user.id, + archived: input.archived, + favourited: input.favourited, + }, + orderBy: { + createdAt: "desc", + }, + select: defaultBookmarkFields, + }) + ).map(toZodSchema); + + return { bookmarks }; + }), +}); diff --git a/packages/web/server/api/trpc.ts b/packages/web/server/api/trpc.ts new file mode 100644 index 00000000..82aa2d18 --- /dev/null +++ b/packages/web/server/api/trpc.ts @@ -0,0 +1,31 @@ +import { TRPCError, initTRPC } from "@trpc/server"; +import { Session } from "next-auth"; + +export type Context = { + session: Session | null; +}; + +// Avoid exporting the entire t-object +// since it's not very descriptive. +// For instance, the use of a t variable +// is common in i18n libraries. +const t = initTRPC.context<Context>().create(); +export const createCallerFactory = t.createCallerFactory; +// Base router and procedure helpers +export const router = t.router; +export const procedure = t.procedure; +export const publicProcedure = t.procedure; + +export const authedProcedure = t.procedure.use(function isAuthed(opts) { + const user = opts.ctx.session?.user; + + if (!user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return opts.next({ + ctx: { + user, + }, + }); +}); diff --git a/packages/web/lib/auth.ts b/packages/web/server/auth.ts index df98e6b8..05d3d296 100644 --- a/packages/web/lib/auth.ts +++ b/packages/web/server/auth.ts @@ -1,8 +1,20 @@ -import NextAuth, { NextAuthOptions } from "next-auth"; +import NextAuth, { NextAuthOptions, getServerSession } from "next-auth"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import AuthentikProvider from "next-auth/providers/authentik"; -import serverConfig from "@/lib/config"; -import prisma from "@remember/db"; +import serverConfig from "@/server/config"; +import { prisma } from "@remember/db"; +import { DefaultSession } from "next-auth"; + +declare module "next-auth" { + /** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ + export interface Session { + user: { + id: string; + } & DefaultSession["user"]; + } +} const providers = []; @@ -23,3 +35,5 @@ export const authOptions: NextAuthOptions = { }; export const authHandler = NextAuth(authOptions); + +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/web/lib/config.ts b/packages/web/server/config.ts index dbf6620e..dbf6620e 100644 --- a/packages/web/lib/config.ts +++ b/packages/web/server/config.ts diff --git a/packages/web/server/routers/_app.ts b/packages/web/server/routers/_app.ts deleted file mode 100644 index 47c586b7..00000000 --- a/packages/web/server/routers/_app.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; -import { procedure, router } from "../trpc"; -export const appRouter = router({ - hello: procedure - .input( - z.object({ - text: z.string(), - }), - ) - .query((opts) => { - return { - greeting: `hello ${opts.input.text}`, - }; - }), -}); -// export type definition of API -export type AppRouter = typeof appRouter; diff --git a/packages/web/server/trpc.ts b/packages/web/server/trpc.ts deleted file mode 100644 index b34424ed..00000000 --- a/packages/web/server/trpc.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { initTRPC } from '@trpc/server'; -// Avoid exporting the entire t-object -// since it's not very descriptive. -// For instance, the use of a t variable -// is common in i18n libraries. -const t = initTRPC.create(); -// Base router and procedure helpers -export const router = t.router; -export const procedure = t.procedure; diff --git a/packages/web/server/utils.ts b/packages/web/server/utils.ts deleted file mode 100644 index 70c06585..00000000 --- a/packages/web/server/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { httpBatchLink } from "@trpc/client"; -import { createTRPCNext } from "@trpc/next"; -import type { AppRouter } from "../server/routers/_app"; -import serverConfig from "@/lib/config"; - -export const trpc = createTRPCNext<AppRouter>({ - config(_opts) { - return { - links: [ - httpBatchLink({ - url: `${serverConfig.api_url}/api/trpc`, - }), - ], - }; - }, -}); |
