diff options
| -rw-r--r-- | apps/web/app/dashboard/tags/[tagName]/page.tsx | 36 | ||||
| -rw-r--r-- | packages/trpc/routers/_app.ts | 2 | ||||
| -rw-r--r-- | packages/trpc/routers/tags.ts | 99 |
3 files changed, 113 insertions, 24 deletions
diff --git a/apps/web/app/dashboard/tags/[tagName]/page.tsx b/apps/web/app/dashboard/tags/[tagName]/page.tsx index 51b3cb0b..0c5c1c1f 100644 --- a/apps/web/app/dashboard/tags/[tagName]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagName]/page.tsx @@ -2,10 +2,7 @@ import { notFound, redirect } from "next/navigation"; import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid"; import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; -import { and, eq } from "drizzle-orm"; - -import { db } from "@hoarder/db"; -import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema"; +import { TRPCError } from "@trpc/server"; export default async function TagPage({ params, @@ -17,30 +14,21 @@ export default async function TagPage({ redirect("/"); } const tagName = decodeURIComponent(params.tagName); - const tag = await db.query.bookmarkTags.findFirst({ - where: and( - eq(bookmarkTags.userId, session.user.id), - eq(bookmarkTags.name, tagName), - ), - columns: { - id: true, - }, - }); - if (!tag) { - // TODO: Better error message when the tag is not there - notFound(); + let tag; + try { + tag = await api.tags.get({ tagName }); + } catch (e) { + if (e instanceof TRPCError) { + if (e.code == "NOT_FOUND") { + notFound(); + } + } + throw e; } - const bookmarkIds = await db.query.tagsOnBookmarks.findMany({ - where: eq(tagsOnBookmarks.tagId, tag.id), - columns: { - bookmarkId: true, - }, - }); - const query = { - ids: bookmarkIds.map((b) => b.bookmarkId), + ids: tag.bookmarks, archived: false, }; diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index 6e5dd91d..780fd76d 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -3,12 +3,14 @@ import { adminAppRouter } from "./admin"; import { apiKeysAppRouter } from "./apiKeys"; import { bookmarksAppRouter } from "./bookmarks"; import { listsAppRouter } from "./lists"; +import { tagsAppRouter } from "./tags"; import { usersAppRouter } from "./users"; export const appRouter = router({ bookmarks: bookmarksAppRouter, apiKeys: apiKeysAppRouter, users: usersAppRouter, lists: listsAppRouter, + tags: tagsAppRouter, admin: adminAppRouter, }); // export type definition of API diff --git a/packages/trpc/routers/tags.ts b/packages/trpc/routers/tags.ts new file mode 100644 index 00000000..af11f34c --- /dev/null +++ b/packages/trpc/routers/tags.ts @@ -0,0 +1,99 @@ +import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { bookmarks, bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema"; + +import type { Context } from "../index"; +import { authedProcedure, router } from "../index"; + +function conditionFromInput(input: { tagName: string } | { tagId: string }) { + if ("tagName" in input) { + return eq(bookmarkTags.name, input.tagName); + } else { + return eq(bookmarkTags.id, input.tagId); + } +} + +const ensureTagOwnership = experimental_trpcMiddleware<{ + ctx: Context; + input: { tagName: string } | { tagId: string }; +}>().create(async (opts) => { + const tag = await opts.ctx.db.query.bookmarkTags.findFirst({ + where: conditionFromInput(opts.input), + columns: { + userId: true, + }, + }); + if (!opts.ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User is not authorized", + }); + } + if (!tag) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Tag not found", + }); + } + if (tag.userId != opts.ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access resource", + }); + } + + return opts.next(); +}); + +export const tagsAppRouter = router({ + get: authedProcedure + .input( + z + .object({ + tagId: z.string(), + }) + .or( + z.object({ + tagName: z.string(), + }), + ), + ) + .output( + z.object({ + id: z.string(), + name: z.string(), + bookmarks: z.array(z.string()), + }), + ) + .use(ensureTagOwnership) + .query(async ({ input, ctx }) => { + const res = await ctx.db + .select({ + id: bookmarkTags.id, + name: bookmarkTags.name, + bookmarkId: bookmarks.id, + }) + .from(bookmarkTags) + .leftJoin(tagsOnBookmarks, eq(bookmarkTags.id, tagsOnBookmarks.tagId)) + .leftJoin(bookmarks, eq(tagsOnBookmarks.bookmarkId, bookmarks.id)) + .where( + and( + conditionFromInput(input), + eq(bookmarkTags.userId, ctx.user.id), + eq(bookmarks.archived, false), + ), + ); + + if (res.length == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + return { + id: res[0].id, + name: res[0].name, + bookmarks: res.flatMap((t) => t.bookmarkId ? [t.bookmarkId] : []), + }; + }), +}); |
