aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-12-27 16:09:29 +0000
committerMohamed Bassem <me@mbassem.com>2024-12-27 16:09:29 +0000
commit86d74e3f32dd5bccc8df195b55391e206df9a1c4 (patch)
tree0db92b5139f9ef0c8909c16db72fbef782b770b6 /packages/trpc/routers
parenta23044bb74e01c861a92417c00d293ff86384e83 (diff)
downloadkarakeep-86d74e3f32dd5bccc8df195b55391e206df9a1c4.tar.zst
feat: Implement highlights support for links. Fixes #620
Diffstat (limited to 'packages/trpc/routers')
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/highlights.ts125
2 files changed, 127 insertions, 0 deletions
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];
+ }),
+});