aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkamtschatka <simon.schatka@gmx.at>2024-07-01 13:03:53 +0200
committerGitHub <noreply@github.com>2024-07-01 12:03:53 +0100
commite6486465decd612f7e437abe904960a47ff359ce (patch)
treef36fd7efbcf2a083905061d8c5f1310f36350ced
parentccbff18a9763e458c07f46cb3a331062df14a9b9 (diff)
downloadkarakeep-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>
-rw-r--r--apps/browser-extension/src/SavePage.tsx3
-rw-r--r--apps/cli/src/commands/bookmarks.ts13
-rw-r--r--apps/mobile/app/dashboard/add-link.tsx4
-rw-r--r--apps/mobile/app/dashboard/add-note.tsx7
-rw-r--r--apps/mobile/app/sharing.tsx6
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx13
-rw-r--r--apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx3
-rw-r--r--apps/mobile/lib/upload.ts4
-rw-r--r--apps/web/components/dashboard/UploadDropzone.tsx5
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkCard.tsx8
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx7
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx6
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx12
-rw-r--r--apps/web/components/dashboard/preview/AssetContentSection.tsx4
-rw-r--r--apps/web/components/dashboard/preview/BookmarkPreview.tsx12
-rw-r--r--apps/web/components/dashboard/preview/EditableTitle.tsx8
-rw-r--r--apps/web/components/dashboard/preview/LinkContentSection.tsx8
-rw-r--r--apps/web/components/dashboard/preview/TextContentSection.tsx4
-rw-r--r--apps/workers/crawlerWorker.ts6
-rw-r--r--packages/db/drizzle/0025_aspiring_skaar.sql11
-rw-r--r--packages/db/drizzle/meta/0025_snapshot.json1067
-rw-r--r--packages/db/drizzle/meta/_journal.json7
-rw-r--r--packages/db/schema.ts5
-rw-r--r--packages/shared-react/utils/bookmarkUtils.ts5
-rw-r--r--packages/shared/types/bookmarks.ts15
-rw-r--r--packages/trpc/routers/bookmarks.test.ts38
-rw-r--r--packages/trpc/routers/bookmarks.ts105
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,