aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-15 13:32:05 +0000
committerMohamedBassem <me@mbassem.com>2024-03-15 13:32:05 +0000
commita9e4ec0922b5da71aaae7f6a037f124c86b38edc (patch)
tree67d3470ef6ef2f7bbd6971614251697b10c172a1
parentaa8df79ab48c2e2d9f8b10fbdf4d71197f41303e (diff)
downloadkarakeep-a9e4ec0922b5da71aaae7f6a037f124c86b38edc.tar.zst
refactor: Move tag fetching to trpc to reuse in the mobile app
-rw-r--r--apps/web/app/dashboard/tags/[tagName]/page.tsx36
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/tags.ts99
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] : []),
+ };
+ }),
+});