aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-11 14:54:52 +0000
committerMohamedBassem <me@mbassem.com>2024-02-11 14:55:09 +0000
commit2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d (patch)
treec4738ba0bc011d60361f89aca9be3293474ab9e9 /packages
parentc2f1d6d8b8a0f09820153fc736806b147d46abfe (diff)
downloadkarakeep-2c2d05fd0a2c3c26d765f8a6beb88d907a097c1d.tar.zst
refactor: Migrating to trpc instead of next's route handers
Diffstat (limited to 'packages')
-rw-r--r--packages/db/index.ts22
-rw-r--r--packages/web/app/api/auth/[...nextauth]/route.tsx2
-rw-r--r--packages/web/app/api/trpc/[trpc]/route.ts10
-rw-r--r--packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts70
-rw-r--r--packages/web/app/api/v1/bookmarks/route.ts55
-rw-r--r--packages/web/app/dashboard/bookmarks/archive/page.tsx2
-rw-r--r--packages/web/app/dashboard/bookmarks/components/AddLink.tsx9
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx40
-rw-r--r--packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx15
-rw-r--r--packages/web/app/dashboard/components/Sidebar.tsx5
-rw-r--r--packages/web/lib/api.ts91
-rw-r--r--packages/web/lib/services/bookmarks.ts121
-rw-r--r--packages/web/lib/trpc.tsx20
-rw-r--r--packages/web/lib/types/api/bookmarks.ts1
-rw-r--r--packages/web/lib/types/next-auth.d.ts12
-rw-r--r--packages/web/package.json4
-rw-r--r--packages/web/server/api/client.ts14
-rw-r--r--packages/web/server/api/routers/_app.ts7
-rw-r--r--packages/web/server/api/routers/bookmarks.ts139
-rw-r--r--packages/web/server/api/trpc.ts31
-rw-r--r--packages/web/server/auth.ts (renamed from packages/web/lib/auth.ts)20
-rw-r--r--packages/web/server/config.ts (renamed from packages/web/lib/config.ts)0
-rw-r--r--packages/web/server/routers/_app.ts17
-rw-r--r--packages/web/server/trpc.ts9
-rw-r--r--packages/web/server/utils.ts16
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`,
- }),
- ],
- };
- },
-});