aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/server/api/routers/bookmarks.ts
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-05 13:11:06 +0000
committerMohamedBassem <me@mbassem.com>2024-03-05 13:11:06 +0000
commit8a46ecb7373d6c5e7300861169ea51a7917cd2b4 (patch)
tree4ad318c3b5fc8b7a74cba6d0e37b6ade24db829a /packages/web/server/api/routers/bookmarks.ts
parent224aa38d5976523f213e2860b6addc7630d472ba (diff)
downloadkarakeep-8a46ecb7373d6c5e7300861169ea51a7917cd2b4.tar.zst
refactor: Extract trpc logic into its package
Diffstat (limited to 'packages/web/server/api/routers/bookmarks.ts')
-rw-r--r--packages/web/server/api/routers/bookmarks.ts454
1 files changed, 0 insertions, 454 deletions
diff --git a/packages/web/server/api/routers/bookmarks.ts b/packages/web/server/api/routers/bookmarks.ts
deleted file mode 100644
index 73818508..00000000
--- a/packages/web/server/api/routers/bookmarks.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-import { z } from "zod";
-import { Context, authedProcedure, router } from "../trpc";
-import { getSearchIdxClient } from "@hoarder/shared/search";
-import {
- ZBookmark,
- ZBookmarkContent,
- zBareBookmarkSchema,
- zBookmarkSchema,
- zGetBookmarksRequestSchema,
- zGetBookmarksResponseSchema,
- zNewBookmarkRequestSchema,
- zUpdateBookmarksRequestSchema,
-} from "@/lib/types/api/bookmarks";
-import {
- bookmarkLinks,
- bookmarkTags,
- bookmarkTexts,
- bookmarks,
- tagsOnBookmarks,
-} from "@hoarder/db/schema";
-import {
- LinkCrawlerQueue,
- OpenAIQueue,
- SearchIndexingQueue,
-} from "@hoarder/shared/queues";
-import { TRPCError, experimental_trpcMiddleware } from "@trpc/server";
-import { and, desc, eq, inArray } from "drizzle-orm";
-import { ZBookmarkTags } from "@/lib/types/api/tags";
-
-import { db as DONT_USE_db } from "@hoarder/db";
-
-const ensureBookmarkOwnership = experimental_trpcMiddleware<{
- ctx: Context;
- input: { bookmarkId: string };
-}>().create(async (opts) => {
- const bookmark = await opts.ctx.db.query.bookmarks.findFirst({
- where: eq(bookmarks.id, opts.input.bookmarkId),
- columns: {
- userId: true,
- },
- });
- if (!opts.ctx.user) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message: "User is not authorized",
- });
- }
- if (!bookmark) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Bookmark not found",
- });
- }
- if (bookmark.userId != opts.ctx.user.id) {
- throw new TRPCError({
- code: "FORBIDDEN",
- message: "User is not allowed to access resource",
- });
- }
-
- return opts.next();
-});
-
-async function dummyDrizzleReturnType() {
- const x = await DONT_USE_db.query.bookmarks.findFirst({
- with: {
- tagsOnBookmarks: {
- with: {
- tag: true,
- },
- },
- link: true,
- text: true,
- },
- });
- if (!x) {
- throw new Error();
- }
- return x;
-}
-
-function toZodSchema(
- bookmark: Awaited<ReturnType<typeof dummyDrizzleReturnType>>,
-): ZBookmark {
- const { tagsOnBookmarks, link, text, ...rest } = bookmark;
-
- let content: ZBookmarkContent;
- if (link) {
- content = { type: "link", ...link };
- } else if (text) {
- content = { type: "text", text: text.text || "" };
- } else {
- throw new Error("Unknown content type");
- }
-
- return {
- tags: tagsOnBookmarks.map((t) => ({
- attachedBy: t.attachedBy,
- ...t.tag,
- })),
- content,
- ...rest,
- };
-}
-
-export const bookmarksAppRouter = router({
- createBookmark: authedProcedure
- .input(zNewBookmarkRequestSchema)
- .output(zBookmarkSchema)
- .mutation(async ({ input, ctx }) => {
- const bookmark = await ctx.db.transaction(
- async (tx): Promise<ZBookmark> => {
- const bookmark = (
- await tx
- .insert(bookmarks)
- .values({
- userId: ctx.user.id,
- })
- .returning()
- )[0];
-
- let content: ZBookmarkContent;
-
- switch (input.type) {
- case "link": {
- const link = (
- await tx
- .insert(bookmarkLinks)
- .values({
- id: bookmark.id,
- url: input.url,
- })
- .returning()
- )[0];
- content = {
- type: "link",
- ...link,
- };
- break;
- }
- case "text": {
- const text = (
- await tx
- .insert(bookmarkTexts)
- .values({ id: bookmark.id, text: input.text })
- .returning()
- )[0];
- content = {
- type: "text",
- text: text.text || "",
- };
- break;
- }
- }
-
- return {
- tags: [] as ZBookmarkTags[],
- content,
- ...bookmark,
- };
- },
- );
-
- // Enqueue crawling request
- switch (bookmark.content.type) {
- case "link": {
- // The crawling job triggers openai when it's done
- await LinkCrawlerQueue.add("crawl", {
- bookmarkId: bookmark.id,
- });
- break;
- }
- case "text": {
- await OpenAIQueue.add("openai", {
- bookmarkId: bookmark.id,
- });
- break;
- }
- }
- SearchIndexingQueue.add("search_indexing", {
- bookmarkId: bookmark.id,
- type: "index",
- });
- return bookmark;
- }),
-
- updateBookmark: authedProcedure
- .input(zUpdateBookmarksRequestSchema)
- .output(zBareBookmarkSchema)
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- const res = await ctx.db
- .update(bookmarks)
- .set({
- archived: input.archived,
- favourited: input.favourited,
- })
- .where(
- and(
- eq(bookmarks.userId, ctx.user.id),
- eq(bookmarks.id, input.bookmarkId),
- ),
- )
- .returning();
- if (res.length == 0) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Bookmark not found",
- });
- }
- return res[0];
- }),
-
- updateBookmarkText: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- text: z.string().max(2000),
- }),
- )
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- const res = await ctx.db
- .update(bookmarkTexts)
- .set({
- text: input.text,
- })
- .where(and(eq(bookmarkTexts.id, input.bookmarkId)))
- .returning();
- if (res.length == 0) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Bookmark not found",
- });
- }
- SearchIndexingQueue.add("search_indexing", {
- bookmarkId: input.bookmarkId,
- type: "index",
- });
- }),
-
- deleteBookmark: authedProcedure
- .input(z.object({ bookmarkId: z.string() }))
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- await ctx.db
- .delete(bookmarks)
- .where(
- and(
- eq(bookmarks.userId, ctx.user.id),
- eq(bookmarks.id, input.bookmarkId),
- ),
- );
- SearchIndexingQueue.add("search_indexing", {
- bookmarkId: input.bookmarkId,
- type: "delete",
- });
- }),
- recrawlBookmark: authedProcedure
- .input(z.object({ bookmarkId: z.string() }))
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input }) => {
- await LinkCrawlerQueue.add("crawl", {
- bookmarkId: input.bookmarkId,
- });
- }),
- getBookmark: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- }),
- )
- .output(zBookmarkSchema)
- .use(ensureBookmarkOwnership)
- .query(async ({ input, ctx }) => {
- const bookmark = await ctx.db.query.bookmarks.findFirst({
- where: and(
- eq(bookmarks.userId, ctx.user.id),
- eq(bookmarks.id, input.bookmarkId),
- ),
- with: {
- tagsOnBookmarks: {
- with: {
- tag: true,
- },
- },
- link: true,
- text: true,
- },
- });
- if (!bookmark) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Bookmark not found",
- });
- }
-
- return toZodSchema(bookmark);
- }),
- searchBookmarks: authedProcedure
- .input(
- z.object({
- text: z.string(),
- }),
- )
- .output(zGetBookmarksResponseSchema)
- .query(async ({ input, ctx }) => {
- const client = await getSearchIdxClient();
- if (!client) {
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Search functionality is not configured",
- });
- }
- const resp = await client.search(input.text, {
- filter: [`userId = '${ctx.user.id}'`],
- });
-
- if (resp.hits.length == 0) {
- return { bookmarks: [] };
- }
- const results = await ctx.db.query.bookmarks.findMany({
- where: and(
- eq(bookmarks.userId, ctx.user.id),
- inArray(
- bookmarks.id,
- resp.hits.map((h) => h.id),
- ),
- ),
- with: {
- tagsOnBookmarks: {
- with: {
- tag: true,
- },
- },
- link: true,
- text: true,
- },
- });
-
- return { bookmarks: results.map(toZodSchema) };
- }),
- getBookmarks: authedProcedure
- .input(zGetBookmarksRequestSchema)
- .output(zGetBookmarksResponseSchema)
- .query(async ({ input, ctx }) => {
- if (input.ids && input.ids.length == 0) {
- return { bookmarks: [] };
- }
- const results = await ctx.db.query.bookmarks.findMany({
- where: and(
- eq(bookmarks.userId, ctx.user.id),
- input.archived !== undefined
- ? eq(bookmarks.archived, input.archived)
- : undefined,
- input.favourited !== undefined
- ? eq(bookmarks.favourited, input.favourited)
- : undefined,
- input.ids ? inArray(bookmarks.id, input.ids) : undefined,
- ),
- orderBy: [desc(bookmarks.createdAt)],
- with: {
- tagsOnBookmarks: {
- with: {
- tag: true,
- },
- },
- link: true,
- text: true,
- },
- });
-
- return { bookmarks: results.map(toZodSchema) };
- }),
-
- updateTags: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- attach: z.array(
- z.object({
- tagId: z.string().optional(), // If the tag already exists and we know its id
- tag: z.string(),
- }),
- ),
- // Detach by tag ids
- detach: z.array(z.object({ tagId: z.string() })),
- }),
- )
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- await ctx.db.transaction(async (tx) => {
- // Detaches
- if (input.detach.length > 0) {
- await tx.delete(tagsOnBookmarks).where(
- and(
- eq(tagsOnBookmarks.bookmarkId, input.bookmarkId),
- inArray(
- tagsOnBookmarks.tagId,
- input.detach.map((t) => t.tagId),
- ),
- ),
- );
- }
-
- if (input.attach.length == 0) {
- return;
- }
-
- // New Tags
- const toBeCreatedTags = input.attach
- .filter((i) => i.tagId === undefined)
- .map((i) => ({
- name: i.tag,
- userId: ctx.user.id,
- }));
-
- if (toBeCreatedTags.length > 0) {
- await tx
- .insert(bookmarkTags)
- .values(toBeCreatedTags)
- .onConflictDoNothing()
- .returning();
- }
-
- const allIds = (
- await tx.query.bookmarkTags.findMany({
- where: and(
- eq(bookmarkTags.userId, ctx.user.id),
- inArray(
- bookmarkTags.name,
- input.attach.map((t) => t.tag),
- ),
- ),
- columns: {
- id: true,
- },
- })
- ).map((t) => t.id);
-
- await tx
- .insert(tagsOnBookmarks)
- .values(
- allIds.map((i) => ({
- tagId: i as string,
- bookmarkId: input.bookmarkId,
- attachedBy: "human" as const,
- userId: ctx.user.id,
- })),
- )
- .onConflictDoNothing();
- });
- }),
-});