diff options
| author | MohamedBassem <me@mbassem.com> | 2025-04-07 01:03:26 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2025-04-08 03:48:12 -0700 |
| commit | 3207264fc13c275d6dcfbd2628cc6b3974ceeaed (patch) | |
| tree | d426ffe0fe6bc3b9e692d96af94aa8d5d2a51162 /packages | |
| parent | 817eb58832a3e715e21892417b7624f4b1cf0d46 (diff) | |
| download | karakeep-3207264fc13c275d6dcfbd2628cc6b3974ceeaed.tar.zst | |
feat: Allow editing bookmark details
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/shared-react/utils/bookmarkUtils.ts | 17 | ||||
| -rw-r--r-- | packages/shared/types/bookmarks.ts | 10 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.test.ts | 63 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 141 |
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 |
