From 86d74e3f32dd5bccc8df195b55391e206df9a1c4 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Fri, 27 Dec 2024 16:09:29 +0000 Subject: feat: Implement highlights support for links. Fixes #620 --- packages/trpc/routers/_app.ts | 2 + packages/trpc/routers/highlights.ts | 125 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 packages/trpc/routers/highlights.ts (limited to 'packages/trpc/routers') diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts index ea1e0ca8..91030d8e 100644 --- a/packages/trpc/routers/_app.ts +++ b/packages/trpc/routers/_app.ts @@ -3,6 +3,7 @@ import { adminAppRouter } from "./admin"; import { apiKeysAppRouter } from "./apiKeys"; import { bookmarksAppRouter } from "./bookmarks"; import { feedsAppRouter } from "./feeds"; +import { highlightsAppRouter } from "./highlights"; import { listsAppRouter } from "./lists"; import { promptsAppRouter } from "./prompts"; import { tagsAppRouter } from "./tags"; @@ -17,6 +18,7 @@ export const appRouter = router({ prompts: promptsAppRouter, admin: adminAppRouter, feeds: feedsAppRouter, + highlights: highlightsAppRouter, }); // export type definition of API export type AppRouter = typeof appRouter; diff --git a/packages/trpc/routers/highlights.ts b/packages/trpc/routers/highlights.ts new file mode 100644 index 00000000..14d0ffa9 --- /dev/null +++ b/packages/trpc/routers/highlights.ts @@ -0,0 +1,125 @@ +import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +import { highlights } from "@hoarder/db/schema"; +import { + zHighlightSchema, + zNewHighlightSchema, + zUpdateHighlightSchema, +} from "@hoarder/shared/types/highlights"; + +import { authedProcedure, Context, router } from "../index"; +import { ensureBookmarkOwnership } from "./bookmarks"; + +const ensureHighlightOwnership = experimental_trpcMiddleware<{ + ctx: Context; + input: { highlightId: string }; +}>().create(async (opts) => { + const highlight = await opts.ctx.db.query.highlights.findFirst({ + where: eq(highlights.id, opts.input.highlightId), + columns: { + userId: true, + }, + }); + if (!opts.ctx.user) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User is not authorized", + }); + } + if (!highlight) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Bookmark not found", + }); + } + if (highlight.userId != opts.ctx.user.id) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User is not allowed to access resource", + }); + } + + return opts.next(); +}); + +export const highlightsAppRouter = router({ + create: authedProcedure + .input(zNewHighlightSchema) + .output(zHighlightSchema) + .use(ensureBookmarkOwnership) + .mutation(async ({ input, ctx }) => { + const [result] = await ctx.db + .insert(highlights) + .values({ + bookmarkId: input.bookmarkId, + startOffset: input.startOffset, + endOffset: input.endOffset, + color: input.color, + text: input.text, + note: input.note, + userId: ctx.user.id, + }) + .returning(); + return result; + }), + getForBookmark: authedProcedure + .input(z.object({ bookmarkId: z.string() })) + .output(z.object({ highlights: z.array(zHighlightSchema) })) + .use(ensureBookmarkOwnership) + .query(async ({ input, ctx }) => { + const results = await ctx.db.query.highlights.findMany({ + where: and( + eq(highlights.bookmarkId, input.bookmarkId), + eq(highlights.userId, ctx.user.id), + ), + }); + return { highlights: results }; + }), + delete: authedProcedure + .input(z.object({ highlightId: z.string() })) + .output(zHighlightSchema) + .use(ensureHighlightOwnership) + .mutation(async ({ input, ctx }) => { + const result = await ctx.db + .delete(highlights) + .where( + and( + eq(highlights.id, input.highlightId), + eq(highlights.userId, ctx.user.id), + ), + ) + .returning(); + if (result.length == 0) { + throw new TRPCError({ + code: "NOT_FOUND", + }); + } + return result[0]; + }), + update: authedProcedure + .input(zUpdateHighlightSchema) + .output(zHighlightSchema) + .use(ensureHighlightOwnership) + .mutation(async ({ input, ctx }) => { + const result = await ctx.db + .update(highlights) + .set({ + color: input.color, + }) + .where( + and( + eq(highlights.id, input.highlightId), + eq(highlights.userId, ctx.user.id), + ), + ) + .returning(); + if (result.length == 0) { + throw new TRPCError({ + code: "NOT_FOUND", + }); + } + return result[0]; + }), +}); -- cgit v1.2.3-70-g09d2