diff options
| author | kamtschatka <simon.schatka@gmx.at> | 2024-07-01 13:03:53 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-01 12:03:53 +0100 |
| commit | e6486465decd612f7e437abe904960a47ff359ce (patch) | |
| tree | f36fd7efbcf2a083905061d8c5f1310f36350ced | |
| parent | ccbff18a9763e458c07f46cb3a331062df14a9b9 (diff) | |
| download | karakeep-e6486465decd612f7e437abe904960a47ff359ce.tar.zst | |
refactor: added the bookmark type to the database (#256)
* refactoring asset types
Extracted out functions to silently delete assets and to update them after crawling
Generalized the mapping of assets to bookmark fields to make extending them easier
* Added the bookmark type to the database
Introduced an enum to have better type safety
cleaned up the code and based some code on the type directly
* add BookmarkType.UNKNWON
* lint and remove unused function
---------
Co-authored-by: MohamedBassem <me@mbassem.com>
27 files changed, 1266 insertions, 120 deletions
diff --git a/apps/browser-extension/src/SavePage.tsx b/apps/browser-extension/src/SavePage.tsx index 9cc1521a..c6f85b3b 100644 --- a/apps/browser-extension/src/SavePage.tsx +++ b/apps/browser-extension/src/SavePage.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; +import { BookmarkTypes } from "../../../packages/shared/types/bookmarks"; import Spinner from "./Spinner"; import { api } from "./utils/trpc"; @@ -32,7 +33,7 @@ export default function SavePage() { } createBookmark({ - type: "link", + type: BookmarkTypes.LINK, url: currentUrl, }); } diff --git a/apps/cli/src/commands/bookmarks.ts b/apps/cli/src/commands/bookmarks.ts index 40442ec1..6efb41d9 100644 --- a/apps/cli/src/commands/bookmarks.ts +++ b/apps/cli/src/commands/bookmarks.ts @@ -9,7 +9,10 @@ import { getAPIClient } from "@/lib/trpc"; import { Command } from "@commander-js/extra-typings"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; -import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@hoarder/shared/types/bookmarks"; +import { + BookmarkTypes, + MAX_NUM_BOOKMARKS_PER_PAGE, +} from "@hoarder/shared/types/bookmarks"; export const bookmarkCmd = new Command() .name("bookmarks") @@ -26,7 +29,7 @@ function normalizeBookmark(bookmark: ZBookmark) { tags: bookmark.tags.map((t) => t.name), }; - if (ret.content.type == "link" && ret.content.htmlContent) { + if (ret.content.type == BookmarkTypes.LINK && ret.content.htmlContent) { if (ret.content.htmlContent.length > 10) { ret.content.htmlContent = ret.content.htmlContent.substring(0, 10) + "... <CROPPED>"; @@ -63,7 +66,7 @@ bookmarkCmd const promises = [ ...opts.link.map((url) => api.bookmarks.createBookmark - .mutate({ type: "link", url }) + .mutate({ type: BookmarkTypes.LINK, url }) .then((bookmark: ZBookmark) => { results.push(normalizeBookmark(bookmark)); }) @@ -71,7 +74,7 @@ bookmarkCmd ), ...opts.note.map((text) => api.bookmarks.createBookmark - .mutate({ type: "text", text }) + .mutate({ type: BookmarkTypes.TEXT, text }) .then((bookmark: ZBookmark) => { results.push(normalizeBookmark(bookmark)); }) @@ -87,7 +90,7 @@ bookmarkCmd const text = fs.readFileSync(0, "utf-8"); promises.push( api.bookmarks.createBookmark - .mutate({ type: "text", text }) + .mutate({ type: BookmarkTypes.TEXT, text }) .then((bookmark: ZBookmark) => { results.push(normalizeBookmark(bookmark)); }) diff --git a/apps/mobile/app/dashboard/add-link.tsx b/apps/mobile/app/dashboard/add-link.tsx index 5096a9e7..d9773fb4 100644 --- a/apps/mobile/app/dashboard/add-link.tsx +++ b/apps/mobile/app/dashboard/add-link.tsx @@ -6,6 +6,8 @@ import { Input } from "@/components/ui/Input"; import { useToast } from "@/components/ui/Toast"; import { api } from "@/lib/trpc"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; + export default function AddNote() { const [text, setText] = useState(""); const [error, setError] = useState<string | undefined>(); @@ -54,7 +56,7 @@ export default function AddNote() { autoFocus /> <Button - onPress={() => mutate({ type: "link", url: text })} + onPress={() => mutate({ type: BookmarkTypes.LINK, url: text })} label="Add Link" /> </View> diff --git a/apps/mobile/app/dashboard/add-note.tsx b/apps/mobile/app/dashboard/add-note.tsx index 40c97456..6b8f0fef 100644 --- a/apps/mobile/app/dashboard/add-note.tsx +++ b/apps/mobile/app/dashboard/add-note.tsx @@ -5,6 +5,8 @@ import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { api } from "@/lib/trpc"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; + export default function AddNote() { const [text, setText] = useState(""); const [error, setError] = useState<string | undefined>(); @@ -45,7 +47,10 @@ export default function AddNote() { placeholder="What's on your mind?" autoFocus /> - <Button onPress={() => mutate({ type: "text", text })} label="Add Note" /> + <Button + onPress={() => mutate({ type: BookmarkTypes.TEXT, text })} + label="Add Note" + /> </View> ); } diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx index 7339a017..d1d39e5b 100644 --- a/apps/mobile/app/sharing.tsx +++ b/apps/mobile/app/sharing.tsx @@ -7,7 +7,7 @@ import { api } from "@/lib/trpc"; import { useUploadAsset } from "@/lib/upload"; import { z } from "zod"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; type Mode = | { type: "idle" } @@ -45,9 +45,9 @@ function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) { const val = z.string().url(); if (val.safeParse(shareIntent.text).success) { // This is a URL, else treated as text - mutate({ type: "link", url: shareIntent.text }); + mutate({ type: BookmarkTypes.LINK, url: shareIntent.text }); } else { - mutate({ type: "text", text: shareIntent.text }); + mutate({ type: BookmarkTypes.TEXT, text: shareIntent.text }); } } else if (!isPending && shareIntent?.files) { uploadAsset({ diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 128696a1..8faa8618 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -26,6 +26,7 @@ import { isBookmarkStillLoading, isBookmarkStillTagging, } from "@hoarder/shared-react/utils/bookmarkUtils"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { TailwindResolver } from "../TailwindResolver"; import { Divider } from "../ui/Divider"; @@ -161,7 +162,7 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) { function LinkCard({ bookmark }: { bookmark: ZBookmark }) { const { settings } = useAppSettings(); - if (bookmark.content.type !== "link") { + if (bookmark.content.type !== BookmarkTypes.LINK) { throw new Error("Wrong content type rendered"); } @@ -223,7 +224,7 @@ function LinkCard({ bookmark }: { bookmark: ZBookmark }) { } function TextCard({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type !== "text") { + if (bookmark.content.type !== BookmarkTypes.TEXT) { throw new Error("Wrong content type rendered"); } const content = bookmark.content.text; @@ -262,7 +263,7 @@ function TextCard({ bookmark }: { bookmark: ZBookmark }) { function AssetCard({ bookmark }: { bookmark: ZBookmark }) { const { settings } = useAppSettings(); - if (bookmark.content.type !== "asset") { + if (bookmark.content.type !== BookmarkTypes.ASSET) { throw new Error("Wrong content type rendered"); } const title = bookmark.title ?? bookmark.content.fileName; @@ -322,13 +323,13 @@ export default function BookmarkCard({ let comp; switch (bookmark.content.type) { - case "link": + case BookmarkTypes.LINK: comp = <LinkCard bookmark={bookmark} />; break; - case "text": + case BookmarkTypes.TEXT: comp = <TextCard bookmark={bookmark} />; break; - case "asset": + case BookmarkTypes.ASSET: comp = <AssetCard bookmark={bookmark} />; break; } diff --git a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx index 54288362..87f115f9 100644 --- a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx +++ b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx @@ -2,6 +2,7 @@ import { Text } from "react-native"; import { api } from "@/lib/trpc"; import type { ZGetBookmarksRequest } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import FullPageSpinner from "../ui/FullPageSpinner"; import BookmarkList from "./BookmarkList"; @@ -46,7 +47,7 @@ export default function UpdatingBookmarkList({ <BookmarkList bookmarks={data.pages .flatMap((p) => p.bookmarks) - .filter((b) => b.content.type != "unknown")} + .filter((b) => b.content.type != BookmarkTypes.UNKNWON)} header={header} onRefresh={onRefresh} fetchNextPage={fetchNextPage} diff --git a/apps/mobile/lib/upload.ts b/apps/mobile/lib/upload.ts index 0b6db549..b31faa90 100644 --- a/apps/mobile/lib/upload.ts +++ b/apps/mobile/lib/upload.ts @@ -1,6 +1,6 @@ import { useMutation } from "@tanstack/react-query"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import { zUploadErrorSchema, zUploadResponseSchema, @@ -59,7 +59,7 @@ export function useUploadAsset( const assetId = resp.assetId; const assetType = resp.contentType === "application/pdf" ? "pdf" : "image"; - createBookmark({ type: "asset", assetId, assetType }); + createBookmark({ type: BookmarkTypes.ASSET, assetId, assetType }); }, onError: (e) => { if (options.onError) { diff --git a/apps/web/components/dashboard/UploadDropzone.tsx b/apps/web/components/dashboard/UploadDropzone.tsx index 2807b892..05e8901e 100644 --- a/apps/web/components/dashboard/UploadDropzone.tsx +++ b/apps/web/components/dashboard/UploadDropzone.tsx @@ -8,6 +8,7 @@ import { TRPCClientError } from "@trpc/client"; import DropZone from "react-dropzone"; import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { zUploadErrorSchema, zUploadResponseSchema, @@ -50,7 +51,7 @@ export function useUploadAsset() { onSuccess: async (resp) => { const assetType = resp.contentType === "application/pdf" ? "pdf" : "image"; - return createBookmark({ ...resp, type: "asset", assetType }); + return createBookmark({ ...resp, type: BookmarkTypes.ASSET, assetType }); }, onError: (error, req) => { const err = zUploadErrorSchema.parse(JSON.parse(error.message)); @@ -68,7 +69,7 @@ export function useUploadAsset() { onSuccess: async (resp) => { return Promise.all( resp.map((url) => - createBookmark({ type: "link", url: url.toString() }), + createBookmark({ type: BookmarkTypes.LINK, url: url.toString() }), ), ); }, diff --git a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx index 76316de7..ec0d4069 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkCard.tsx @@ -1,7 +1,7 @@ import { api } from "@/lib/trpc"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { isBookmarkStillLoading } from "@hoarder/shared-react/utils/bookmarkUtils"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import AssetCard from "./AssetCard"; import LinkCard from "./LinkCard"; @@ -34,21 +34,21 @@ export default function BookmarkCard({ ); switch (bookmark.content.type) { - case "link": + case BookmarkTypes.LINK: return ( <LinkCard className={className} bookmark={{ ...bookmark, content: bookmark.content }} /> ); - case "text": + case BookmarkTypes.TEXT: return ( <TextCard className={className} bookmark={{ ...bookmark, content: bookmark.content }} /> ); - case "asset": + case BookmarkTypes.ASSET: return ( <AssetCard className={className} diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index e5f38eae..4007090e 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -32,6 +32,7 @@ import { } from "@hoarder/shared-react/hooks//bookmarks"; import { useRemoveBookmarkFromList } from "@hoarder/shared-react/hooks//lists"; import { useBookmarkGridContext } from "@hoarder/shared-react/hooks/bookmark-grid-context"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; import { ArchivedActionIcon, FavouritedActionIcon } from "./icons"; @@ -115,7 +116,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-fit"> - {bookmark.content.type === "text" && ( + {bookmark.content.type === BookmarkTypes.TEXT && ( <DropdownMenuItem onClick={() => setTextEditorOpen(true)}> <Pencil className="mr-2 size-4" /> <span>Edit</span> @@ -151,7 +152,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { /> <span>{bookmark.archived ? "Un-archive" : "Archive"}</span> </DropdownMenuItem> - {bookmark.content.type === "link" && ( + {bookmark.content.type === BookmarkTypes.LINK && ( <DropdownMenuItem onClick={() => { navigator.clipboard.writeText( @@ -191,7 +192,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </DropdownMenuItem> )} - {bookmark.content.type === "link" && ( + {bookmark.content.type === BookmarkTypes.LINK && ( <DropdownMenuItem disabled={demoMode} onClick={() => diff --git a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx index db69e1a3..74e94f94 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx @@ -14,7 +14,7 @@ import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; export function BookmarkedTextEditor({ bookmark, @@ -27,7 +27,9 @@ export function BookmarkedTextEditor({ }) { const isNewBookmark = bookmark === undefined; const [noteText, setNoteText] = useState( - bookmark && bookmark.content.type == "text" ? bookmark.content.text : "", + bookmark && bookmark.content.type == BookmarkTypes.TEXT + ? bookmark.content.text + : "", ); const invalidateOneBookmarksCache = diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index a1055e8e..78bd0742 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -19,6 +19,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { useCreateBookmarkWithPostHook } from "@hoarder/shared-react/hooks/bookmarks"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { useUploadAsset } from "../UploadDropzone"; @@ -99,7 +100,7 @@ export default function EditorCard({ className }: { className?: string }) { if (urls.length === 1) { // Only 1 url in the textfield --> simply import it - mutate({ type: "link", url: text }); + mutate({ type: BookmarkTypes.LINK, url: text }); return; } // multiple urls found --> ask the user if it should be imported as multiple URLs or as a text bookmark @@ -128,7 +129,7 @@ export default function EditorCard({ className }: { className?: string }) { tryToImportUrls(text); } catch (e) { // Not a URL - mutate({ type: "text", text }); + mutate({ type: BookmarkTypes.TEXT, text }); } }; @@ -240,7 +241,10 @@ export default function EditorCard({ className }: { className?: string }) { variant="secondary" loading={isPending} onClick={() => { - mutate({ type: "text", text: multiUrlImportState.text }); + mutate({ + type: BookmarkTypes.TEXT, + text: multiUrlImportState.text, + }); setMultiUrlImportState(null); }} > @@ -254,7 +258,7 @@ export default function EditorCard({ className }: { className?: string }) { loading={isPending} onClick={() => { multiUrlImportState.urls.forEach((url) => - mutate({ type: "link", url: url.toString() }), + mutate({ type: BookmarkTypes.LINK, url: url.toString() }), ); setMultiUrlImportState(null); }} diff --git a/apps/web/components/dashboard/preview/AssetContentSection.tsx b/apps/web/components/dashboard/preview/AssetContentSection.tsx index 4d6bb976..03ab8a43 100644 --- a/apps/web/components/dashboard/preview/AssetContentSection.tsx +++ b/apps/web/components/dashboard/preview/AssetContentSection.tsx @@ -1,10 +1,10 @@ import Image from "next/image"; import Link from "next/link"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; export function AssetContentSection({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type != "asset") { + if (bookmark.content.type != BookmarkTypes.ASSET) { throw new Error("Invalid content type"); } diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index 6a1068af..01e57e05 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -17,11 +17,11 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { CalendarDays, ExternalLink } from "lucide-react"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { isBookmarkStillCrawling, isBookmarkStillLoading, } from "@hoarder/shared-react/utils/bookmarkUtils"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import ActionBar from "./ActionBar"; import { AssetContentSection } from "./AssetContentSection"; @@ -66,10 +66,10 @@ function CreationTime({ createdAt }: { createdAt: Date }) { } function getSourceUrl(bookmark: ZBookmark) { - if (bookmark.content.type === "link") { + if (bookmark.content.type === BookmarkTypes.LINK) { return bookmark.content.url; } - if (bookmark.content.type === "asset") { + if (bookmark.content.type === BookmarkTypes.ASSET) { return bookmark.content.sourceUrl; } return null; @@ -108,15 +108,15 @@ export default function BookmarkPreview({ let content; switch (bookmark.content.type) { - case "link": { + case BookmarkTypes.LINK: { content = <LinkContentSection bookmark={bookmark} />; break; } - case "text": { + case BookmarkTypes.TEXT: { content = <TextContentSection bookmark={bookmark} />; break; } - case "asset": { + case BookmarkTypes.ASSET: { content = <AssetContentSection bookmark={bookmark} />; break; } diff --git a/apps/web/components/dashboard/preview/EditableTitle.tsx b/apps/web/components/dashboard/preview/EditableTitle.tsx index 237ad108..03b95e74 100644 --- a/apps/web/components/dashboard/preview/EditableTitle.tsx +++ b/apps/web/components/dashboard/preview/EditableTitle.tsx @@ -1,7 +1,7 @@ import { toast } from "@/components/ui/use-toast"; import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; -import { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import { EditableText } from "../EditableText"; @@ -16,13 +16,13 @@ export function EditableTitle({ bookmark }: { bookmark: ZBookmark }) { let title: string | null = null; switch (bookmark.content.type) { - case "link": + case BookmarkTypes.LINK: title = bookmark.content.title ?? bookmark.content.url; break; - case "text": + case BookmarkTypes.TEXT: title = null; break; - case "asset": + case BookmarkTypes.ASSET: title = bookmark.content.fileName ?? null; break; } diff --git a/apps/web/components/dashboard/preview/LinkContentSection.tsx b/apps/web/components/dashboard/preview/LinkContentSection.tsx index 3aeacdcd..f2069821 100644 --- a/apps/web/components/dashboard/preview/LinkContentSection.tsx +++ b/apps/web/components/dashboard/preview/LinkContentSection.tsx @@ -10,7 +10,11 @@ import { } from "@/components/ui/select"; import { ScrollArea } from "@radix-ui/react-scroll-area"; -import { ZBookmark, ZBookmarkedLink } from "@hoarder/shared/types/bookmarks"; +import { + BookmarkTypes, + ZBookmark, + ZBookmarkedLink, +} from "@hoarder/shared/types/bookmarks"; function FullPageArchiveSection({ link }: { link: ZBookmarkedLink }) { return ( @@ -63,7 +67,7 @@ export default function LinkContentSection({ }) { const [section, setSection] = useState<string>("cached"); - if (bookmark.content.type != "link") { + if (bookmark.content.type != BookmarkTypes.LINK) { throw new Error("Invalid content type"); } diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx index 2df1e964..76cb23ea 100644 --- a/apps/web/components/dashboard/preview/TextContentSection.tsx +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -1,10 +1,10 @@ import { MarkdownComponent } from "@/components/ui/markdown-component"; import { ScrollArea } from "@radix-ui/react-scroll-area"; -import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; export function TextContentSection({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type != "text") { + if (bookmark.content.type != BookmarkTypes.TEXT) { throw new Error("Invalid content type"); } return ( diff --git a/apps/workers/crawlerWorker.ts b/apps/workers/crawlerWorker.ts index c0e1bd1b..f2a51fc8 100644 --- a/apps/workers/crawlerWorker.ts +++ b/apps/workers/crawlerWorker.ts @@ -52,6 +52,7 @@ import { triggerSearchReindex, zCrawlLinkRequestSchema, } from "@hoarder/shared/queues"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; const metascraperParser = metascraper([ metascraperAmazon(), @@ -500,6 +501,11 @@ async function handleAsAssetBookmark( fileName: path.basename(new URL(url).pathname), sourceUrl: url, }); + // Switch the type of the bookmark from LINK to ASSET + await trx + .update(bookmarks) + .set({ type: BookmarkTypes.ASSET }) + .where(eq(bookmarks.id, bookmarkId)); await trx.delete(bookmarkLinks).where(eq(bookmarkLinks.id, bookmarkId)); }); } diff --git a/packages/db/drizzle/0025_aspiring_skaar.sql b/packages/db/drizzle/0025_aspiring_skaar.sql new file mode 100644 index 00000000..a8f12eec --- /dev/null +++ b/packages/db/drizzle/0025_aspiring_skaar.sql @@ -0,0 +1,11 @@ +ALTER TABLE bookmarks ADD `type` text NOT NULL DEFAULT "text";--> statement-breakpoint
+-- Fill in the bookmark type
+UPDATE bookmarks
+SET type = CASE
+ WHEN EXISTS (SELECT 1 FROM bookmarkLinks WHERE bookmarkLinks.id = bookmarks.id)
+ THEN 'link'
+ WHEN EXISTS (SELECT 1 FROM bookmarkTexts WHERE bookmarkTexts.id = bookmarks.id)
+ THEN 'text'
+ WHEN EXISTS (SELECT 1 FROM bookmarkAssets WHERE bookmarkAssets.id = bookmarks.id)
+ THEN 'asset'
+END;
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0025_snapshot.json b/packages/db/drizzle/meta/0025_snapshot.json new file mode 100644 index 00000000..2f496fd9 --- /dev/null +++ b/packages/db/drizzle/meta/0025_snapshot.json @@ -0,0 +1,1067 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "e3541a64-2e1d-407a-a6b0-7384f402da4d", + "prevId": "8a080f45-d358-465e-80b7-ae0c557e3872", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "assets": { + "name": "assets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "assets_bookmarkId_idx": { + "name": "assets_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "assets_assetType_idx": { + "name": "assets_assetType_idx", + "columns": [ + "assetType" + ], + "isUnique": false + } + }, + "foreignKeys": { + "assets_bookmarkId_bookmarks_id_fk": { + "name": "assets_bookmarkId_bookmarks_id_fk", + "tableFrom": "assets", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sourceUrl": { + "name": "sourceUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + } + }, + "indexes": { + "bookmarkLinks_url_idx": { + "name": "bookmarkLinks_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parentId": { + "name": "parentId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarkLists_parentId_bookmarkLists_id_fk": { + "name": "bookmarkLists_parentId_bookmarkLists_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "parentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 8196ad5f..420970ad 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1719135100480, "tag": "0024_premium_hammerhead", "breakpoints": true + }, + { + "idx": 25, + "version": "5", + "when": 1719251349398, + "tag": "0025_aspiring_skaar", + "breakpoints": true } ] }
\ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 63d23782..c3e8e136 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -11,6 +11,8 @@ import { unique, } from "drizzle-orm/sqlite-core"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; + function createdAtField() { return integer("createdAt", { mode: "timestamp" }) .notNull() @@ -117,6 +119,9 @@ export const bookmarks = sqliteTable( enum: ["pending", "failure", "success"], }).default("pending"), note: text("note"), + type: text("type", { + enum: [BookmarkTypes.LINK, BookmarkTypes.TEXT, BookmarkTypes.ASSET], + }).notNull(), }, (b) => ({ userIdIdx: index("bookmarks_userId_idx").on(b.userId), diff --git a/packages/shared-react/utils/bookmarkUtils.ts b/packages/shared-react/utils/bookmarkUtils.ts index da199a40..e7322712 100644 --- a/packages/shared-react/utils/bookmarkUtils.ts +++ b/packages/shared-react/utils/bookmarkUtils.ts @@ -1,4 +1,5 @@ -import type { +import { + BookmarkTypes, ZBookmark, ZBookmarkedLink, } from "@hoarder/shared/types/bookmarks"; @@ -21,7 +22,7 @@ export function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) { export function isBookmarkStillCrawling(bookmark: ZBookmark) { return ( - bookmark.content.type == "link" && + bookmark.content.type == BookmarkTypes.LINK && !bookmark.content.crawledAt && Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC ); diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts index c9e3e1a5..c0c12b56 100644 --- a/packages/shared/types/bookmarks.ts +++ b/packages/shared/types/bookmarks.ts @@ -4,8 +4,15 @@ import { zBookmarkTagSchema } from "./tags"; const MAX_TITLE_LENGTH = 100; +export const enum BookmarkTypes { + LINK = "link", + TEXT = "text", + ASSET = "asset", + UNKNWON = "unknown", +} + export const zBookmarkedLinkSchema = z.object({ - type: z.literal("link"), + type: z.literal(BookmarkTypes.LINK), url: z.string().url(), title: z.string().nullish(), description: z.string().nullish(), @@ -20,13 +27,13 @@ export const zBookmarkedLinkSchema = z.object({ export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>; export const zBookmarkedTextSchema = z.object({ - type: z.literal("text"), + type: z.literal(BookmarkTypes.TEXT), text: z.string(), }); export type ZBookmarkedText = z.infer<typeof zBookmarkedTextSchema>; export const zBookmarkedAssetSchema = z.object({ - type: z.literal("asset"), + type: z.literal(BookmarkTypes.ASSET), assetType: z.enum(["image", "pdf"]), assetId: z.string(), fileName: z.string().nullish(), @@ -38,7 +45,7 @@ export const zBookmarkContentSchema = z.discriminatedUnion("type", [ zBookmarkedLinkSchema, zBookmarkedTextSchema, zBookmarkedAssetSchema, - z.object({ type: z.literal("unknown") }), + z.object({ type: z.literal(BookmarkTypes.UNKNWON) }), ]); export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>; diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts index c9627626..67e73ad1 100644 --- a/packages/trpc/routers/bookmarks.test.ts +++ b/packages/trpc/routers/bookmarks.test.ts @@ -1,6 +1,7 @@ import { assert, beforeEach, describe, expect, test } from "vitest"; import { bookmarks } from "@hoarder/db/schema"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import type { CustomTestContext } from "../testUtils"; import { defaultBeforeEach } from "../testUtils"; @@ -12,15 +13,15 @@ describe("Bookmark Routes", () => { const api = apiCallers[0].bookmarks; const bookmark = await api.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); const res = await api.getBookmark({ bookmarkId: bookmark.id }); - assert(res.content.type == "link"); + assert(res.content.type == BookmarkTypes.LINK); expect(res.content.url).toEqual("https://google.com"); expect(res.favourited).toEqual(false); expect(res.archived).toEqual(false); - expect(res.content.type).toEqual("link"); + expect(res.content.type).toEqual(BookmarkTypes.LINK); }); test<CustomTestContext>("delete bookmark", async ({ apiCallers }) => { @@ -29,7 +30,7 @@ describe("Bookmark Routes", () => { // Create the bookmark const bookmark = await api.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); // It should exist @@ -50,7 +51,7 @@ describe("Bookmark Routes", () => { // Create the bookmark const bookmark = await api.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); await api.updateBookmark({ @@ -71,12 +72,12 @@ describe("Bookmark Routes", () => { const bookmark1 = await api.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); const bookmark2 = await api.createBookmark({ url: "https://google2.com", - type: "link", + type: BookmarkTypes.LINK, }); { @@ -120,7 +121,7 @@ describe("Bookmark Routes", () => { const api = apiCallers[0].bookmarks; const createdBookmark = await api.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); await api.updateTags({ @@ -161,7 +162,7 @@ describe("Bookmark Routes", () => { const api = apiCallers[0].bookmarks; const createdBookmark = await api.createBookmark({ text: "HELLO WORLD", - type: "text", + type: BookmarkTypes.TEXT, }); await api.updateBookmarkText({ @@ -170,17 +171,17 @@ describe("Bookmark Routes", () => { }); const bookmark = await api.getBookmark({ bookmarkId: createdBookmark.id }); - assert(bookmark.content.type == "text"); + assert(bookmark.content.type == BookmarkTypes.TEXT); expect(bookmark.content.text).toEqual("WORLD HELLO"); }); test<CustomTestContext>("privacy", async ({ apiCallers }) => { const user1Bookmark = await apiCallers[0].bookmarks.createBookmark({ - type: "link", + type: BookmarkTypes.LINK, url: "https://google.com", }); const user2Bookmark = await apiCallers[1].bookmarks.createBookmark({ - type: "link", + type: BookmarkTypes.LINK, url: "https://google.com", }); @@ -219,20 +220,20 @@ describe("Bookmark Routes", () => { // Two users with google in their bookmarks const bookmark1User1 = await apiCallers[0].bookmarks.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); expect(bookmark1User1.alreadyExists).toEqual(false); const bookmark1User2 = await apiCallers[1].bookmarks.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); expect(bookmark1User2.alreadyExists).toEqual(false); // User1 attempting to re-add google. Should return the existing bookmark const bookmark2User1 = await apiCallers[0].bookmarks.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); expect(bookmark2User1.alreadyExists).toEqual(true); expect(bookmark2User1.id).toEqual(bookmark1User1.id); @@ -240,7 +241,7 @@ describe("Bookmark Routes", () => { // User2 attempting to re-add google. Should return the existing bookmark const bookmark2User2 = await apiCallers[1].bookmarks.createBookmark({ url: "https://google.com", - type: "link", + type: BookmarkTypes.LINK, }); expect(bookmark2User2.alreadyExists).toEqual(true); expect(bookmark2User2.id).toEqual(bookmark1User2.id); @@ -248,7 +249,7 @@ describe("Bookmark Routes", () => { // User1 adding google2. Should not return an existing bookmark const bookmark3User1 = await apiCallers[0].bookmarks.createBookmark({ url: "https://google2.com", - type: "link", + type: BookmarkTypes.LINK, }); expect(bookmark3User1.alreadyExists).toEqual(false); }); @@ -261,6 +262,7 @@ describe("Bookmark Routes", () => { const bookmarkWithDate = (date_ms: number) => ({ userId: user.id, createdAt: new Date(date_ms), + type: BookmarkTypes.TEXT as const, }); // One normal bookmark @@ -274,7 +276,7 @@ describe("Bookmark Routes", () => { for (let i = 0; i < 10; i++) { values.push(bookmarkWithDate(now)); } - // And then another one with a second afterards + // And then another one with a second afterwards for (let i = 0; i < 10; i++) { now -= 1000; values.push(bookmarkWithDate(now)); diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 8644bbcf..edf196bb 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -29,6 +29,7 @@ import { } from "@hoarder/shared/queues"; import { getSearchIdxClient } from "@hoarder/shared/search"; import { + BookmarkTypes, DEFAULT_NUM_BOOKMARKS_PER_PAGE, zBareBookmarkSchema, zBookmarkSchema, @@ -176,25 +177,29 @@ async function cleanupAssetForBookmark( function toZodSchema(bookmark: BookmarkQueryReturnType): ZBookmark { const { tagsOnBookmarks, link, text, asset, assets, ...rest } = bookmark; - let content: ZBookmarkContent; - if (link) { - content = { - type: "link", - ...mapAssetsToBookmarkFields(assets), - ...link, - }; - } else if (text) { - content = { type: "text", text: text.text ?? "" }; - } else if (asset) { - content = { - type: "asset", - assetType: asset.assetType, - assetId: asset.assetId, - fileName: asset.fileName, - sourceUrl: asset.sourceUrl, - }; - } else { - content = { type: "unknown" }; + let content: ZBookmarkContent = { + type: BookmarkTypes.UNKNWON, + }; + switch (bookmark.type) { + case BookmarkTypes.LINK: + content = { + type: bookmark.type, + ...mapAssetsToBookmarkFields(assets), + ...link, + }; + break; + case BookmarkTypes.TEXT: + content = { type: bookmark.type, text: text.text ?? "" }; + break; + case BookmarkTypes.ASSET: + content = { + type: bookmark.type, + assetType: asset.assetType, + assetId: asset.assetId, + fileName: asset.fileName, + sourceUrl: asset.sourceUrl, + }; + break; } return { @@ -218,19 +223,23 @@ export const bookmarksAppRouter = router({ ), ) .mutation(async ({ input, ctx }) => { - if (input.type == "link") { + if (input.type == BookmarkTypes.LINK) { // This doesn't 100% protect from duplicates because of races, but it's more than enough for this usecase. const alreadyExists = await attemptToDedupLink(ctx, input.url); if (alreadyExists) { return { ...alreadyExists, alreadyExists: true }; } } + if (input.type == BookmarkTypes.UNKNWON) { + throw new TRPCError({ code: "BAD_REQUEST" }); + } const bookmark = await ctx.db.transaction(async (tx) => { const bookmark = ( await tx .insert(bookmarks) .values({ userId: ctx.user.id, + type: input.type, }) .returning() )[0]; @@ -238,7 +247,7 @@ export const bookmarksAppRouter = router({ let content: ZBookmarkContent; switch (input.type) { - case "link": { + case BookmarkTypes.LINK: { const link = ( await tx .insert(bookmarkLinks) @@ -249,12 +258,12 @@ export const bookmarksAppRouter = router({ .returning() )[0]; content = { - type: "link", + type: BookmarkTypes.LINK, ...link, }; break; } - case "text": { + case BookmarkTypes.TEXT: { const text = ( await tx .insert(bookmarkTexts) @@ -262,12 +271,12 @@ export const bookmarksAppRouter = router({ .returning() )[0]; content = { - type: "text", + type: BookmarkTypes.TEXT, text: text.text ?? "", }; break; } - case "asset": { + case BookmarkTypes.ASSET: { const [asset] = await tx .insert(bookmarkAssets) .values({ @@ -281,15 +290,12 @@ export const bookmarksAppRouter = router({ }) .returning(); content = { - type: "asset", + type: BookmarkTypes.ASSET, assetType: asset.assetType, assetId: asset.assetId, }; break; } - case "unknown": { - throw new TRPCError({ code: "BAD_REQUEST" }); - } } return { @@ -302,15 +308,15 @@ export const bookmarksAppRouter = router({ // Enqueue crawling request switch (bookmark.content.type) { - case "link": { + case BookmarkTypes.LINK: { // The crawling job triggers openai when it's done await LinkCrawlerQueue.add("crawl", { bookmarkId: bookmark.id, }); break; } - case "text": - case "asset": { + case BookmarkTypes.TEXT: + case BookmarkTypes.ASSET: { await OpenAIQueue.add("openai", { bookmarkId: bookmark.id, }); @@ -565,19 +571,28 @@ export const bookmarksAppRouter = router({ const bookmarkId = row.bookmarksSq.id; if (!acc[bookmarkId]) { let content: ZBookmarkContent; - if (row.bookmarkLinks) { - content = { type: "link", ...row.bookmarkLinks }; - } else if (row.bookmarkTexts) { - content = { type: "text", text: row.bookmarkTexts.text ?? "" }; - } else if (row.bookmarkAssets) { - content = { - type: "asset", - assetId: row.bookmarkAssets.assetId, - assetType: row.bookmarkAssets.assetType, - fileName: row.bookmarkAssets.fileName, - }; - } else { - content = { type: "unknown" }; + switch (row.bookmarksSq.type) { + case BookmarkTypes.LINK: { + content = { type: row.bookmarksSq.type, ...row.bookmarkLinks! }; + break; + } + case BookmarkTypes.TEXT: { + content = { + type: row.bookmarksSq.type, + text: row.bookmarkTexts?.text ?? "", + }; + break; + } + case BookmarkTypes.ASSET: { + const bookmarkAssets = row.bookmarkAssets!; + content = { + type: row.bookmarksSq.type, + assetId: bookmarkAssets.assetId, + assetType: bookmarkAssets.assetType, + fileName: bookmarkAssets.fileName, + }; + break; + } } acc[bookmarkId] = { ...row.bookmarksSq, |
