aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/api/v1
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-05-18 16:58:08 +0100
committerGitHub <noreply@github.com>2025-05-18 16:58:08 +0100
commit3505cb7d6416d101a4fcb1be27fc22e0171bacd2 (patch)
treeef9f55504b8a5b20add8c0ebe916972ab4ab0178 /apps/web/app/api/v1
parent74e74fa6425f072107de3a9bc9dd8f91c5ac9a7d (diff)
downloadkarakeep-3505cb7d6416d101a4fcb1be27fc22e0171bacd2.tar.zst
refactor: Migrate from NextJs's API routes to Hono based routes for the API (#1432)
* Setup Hono and migrate the highlights API there * Implement the tags and lists endpoint * Implement the bookmarks and users endpoints * Add the trpc error code adapter * Remove the old nextjs handlers * fix api key not found handling * Fix trpc error handling * Fix 204 handling * Fix search ordering * Implement the singlefile endpoint * Implement the asset serving endpoints * Implement webauth * Add hono as a catch all route under api * fix tests
Diffstat (limited to 'apps/web/app/api/v1')
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts37
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts36
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/highlights/route.ts18
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/lists/route.ts18
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts54
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/summarize/route.ts19
-rw-r--r--apps/web/app/api/v1/bookmarks/[bookmarkId]/tags/route.ts45
-rw-r--r--apps/web/app/api/v1/bookmarks/route.ts46
-rw-r--r--apps/web/app/api/v1/bookmarks/search/route.ts44
-rw-r--r--apps/web/app/api/v1/bookmarks/singlefile/route.ts54
-rw-r--r--apps/web/app/api/v1/highlights/[highlightId]/route.ts50
-rw-r--r--apps/web/app/api/v1/highlights/route.ts30
-rw-r--r--apps/web/app/api/v1/lists/[listId]/bookmarks/[bookmarkId]/route.ts35
-rw-r--r--apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts19
-rw-r--r--apps/web/app/api/v1/lists/[listId]/route.ts55
-rw-r--r--apps/web/app/api/v1/lists/route.ts26
-rw-r--r--apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts27
-rw-r--r--apps/web/app/api/v1/tags/[tagId]/route.ts55
-rw-r--r--apps/web/app/api/v1/tags/route.ts14
-rw-r--r--apps/web/app/api/v1/users/me/route.ts14
-rw-r--r--apps/web/app/api/v1/users/me/stats/route.ts14
-rw-r--r--apps/web/app/api/v1/utils/handler.ts170
-rw-r--r--apps/web/app/api/v1/utils/pagination.ts30
-rw-r--r--apps/web/app/api/v1/utils/types.ts23
24 files changed, 0 insertions, 933 deletions
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts
deleted file mode 100644
index 88e203de..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-import { z } from "zod";
-
-export const dynamic = "force-dynamic";
-
-export const PUT = (
- req: NextRequest,
- params: { params: { bookmarkId: string; assetId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: z.object({ assetId: z.string() }),
- handler: async ({ api, body }) => {
- await api.assets.replaceAsset({
- bookmarkId: params.params.bookmarkId,
- oldAssetId: params.params.assetId,
- newAssetId: body!.assetId,
- });
- return { status: 204 };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- params: { params: { bookmarkId: string; assetId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- await api.assets.detachAsset({
- bookmarkId: params.params.bookmarkId,
- assetId: params.params.assetId,
- });
- return { status: 204 };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts
deleted file mode 100644
index 6c7c70d7..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zAssetSchema } from "@karakeep/shared/types/bookmarks";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const resp = await api.bookmarks.getBookmark({
- bookmarkId: params.params.bookmarkId,
- });
- return { status: 200, resp: { assets: resp.assets } };
- },
- });
-
-export const POST = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: zAssetSchema,
- handler: async ({ api, body }) => {
- const asset = await api.assets.attachAsset({
- bookmarkId: params.params.bookmarkId,
- asset: body!,
- });
- return { status: 201, resp: asset };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/highlights/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/highlights/route.ts
deleted file mode 100644
index 4e1f87a0..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/highlights/route.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const resp = await api.highlights.getForBookmark({
- bookmarkId: params.params.bookmarkId,
- });
- return { status: 200, resp };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/lists/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/lists/route.ts
deleted file mode 100644
index ad3052c9..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/lists/route.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const resp = await api.lists.getListsOfBookmark({
- bookmarkId: params.params.bookmarkId,
- });
- return { status: 200, resp };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
deleted file mode 100644
index 9ad18fd3..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zUpdateBookmarksRequestSchema } from "@karakeep/shared/types/bookmarks";
-
-import { zGetBookmarkQueryParamsSchema } from "../../utils/types";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- { params }: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- searchParamsSchema: zGetBookmarkQueryParamsSchema,
- handler: async ({ api, searchParams }) => {
- const bookmark = await api.bookmarks.getBookmark({
- bookmarkId: params.bookmarkId,
- includeContent: searchParams.includeContent,
- });
- return { status: 200, resp: bookmark };
- },
- });
-
-export const PATCH = (
- req: NextRequest,
- { params }: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: zUpdateBookmarksRequestSchema.omit({ bookmarkId: true }),
- handler: async ({ api, body }) => {
- const bookmark = await api.bookmarks.updateBookmark({
- bookmarkId: params.bookmarkId,
- ...body!,
- });
- return { status: 200, resp: bookmark };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- { params }: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- await api.bookmarks.deleteBookmark({
- bookmarkId: params.bookmarkId,
- });
- return { status: 204 };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/summarize/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/summarize/route.ts
deleted file mode 100644
index ea41cad4..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/summarize/route.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const POST = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const bookmark = await api.bookmarks.summarizeBookmark({
- bookmarkId: params.params.bookmarkId,
- });
-
- return { status: 200, resp: bookmark };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/tags/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/tags/route.ts
deleted file mode 100644
index 00c28afa..00000000
--- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/tags/route.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-import { z } from "zod";
-
-import { zManipulatedTagSchema } from "@karakeep/shared/types/bookmarks";
-
-export const dynamic = "force-dynamic";
-
-export const POST = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: z.object({
- tags: z.array(zManipulatedTagSchema),
- }),
- handler: async ({ api, body }) => {
- const resp = await api.bookmarks.updateTags({
- bookmarkId: params.params.bookmarkId,
- attach: body!.tags,
- detach: [],
- });
- return { status: 200, resp: { attached: resp.attached } };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- params: { params: { bookmarkId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: z.object({
- tags: z.array(zManipulatedTagSchema),
- }),
- handler: async ({ api, body }) => {
- const resp = await api.bookmarks.updateTags({
- bookmarkId: params.params.bookmarkId,
- detach: body!.tags,
- attach: [],
- });
- return { status: 200, resp: { detached: resp.detached } };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/route.ts b/apps/web/app/api/v1/bookmarks/route.ts
deleted file mode 100644
index 4df4f6ad..00000000
--- a/apps/web/app/api/v1/bookmarks/route.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { NextRequest } from "next/server";
-import { z } from "zod";
-
-import {
- zNewBookmarkRequestSchema,
- zSortOrder,
-} from "@karakeep/shared/types/bookmarks";
-
-import { buildHandler } from "../utils/handler";
-import { adaptPagination, zPagination } from "../utils/pagination";
-import { zStringBool } from "../utils/types";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- searchParamsSchema: z
- .object({
- favourited: zStringBool.optional(),
- archived: zStringBool.optional(),
- sortOrder: zSortOrder
- .exclude([zSortOrder.Enum.relevance])
- .optional()
- .default(zSortOrder.Enum.desc),
- // TODO: Change the default to false in a couple of releases.
- includeContent: zStringBool.optional().default("true"),
- })
- .and(zPagination),
- handler: async ({ api, searchParams }) => {
- const bookmarks = await api.bookmarks.getBookmarks({
- ...searchParams,
- });
- return { status: 200, resp: adaptPagination(bookmarks) };
- },
- });
-
-export const POST = (req: NextRequest) =>
- buildHandler({
- req,
- bodySchema: zNewBookmarkRequestSchema,
- handler: async ({ api, body }) => {
- const bookmark = await api.bookmarks.createBookmark(body!);
- return { status: 201, resp: bookmark };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/search/route.ts b/apps/web/app/api/v1/bookmarks/search/route.ts
deleted file mode 100644
index e85c7954..00000000
--- a/apps/web/app/api/v1/bookmarks/search/route.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { NextRequest } from "next/server";
-import { z } from "zod";
-
-import { buildHandler } from "../../utils/handler";
-import { zGetBookmarkSearchParamsSchema } from "../../utils/types";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- searchParamsSchema: z
- .object({
- q: z.string(),
- limit: z.coerce.number().optional(),
- cursor: z
- .string()
- // Search cursor V1 is just a number
- .pipe(z.coerce.number())
- .transform((val) => {
- return { ver: 1 as const, offset: val };
- })
- .optional(),
- })
- .and(zGetBookmarkSearchParamsSchema),
- handler: async ({ api, searchParams }) => {
- const bookmarks = await api.bookmarks.searchBookmarks({
- text: searchParams.q,
- cursor: searchParams.cursor,
- sortOrder: searchParams.sortOrder,
- limit: searchParams.limit,
- includeContent: searchParams.includeContent,
- });
- return {
- status: 200,
- resp: {
- bookmarks: bookmarks.bookmarks,
- nextCursor: bookmarks.nextCursor
- ? `${bookmarks.nextCursor.offset}`
- : null,
- },
- };
- },
- });
diff --git a/apps/web/app/api/v1/bookmarks/singlefile/route.ts b/apps/web/app/api/v1/bookmarks/singlefile/route.ts
deleted file mode 100644
index 7c1d7201..00000000
--- a/apps/web/app/api/v1/bookmarks/singlefile/route.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { createContextFromRequest } from "@/server/api/client";
-import { TRPCError } from "@trpc/server";
-
-import serverConfig from "@karakeep/shared/config";
-import { BookmarkTypes } from "@karakeep/shared/types/bookmarks";
-import { createCallerFactory } from "@karakeep/trpc";
-import { appRouter } from "@karakeep/trpc/routers/_app";
-
-import { uploadFromPostData } from "../../../assets/route";
-
-export const dynamic = "force-dynamic";
-
-export async function POST(req: Request) {
- const ctx = await createContextFromRequest(req);
- if (!ctx.user) {
- return Response.json({ error: "Unauthorized" }, { status: 401 });
- }
- if (serverConfig.demoMode) {
- throw new TRPCError({
- message: "Mutations are not allowed in demo mode",
- code: "FORBIDDEN",
- });
- }
- const formData = await req.formData();
- const up = await uploadFromPostData(ctx.user, ctx.db, formData);
-
- if ("error" in up) {
- return Response.json({ error: up.error }, { status: up.status });
- }
-
- const url = formData.get("url");
- if (!url) {
- throw new TRPCError({
- message: "URL is required",
- code: "BAD_REQUEST",
- });
- }
- if (typeof url !== "string") {
- throw new TRPCError({
- message: "URL must be a string",
- code: "BAD_REQUEST",
- });
- }
-
- const createCaller = createCallerFactory(appRouter);
- const api = createCaller(ctx);
-
- const bookmark = await api.bookmarks.createBookmark({
- type: BookmarkTypes.LINK,
- url,
- precrawledArchiveId: up.assetId,
- });
- return Response.json(bookmark, { status: 201 });
-}
diff --git a/apps/web/app/api/v1/highlights/[highlightId]/route.ts b/apps/web/app/api/v1/highlights/[highlightId]/route.ts
deleted file mode 100644
index 50420427..00000000
--- a/apps/web/app/api/v1/highlights/[highlightId]/route.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zUpdateHighlightSchema } from "@karakeep/shared/types/highlights";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- { params }: { params: { highlightId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const highlight = await api.highlights.get({
- highlightId: params.highlightId,
- });
- return { status: 200, resp: highlight };
- },
- });
-
-export const PATCH = (
- req: NextRequest,
- { params }: { params: { highlightId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: zUpdateHighlightSchema.omit({ highlightId: true }),
- handler: async ({ api, body }) => {
- const highlight = await api.highlights.update({
- highlightId: params.highlightId,
- ...body!,
- });
- return { status: 200, resp: highlight };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- { params }: { params: { highlightId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const highlight = await api.highlights.delete({
- highlightId: params.highlightId,
- });
- return { status: 200, resp: highlight };
- },
- });
diff --git a/apps/web/app/api/v1/highlights/route.ts b/apps/web/app/api/v1/highlights/route.ts
deleted file mode 100644
index e95d84f6..00000000
--- a/apps/web/app/api/v1/highlights/route.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zNewHighlightSchema } from "@karakeep/shared/types/highlights";
-
-import { adaptPagination, zPagination } from "../utils/pagination";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- searchParamsSchema: zPagination,
- handler: async ({ api, searchParams }) => {
- const resp = await api.highlights.getAll({
- ...searchParams,
- });
- return { status: 200, resp: adaptPagination(resp) };
- },
- });
-
-export const POST = (req: NextRequest) =>
- buildHandler({
- req,
- bodySchema: zNewHighlightSchema,
- handler: async ({ body, api }) => {
- const resp = await api.highlights.create(body!);
- return { status: 201, resp };
- },
- });
diff --git a/apps/web/app/api/v1/lists/[listId]/bookmarks/[bookmarkId]/route.ts b/apps/web/app/api/v1/lists/[listId]/bookmarks/[bookmarkId]/route.ts
deleted file mode 100644
index 6efe2055..00000000
--- a/apps/web/app/api/v1/lists/[listId]/bookmarks/[bookmarkId]/route.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const PUT = (
- req: NextRequest,
- { params }: { params: { listId: string; bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- // TODO: PUT is supposed to be idempotent, but we currently fail if the bookmark is already in the list.
- await api.lists.addToList({
- listId: params.listId,
- bookmarkId: params.bookmarkId,
- });
- return { status: 204 };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- { params }: { params: { listId: string; bookmarkId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- await api.lists.removeFromList({
- listId: params.listId,
- bookmarkId: params.bookmarkId,
- });
- return { status: 204 };
- },
- });
diff --git a/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts b/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts
deleted file mode 100644
index daf78449..00000000
--- a/apps/web/app/api/v1/lists/[listId]/bookmarks/route.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-import { adaptPagination, zPagination } from "@/app/api/v1/utils/pagination";
-import { zGetBookmarkQueryParamsSchema } from "@/app/api/v1/utils/types";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest, params: { params: { listId: string } }) =>
- buildHandler({
- req,
- searchParamsSchema: zPagination.and(zGetBookmarkQueryParamsSchema),
- handler: async ({ api, searchParams }) => {
- const bookmarks = await api.bookmarks.getBookmarks({
- listId: params.params.listId,
- ...searchParams,
- });
- return { status: 200, resp: adaptPagination(bookmarks) };
- },
- });
diff --git a/apps/web/app/api/v1/lists/[listId]/route.ts b/apps/web/app/api/v1/lists/[listId]/route.ts
deleted file mode 100644
index 2cddbfdb..00000000
--- a/apps/web/app/api/v1/lists/[listId]/route.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zEditBookmarkListSchema } from "@karakeep/shared/types/lists";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- { params }: { params: { listId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const list = await api.lists.get({
- listId: params.listId,
- });
- return {
- status: 200,
- resp: list,
- };
- },
- });
-
-export const PATCH = (
- req: NextRequest,
- { params }: { params: { listId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: zEditBookmarkListSchema.omit({ listId: true }),
- handler: async ({ api, body }) => {
- const list = await api.lists.edit({
- ...body!,
- listId: params.listId,
- });
- return { status: 200, resp: list };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- { params }: { params: { listId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- await api.lists.delete({
- listId: params.listId,
- });
- return {
- status: 204,
- };
- },
- });
diff --git a/apps/web/app/api/v1/lists/route.ts b/apps/web/app/api/v1/lists/route.ts
deleted file mode 100644
index 5def2506..00000000
--- a/apps/web/app/api/v1/lists/route.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { NextRequest } from "next/server";
-
-import { zNewBookmarkListSchema } from "@karakeep/shared/types/lists";
-
-import { buildHandler } from "../utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const lists = await api.lists.list();
- return { status: 200, resp: lists };
- },
- });
-
-export const POST = (req: NextRequest) =>
- buildHandler({
- req,
- bodySchema: zNewBookmarkListSchema,
- handler: async ({ api, body }) => {
- const list = await api.lists.create(body!);
- return { status: 201, resp: list };
- },
- });
diff --git a/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts b/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts
deleted file mode 100644
index aaa5087b..00000000
--- a/apps/web/app/api/v1/tags/[tagId]/bookmarks/route.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-import { adaptPagination, zPagination } from "@/app/api/v1/utils/pagination";
-import { zGetBookmarkQueryParamsSchema } from "@/app/api/v1/utils/types";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- { params }: { params: { tagId: string } },
-) =>
- buildHandler({
- req,
- searchParamsSchema: zPagination.and(zGetBookmarkQueryParamsSchema),
- handler: async ({ api, searchParams }) => {
- const bookmarks = await api.bookmarks.getBookmarks({
- tagId: params.tagId,
- sortOrder: searchParams.sortOrder,
- limit: searchParams.limit,
- cursor: searchParams.cursor,
- });
- return {
- status: 200,
- resp: adaptPagination(bookmarks),
- };
- },
- });
diff --git a/apps/web/app/api/v1/tags/[tagId]/route.ts b/apps/web/app/api/v1/tags/[tagId]/route.ts
deleted file mode 100644
index 234d952d..00000000
--- a/apps/web/app/api/v1/tags/[tagId]/route.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { NextRequest } from "next/server";
-import { buildHandler } from "@/app/api/v1/utils/handler";
-
-import { zUpdateTagRequestSchema } from "@karakeep/shared/types/tags";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (
- req: NextRequest,
- { params }: { params: { tagId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const tag = await api.tags.get({
- tagId: params.tagId,
- });
- return {
- status: 200,
- resp: tag,
- };
- },
- });
-
-export const PATCH = (
- req: NextRequest,
- { params }: { params: { tagId: string } },
-) =>
- buildHandler({
- req,
- bodySchema: zUpdateTagRequestSchema.omit({ tagId: true }),
- handler: async ({ api, body }) => {
- const tag = await api.tags.update({
- tagId: params.tagId,
- ...body!,
- });
- return { status: 200, resp: tag };
- },
- });
-
-export const DELETE = (
- req: NextRequest,
- { params }: { params: { tagId: string } },
-) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- await api.tags.delete({
- tagId: params.tagId,
- });
- return {
- status: 204,
- };
- },
- });
diff --git a/apps/web/app/api/v1/tags/route.ts b/apps/web/app/api/v1/tags/route.ts
deleted file mode 100644
index 9625820c..00000000
--- a/apps/web/app/api/v1/tags/route.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { NextRequest } from "next/server";
-
-import { buildHandler } from "../utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const tags = await api.tags.list();
- return { status: 200, resp: tags };
- },
- });
diff --git a/apps/web/app/api/v1/users/me/route.ts b/apps/web/app/api/v1/users/me/route.ts
deleted file mode 100644
index bf0a3ba2..00000000
--- a/apps/web/app/api/v1/users/me/route.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { NextRequest } from "next/server";
-
-import { buildHandler } from "../../utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const user = await api.users.whoami();
- return { status: 200, resp: user };
- },
- });
diff --git a/apps/web/app/api/v1/users/me/stats/route.ts b/apps/web/app/api/v1/users/me/stats/route.ts
deleted file mode 100644
index 359c3156..00000000
--- a/apps/web/app/api/v1/users/me/stats/route.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { NextRequest } from "next/server";
-
-import { buildHandler } from "../../../utils/handler";
-
-export const dynamic = "force-dynamic";
-
-export const GET = (req: NextRequest) =>
- buildHandler({
- req,
- handler: async ({ api }) => {
- const stats = await api.users.stats();
- return { status: 200, resp: stats };
- },
- });
diff --git a/apps/web/app/api/v1/utils/handler.ts b/apps/web/app/api/v1/utils/handler.ts
deleted file mode 100644
index 9154506d..00000000
--- a/apps/web/app/api/v1/utils/handler.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import { NextRequest } from "next/server";
-import {
- createContextFromRequest,
- createTrcpClientFromCtx,
-} from "@/server/api/client";
-import { TRPCError } from "@trpc/server";
-import { z, ZodError } from "zod";
-
-import { Context } from "@karakeep/trpc";
-
-function trpcCodeToHttpCode(code: TRPCError["code"]) {
- switch (code) {
- case "BAD_REQUEST":
- case "PARSE_ERROR":
- return 400;
- case "UNAUTHORIZED":
- return 401;
- case "FORBIDDEN":
- return 403;
- case "NOT_FOUND":
- return 404;
- case "METHOD_NOT_SUPPORTED":
- return 405;
- case "TIMEOUT":
- return 408;
- case "PAYLOAD_TOO_LARGE":
- return 413;
- case "INTERNAL_SERVER_ERROR":
- return 500;
- default:
- return 500;
- }
-}
-
-interface ErrorMessage {
- path: (string | number)[];
- message: string;
-}
-
-function formatZodError(error: ZodError): string {
- if (!error.issues) {
- return error.message || "An unknown error occurred";
- }
-
- const errors: ErrorMessage[] = error.issues.map((issue) => ({
- path: issue.path,
- message: issue.message,
- }));
-
- const formattedErrors = errors.map((err) => {
- const path = err.path.join(".");
- return path ? `${path}: ${err.message}` : err.message;
- });
-
- return `${formattedErrors.join(", ")}`;
-}
-
-export interface TrpcAPIRequest<SearchParamsT, BodyType> {
- ctx: Context;
- api: ReturnType<typeof createTrcpClientFromCtx>;
- searchParams: SearchParamsT extends z.ZodTypeAny
- ? z.infer<SearchParamsT>
- : undefined;
- body: BodyType extends z.ZodTypeAny
- ? z.infer<BodyType> | undefined
- : undefined;
-}
-
-type SchemaType<T> = T extends z.ZodTypeAny
- ? z.infer<T> | undefined
- : undefined;
-
-export async function buildHandler<
- SearchParamsT extends z.ZodTypeAny | undefined,
- BodyT extends z.ZodTypeAny | undefined,
- InputT extends TrpcAPIRequest<SearchParamsT, BodyT>,
->({
- req,
- handler,
- searchParamsSchema,
- bodySchema,
-}: {
- req: NextRequest;
- handler: (req: InputT) => Promise<{ status: number; resp?: object }>;
- searchParamsSchema?: SearchParamsT | undefined;
- bodySchema?: BodyT | undefined;
-}) {
- try {
- const ctx = await createContextFromRequest(req);
- const api = createTrcpClientFromCtx(ctx);
-
- let searchParams: SchemaType<SearchParamsT> | undefined = undefined;
- if (searchParamsSchema !== undefined) {
- searchParams = searchParamsSchema.parse(
- Object.fromEntries(req.nextUrl.searchParams.entries()),
- ) as SchemaType<SearchParamsT>;
- }
-
- let body: SchemaType<BodyT> | undefined = undefined;
- if (bodySchema) {
- if (req.headers.get("Content-Type") !== "application/json") {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "Content-Type must be application/json",
- });
- }
-
- let bodyJson = undefined;
- try {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- bodyJson = await req.json();
- } catch (e) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: `Invalid JSON: ${(e as Error).message}`,
- });
- }
- body = bodySchema.parse(bodyJson) as SchemaType<BodyT>;
- }
-
- const { status, resp } = await handler({
- ctx,
- api,
- searchParams,
- body,
- } as InputT);
-
- return new Response(resp ? JSON.stringify(resp) : null, {
- status,
- headers: {
- "Content-Type": "application/json",
- },
- });
- } catch (e) {
- if (e instanceof ZodError) {
- return new Response(
- JSON.stringify({ code: "ParseError", message: formatZodError(e) }),
- {
- status: 400,
- headers: {
- "Content-Type": "application/json",
- },
- },
- );
- }
- if (e instanceof TRPCError) {
- let message = e.message;
- if (e.cause instanceof ZodError) {
- message = formatZodError(e.cause);
- }
- return new Response(JSON.stringify({ code: e.code, error: message }), {
- status: trpcCodeToHttpCode(e.code),
- headers: {
- "Content-Type": "application/json",
- },
- });
- } else {
- const error = e as Error;
- console.error(
- `Unexpected error in: ${req.method} ${req.nextUrl.pathname}:\n${error.stack}`,
- );
- return new Response(JSON.stringify({ code: "UnknownError" }), {
- status: 500,
- headers: {
- "Content-Type": "application/json",
- },
- });
- }
- }
-}
diff --git a/apps/web/app/api/v1/utils/pagination.ts b/apps/web/app/api/v1/utils/pagination.ts
deleted file mode 100644
index 12a0b950..00000000
--- a/apps/web/app/api/v1/utils/pagination.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { z } from "zod";
-
-import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@karakeep/shared/types/bookmarks";
-import { zCursorV2 } from "@karakeep/shared/types/pagination";
-
-export const zPagination = z.object({
- limit: z.coerce.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(),
- cursor: z
- .string()
- .refine((val) => val.includes("_"), "Must be a valid cursor")
- .transform((val) => {
- const [id, createdAt] = val.split("_");
- return { id, createdAt };
- })
- .pipe(z.object({ id: z.string(), createdAt: z.coerce.date() }))
- .optional(),
-});
-
-export function adaptPagination<
- T extends { nextCursor: z.infer<typeof zCursorV2> | null },
->(input: T) {
- const { nextCursor, ...rest } = input;
- if (!nextCursor) {
- return input;
- }
- return {
- ...rest,
- nextCursor: `${nextCursor.id}_${nextCursor.createdAt.toISOString()}`,
- };
-}
diff --git a/apps/web/app/api/v1/utils/types.ts b/apps/web/app/api/v1/utils/types.ts
deleted file mode 100644
index bf181ce4..00000000
--- a/apps/web/app/api/v1/utils/types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { z } from "zod";
-
-import { zSortOrder } from "@karakeep/shared/types/bookmarks";
-
-export const zStringBool = z
- .string()
- .refine((val) => val === "true" || val === "false", "Must be true or false")
- .transform((val) => val === "true");
-
-export const zGetBookmarkQueryParamsSchema = z.object({
- sortOrder: zSortOrder
- .exclude([zSortOrder.Enum.relevance])
- .optional()
- .default(zSortOrder.Enum.desc),
- // TODO: Change the default to false in a couple of releases.
- includeContent: zStringBool.optional().default("true"),
-});
-
-export const zGetBookmarkSearchParamsSchema = z.object({
- sortOrder: zSortOrder.optional().default(zSortOrder.Enum.relevance),
- // TODO: Change the default to false in a couple of releases.
- includeContent: zStringBool.optional().default("true"),
-});