aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2025-04-07 01:03:26 +0100
committerMohamedBassem <me@mbassem.com>2025-04-08 03:48:12 -0700
commit3207264fc13c275d6dcfbd2628cc6b3974ceeaed (patch)
treed426ffe0fe6bc3b9e692d96af94aa8d5d2a51162 /packages
parent817eb58832a3e715e21892417b7624f4b1cf0d46 (diff)
downloadkarakeep-3207264fc13c275d6dcfbd2628cc6b3974ceeaed.tar.zst
feat: Allow editing bookmark details
Diffstat (limited to 'packages')
-rw-r--r--packages/shared-react/utils/bookmarkUtils.ts17
-rw-r--r--packages/shared/types/bookmarks.ts10
-rw-r--r--packages/trpc/routers/bookmarks.test.ts63
-rw-r--r--packages/trpc/routers/bookmarks.ts141
4 files changed, 204 insertions, 27 deletions
diff --git a/packages/shared-react/utils/bookmarkUtils.ts b/packages/shared-react/utils/bookmarkUtils.ts
index 0a089f64..fc0fd97d 100644
--- a/packages/shared-react/utils/bookmarkUtils.ts
+++ b/packages/shared-react/utils/bookmarkUtils.ts
@@ -51,3 +51,20 @@ export function getSourceUrl(bookmark: ZBookmark) {
}
return null;
}
+
+export function getBookmarkTitle(bookmark: ZBookmark) {
+ let title: string | null = null;
+ switch (bookmark.content.type) {
+ case BookmarkTypes.LINK:
+ title = bookmark.content.title ?? bookmark.content.url;
+ break;
+ case BookmarkTypes.TEXT:
+ title = null;
+ break;
+ case BookmarkTypes.ASSET:
+ title = bookmark.content.fileName ?? null;
+ break;
+ }
+
+ return bookmark.title ? bookmark.title : title;
+}
diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts
index af7474ad..883dda30 100644
--- a/packages/shared/types/bookmarks.ts
+++ b/packages/shared/types/bookmarks.ts
@@ -196,6 +196,16 @@ export const zUpdateBookmarksRequestSchema = z.object({
note: z.string().optional(),
title: z.string().max(MAX_TITLE_LENGTH).nullish(),
createdAt: z.coerce.date().optional(),
+ // Link specific fields (optional)
+ url: z.string().url().optional(),
+ description: z.string().nullish(),
+ author: z.string().nullish(),
+ publisher: z.string().nullish(),
+ datePublished: z.coerce.date().nullish(),
+ dateModified: z.coerce.date().nullish(),
+
+ // Text specific fields (optional)
+ text: z.string().nullish(),
});
export type ZUpdateBookmarksRequest = z.infer<
typeof zUpdateBookmarksRequestSchema
diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts
index d89f80fd..c3469acc 100644
--- a/packages/trpc/routers/bookmarks.test.ts
+++ b/packages/trpc/routers/bookmarks.test.ts
@@ -60,9 +60,70 @@ describe("Bookmark Routes", () => {
favourited: true,
});
- const res = await api.getBookmark({ bookmarkId: bookmark.id });
+ let res = await api.getBookmark({ bookmarkId: bookmark.id });
expect(res.archived).toBeTruthy();
expect(res.favourited).toBeTruthy();
+
+ // Update other common fields
+ const newDate = new Date(Date.now() - 1000 * 60 * 60 * 24); // Yesterday
+ newDate.setMilliseconds(0);
+ await api.updateBookmark({
+ bookmarkId: bookmark.id,
+ title: "New Title",
+ note: "Test Note",
+ summary: "Test Summary",
+ createdAt: newDate,
+ });
+
+ res = await api.getBookmark({ bookmarkId: bookmark.id });
+ expect(res.title).toEqual("New Title");
+ expect(res.note).toEqual("Test Note");
+ expect(res.summary).toEqual("Test Summary");
+ expect(res.createdAt).toEqual(newDate);
+
+ // Update link-specific fields
+ const linkUpdateDate = new Date(Date.now() - 1000 * 60 * 60 * 48); // 2 days ago
+ linkUpdateDate.setMilliseconds(0);
+ await api.updateBookmark({
+ bookmarkId: bookmark.id,
+ url: "https://new-google.com",
+ description: "New Description",
+ author: "New Author",
+ publisher: "New Publisher",
+ datePublished: linkUpdateDate,
+ dateModified: linkUpdateDate,
+ });
+
+ res = await api.getBookmark({ bookmarkId: bookmark.id });
+ assert(res.content.type === BookmarkTypes.LINK);
+ expect(res.content.url).toEqual("https://new-google.com");
+ expect(res.content.description).toEqual("New Description");
+ expect(res.content.author).toEqual("New Author");
+ expect(res.content.publisher).toEqual("New Publisher");
+ expect(res.content.datePublished).toEqual(linkUpdateDate);
+ expect(res.content.dateModified).toEqual(linkUpdateDate);
+ });
+
+ test<CustomTestContext>("update bookmark - non-link type error", async ({
+ apiCallers,
+ }) => {
+ const api = apiCallers[0].bookmarks;
+
+ // Create a TEXT bookmark
+ const bookmark = await api.createBookmark({
+ text: "Initial text",
+ type: BookmarkTypes.TEXT,
+ });
+
+ // Attempt to update link-specific fields
+ await expect(() =>
+ api.updateBookmark({
+ bookmarkId: bookmark.id,
+ url: "https://should-fail.com", // Link-specific field
+ }),
+ ).rejects.toThrow(
+ /Attempting to set link attributes for non-link type bookmark/,
+ );
});
test<CustomTestContext>("list bookmarks", async ({ apiCallers }) => {
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index c97383cb..9219adc6 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -54,7 +54,6 @@ import { parseSearchQuery } from "@hoarder/shared/searchQueryParser";
import {
BookmarkTypes,
DEFAULT_NUM_BOOKMARKS_PER_PAGE,
- zBareBookmarkSchema,
zBookmarkSchema,
zGetBookmarksRequestSchema,
zGetBookmarksResponseSchema,
@@ -419,35 +418,125 @@ export const bookmarksAppRouter = router({
updateBookmark: authedProcedure
.input(zUpdateBookmarksRequestSchema)
- .output(zBareBookmarkSchema)
+ .output(zBookmarkSchema)
.use(ensureBookmarkOwnership)
.mutation(async ({ input, ctx }) => {
- const res = await ctx.db
- .update(bookmarks)
- .set({
- title: input.title,
- archived: input.archived,
- favourited: input.favourited,
- note: input.note,
- summary: input.summary,
- createdAt: input.createdAt,
- })
- .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",
- });
- }
+ await ctx.db.transaction(async (tx) => {
+ let somethingChanged = false;
+
+ // Update link-specific fields if any are provided
+ const linkUpdateData: Partial<{
+ url: string;
+ description: string | null;
+ author: string | null;
+ publisher: string | null;
+ datePublished: Date | null;
+ dateModified: Date | null;
+ }> = {};
+ if (input.url) {
+ linkUpdateData.url = input.url.trim();
+ }
+ if (input.description !== undefined) {
+ linkUpdateData.description = input.description;
+ }
+ if (input.author !== undefined) {
+ linkUpdateData.author = input.author;
+ }
+ if (input.publisher !== undefined) {
+ linkUpdateData.publisher = input.publisher;
+ }
+ if (input.datePublished !== undefined) {
+ linkUpdateData.datePublished = input.datePublished;
+ }
+ if (input.dateModified !== undefined) {
+ linkUpdateData.dateModified = input.dateModified;
+ }
+
+ if (Object.keys(linkUpdateData).length > 0) {
+ const result = await tx
+ .update(bookmarkLinks)
+ .set(linkUpdateData)
+ .where(eq(bookmarkLinks.id, input.bookmarkId));
+ if (result.changes == 0) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message:
+ "Attempting to set link attributes for non-link type bookmark",
+ });
+ }
+ somethingChanged = true;
+ }
+
+ if (input.text) {
+ const result = await tx
+ .update(bookmarkTexts)
+ .set({
+ text: input.text,
+ })
+ .where(eq(bookmarkLinks.id, input.bookmarkId));
+
+ if (result.changes == 0) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message:
+ "Attempting to set link attributes for non-text type bookmark",
+ });
+ }
+ somethingChanged = true;
+ }
+
+ // Update common bookmark fields
+ const commonUpdateData: Partial<{
+ title: string | null;
+ archived: boolean;
+ favourited: boolean;
+ note: string | null;
+ summary: string | null;
+ createdAt: Date;
+ modifiedAt: Date; // Always update modifiedAt
+ }> = {
+ modifiedAt: new Date(),
+ };
+ if (input.title !== undefined) {
+ commonUpdateData.title = input.title;
+ }
+ if (input.archived !== undefined) {
+ commonUpdateData.archived = input.archived;
+ }
+ if (input.favourited !== undefined) {
+ commonUpdateData.favourited = input.favourited;
+ }
+ if (input.note !== undefined) {
+ commonUpdateData.note = input.note;
+ }
+ if (input.summary !== undefined) {
+ commonUpdateData.summary = input.summary;
+ }
+ if (input.createdAt !== undefined) {
+ commonUpdateData.createdAt = input.createdAt;
+ }
+
+ if (Object.keys(commonUpdateData).length > 1 || somethingChanged) {
+ await tx
+ .update(bookmarks)
+ .set(commonUpdateData)
+ .where(
+ and(
+ eq(bookmarks.userId, ctx.user.id),
+ eq(bookmarks.id, input.bookmarkId),
+ ),
+ );
+ }
+ });
+
+ // Refetch the updated bookmark data to return the full object
+ const updatedBookmark = await getBookmark(ctx, input.bookmarkId);
+
+ // Trigger re-indexing and webhooks
await triggerSearchReindex(input.bookmarkId);
await triggerWebhook(input.bookmarkId, "edited");
- return res[0];
+
+ return updatedBookmark;
}),
updateBookmarkText: authedProcedure