aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--packages/db/prisma/migrations/20240209013653_toplevel_bookmark/migration.sql62
-rw-r--r--packages/db/prisma/schema.prisma54
-rw-r--r--packages/shared/queues.ts4
-rw-r--r--packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts (renamed from packages/web/app/api/v1/links/[linkId]/route.ts)6
-rw-r--r--packages/web/app/api/v1/bookmarks/route.ts (renamed from packages/web/app/api/v1/links/route.ts)22
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx27
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx15
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx21
-rw-r--r--packages/web/app/dashboard/bookmarks/page.tsx4
-rw-r--r--packages/web/lib/api.ts19
-rw-r--r--packages/web/lib/services/bookmarks.ts92
-rw-r--r--packages/web/lib/services/links.ts78
-rw-r--r--packages/web/lib/types/api/bookmarks.ts37
-rw-r--r--packages/web/lib/types/api/links.ts33
-rw-r--r--packages/web/lib/types/api/tags.ts1
-rw-r--r--packages/web/package.json1
-rw-r--r--packages/workers/crawler.ts32
-rw-r--r--packages/workers/openai.ts61
-rw-r--r--yarn.lock2
20 files changed, 330 insertions, 242 deletions
diff --git a/package.json b/package.json
index 1b63088d..8df8a09d 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^20",
+ "es-errors": "^1.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "14.1.0",
"prettier": "3.2.5",
diff --git a/packages/db/prisma/migrations/20240209013653_toplevel_bookmark/migration.sql b/packages/db/prisma/migrations/20240209013653_toplevel_bookmark/migration.sql
new file mode 100644
index 00000000..2b5aa370
--- /dev/null
+++ b/packages/db/prisma/migrations/20240209013653_toplevel_bookmark/migration.sql
@@ -0,0 +1,62 @@
+/*
+ Warnings:
+
+ - You are about to drop the `BookmarkedLinkDetails` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `TagsOnLinks` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the column `createdAt` on the `BookmarkedLink` table. All the data in the column will be lost.
+ - You are about to drop the column `userId` on the `BookmarkedLink` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "TagsOnLinks_linkId_tagId_key";
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "BookmarkedLinkDetails";
+PRAGMA foreign_keys=on;
+
+-- DropTable
+PRAGMA foreign_keys=off;
+DROP TABLE "TagsOnLinks";
+PRAGMA foreign_keys=on;
+
+-- CreateTable
+CREATE TABLE "Bookmark" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "archived" BOOLEAN NOT NULL DEFAULT false,
+ "favourited" BOOLEAN NOT NULL DEFAULT false,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "Bookmark_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "TagsOnBookmarks" (
+ "bookmarkId" TEXT NOT NULL,
+ "tagId" TEXT NOT NULL,
+ "attachedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "attachedBy" TEXT NOT NULL,
+ CONSTRAINT "TagsOnBookmarks_bookmarkId_fkey" FOREIGN KEY ("bookmarkId") REFERENCES "Bookmark" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "TagsOnBookmarks_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "BookmarkTags" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_BookmarkedLink" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "url" TEXT NOT NULL,
+ "title" TEXT,
+ "description" TEXT,
+ "imageUrl" TEXT,
+ "favicon" TEXT,
+ "crawledAt" DATETIME,
+ CONSTRAINT "BookmarkedLink_id_fkey" FOREIGN KEY ("id") REFERENCES "Bookmark" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+INSERT INTO "new_BookmarkedLink" ("id", "url") SELECT "id", "url" FROM "BookmarkedLink";
+DROP TABLE "BookmarkedLink";
+ALTER TABLE "new_BookmarkedLink" RENAME TO "BookmarkedLink";
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TagsOnBookmarks_bookmarkId_tagId_key" ON "TagsOnBookmarks"("bookmarkId", "tagId");
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 0e6d080c..623a9f13 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -38,15 +38,15 @@ model Session {
}
model User {
- id String @id @default(cuid())
+ id String @id @default(cuid())
name String?
- email String? @unique
+ email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
- links BookmarkedLink[]
tags BookmarkTags[]
+ bookmarks Bookmark[]
}
model VerificationToken {
@@ -57,28 +57,34 @@ model VerificationToken {
@@unique([identifier, token])
}
-model BookmarkedLink {
- id String @id @default(cuid())
- url String
- createdAt DateTime @default(now())
+model Bookmark {
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ archived Boolean @default(false)
+ favourited Boolean @default(false)
+ userId String
- userId String
+ // Content relation
+ link BookmarkedLink?
- details BookmarkedLinkDetails?
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- tags TagsOnLinks[]
+ // Other relations
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ tags TagsOnBookmarks[]
}
-model BookmarkedLinkDetails {
- id String @id
+model BookmarkedLink {
+ id String @id
+ url String
+
+ // Crawled info
title String?
description String?
imageUrl String?
favicon String?
- createdAt DateTime @default(now())
+ crawledAt DateTime?
- link BookmarkedLink @relation(fields: [id], references: [id], onDelete: Cascade)
+ // Relations
+ parentBookmark Bookmark @relation(fields: [id], references: [id], onDelete: Cascade)
}
model BookmarkTags {
@@ -88,18 +94,20 @@ model BookmarkTags {
userId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- attachedLinks TagsOnLinks[]
+ // Relations
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ bookmarks TagsOnBookmarks[]
}
-model TagsOnLinks {
- link BookmarkedLink @relation(fields: [linkId], references: [id], onDelete: Cascade)
- linkId String
+model TagsOnBookmarks {
+ bookmark Bookmark @relation(fields: [bookmarkId], references: [id], onDelete: Cascade)
+ bookmarkId String
tag BookmarkTags @relation(fields: [tagId], references: [id], onDelete: Cascade)
tagId String
- attachedAt DateTime @default(now())
+ attachedAt DateTime @default(now())
+ attachedBy String // "human" or "ai" (if only prisma sqlite supported enums)
- @@unique([linkId, tagId])
+ @@unique([bookmarkId, tagId])
}
diff --git a/packages/shared/queues.ts b/packages/shared/queues.ts
index a607131d..6a49d749 100644
--- a/packages/shared/queues.ts
+++ b/packages/shared/queues.ts
@@ -8,7 +8,7 @@ export const queueConnectionDetails = {
// Link Crawler
export const zCrawlLinkRequestSchema = z.object({
- linkId: z.string(),
+ bookmarkId: z.string(),
url: z.string().url(),
});
export type ZCrawlLinkRequest = z.infer<typeof zCrawlLinkRequestSchema>;
@@ -20,7 +20,7 @@ export const LinkCrawlerQueue = new Queue<ZCrawlLinkRequest, void>(
// OpenAI Worker
export const zOpenAIRequestSchema = z.object({
- linkId: z.string(),
+ bookmarkId: z.string(),
});
export type ZOpenAIRequest = z.infer<typeof zOpenAIRequestSchema>;
diff --git a/packages/web/app/api/v1/links/[linkId]/route.ts b/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
index 39449d6d..6adcf771 100644
--- a/packages/web/app/api/v1/links/[linkId]/route.ts
+++ b/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
@@ -1,5 +1,5 @@
import { authOptions } from "@/lib/auth";
-import { unbookmarkLink } from "@/lib/services/links";
+import { deleteBookmark } from "@/lib/services/bookmarks";
import { Prisma } from "@remember/db";
import { getServerSession } from "next-auth";
@@ -7,7 +7,7 @@ import { NextRequest } from "next/server";
export async function DELETE(
_request: NextRequest,
- { params }: { params: { linkId: string } },
+ { params }: { params: { bookmarkId: string } },
) {
// TODO: We probably should be using an API key here instead of the session;
const session = await getServerSession(authOptions);
@@ -16,7 +16,7 @@ export async function DELETE(
}
try {
- await unbookmarkLink(params.linkId, session.user.id);
+ await deleteBookmark(params.bookmarkId, session.user.id);
} catch (e: unknown) {
if (
e instanceof Prisma.PrismaClientKnownRequestError &&
diff --git a/packages/web/app/api/v1/links/route.ts b/packages/web/app/api/v1/bookmarks/route.ts
index 87541634..b9305ca8 100644
--- a/packages/web/app/api/v1/links/route.ts
+++ b/packages/web/app/api/v1/bookmarks/route.ts
@@ -1,11 +1,11 @@
import { authOptions } from "@/lib/auth";
-import { bookmarkLink, getLinks } from "@/lib/services/links";
+import { bookmarkLink, getBookmarks } from "@/lib/services/bookmarks";
import {
- zNewBookmarkedLinkRequestSchema,
- ZGetLinksResponse,
- ZBookmarkedLink,
-} from "@/lib/types/api/links";
+ zNewBookmarkRequestSchema,
+ ZGetBookmarksResponse,
+ ZBookmark,
+} from "@/lib/types/api/bookmarks";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
@@ -16,9 +16,7 @@ export async function POST(request: NextRequest) {
return new Response(null, { status: 401 });
}
- const linkRequest = zNewBookmarkedLinkRequestSchema.safeParse(
- await request.json(),
- );
+ const linkRequest = zNewBookmarkRequestSchema.safeParse(await request.json());
if (!linkRequest.success) {
return NextResponse.json(
@@ -29,9 +27,9 @@ export async function POST(request: NextRequest) {
);
}
- const link = await bookmarkLink(linkRequest.data.url, session.user.id);
+ const bookmark = await bookmarkLink(linkRequest.data.url, session.user.id);
- let response: ZBookmarkedLink = { ...link };
+ let response: ZBookmark = { ...bookmark };
return NextResponse.json(response, { status: 201 });
}
@@ -42,8 +40,8 @@ export async function GET() {
return new Response(null, { status: 401 });
}
- const links = await getLinks(session.user.id);
+ const bookmarks = await getBookmarks(session.user.id);
- let response: ZGetLinksResponse = { links };
+ let response: ZGetBookmarksResponse = { bookmarks };
return NextResponse.json(response);
}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
new file mode 100644
index 00000000..2b6b19b6
--- /dev/null
+++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
@@ -0,0 +1,27 @@
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import { authOptions } from "@/lib/auth";
+import { getBookmarks } from "@/lib/services/bookmarks";
+import LinkCard from "./LinkCard";
+import { ZBookmark } from "@/lib/types/api/bookmarks";
+
+function renderBookmark(bookmark: ZBookmark) {
+ switch (bookmark.content.type) {
+ case "link":
+ return <LinkCard key={bookmark.id} bookmark={bookmark} />;
+ }
+}
+
+export default async function BookmarksGrid() {
+ const session = await getServerSession(authOptions);
+ if (!session) {
+ redirect("/");
+ }
+ const bookmarks = await getBookmarks(session.user.id);
+
+ return (
+ <div className="container grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
+ {bookmarks.map((b) => renderBookmark(b))}
+ </div>
+ );
+}
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
index b5a051e8..1cbd8865 100644
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
@@ -16,7 +16,7 @@ import {
} from "@/components/ui/imageCard";
import { useToast } from "@/components/ui/use-toast";
import APIClient from "@/lib/api";
-import { ZBookmarkedLink } from "@/lib/types/api/links";
+import { ZBookmark } from "@/lib/types/api/bookmarks";
import { MoreHorizontal, Trash2 } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -26,7 +26,7 @@ export function LinkOptions({ linkId }: { linkId: string }) {
const router = useRouter();
const unbookmarkLink = async () => {
- let [_, error] = await APIClient.unbookmarkLink(linkId);
+ let [_, error] = await APIClient.deleteBookmark(linkId);
if (error) {
toast({
@@ -59,7 +59,8 @@ export function LinkOptions({ linkId }: { linkId: string }) {
);
}
-export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
+export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
+ const link = bookmark.content;
const parsedUrl = new URL(link.url);
return (
@@ -67,15 +68,15 @@ export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
className={
"bg-gray-50 duration-300 ease-in border border-grey-100 hover:transition-all hover:border-blue-300"
}
- image={link.details?.imageUrl ?? undefined}
+ image={link?.imageUrl ?? undefined}
>
<ImageCardTitle>
<Link className="line-clamp-3" href={link.url}>
- {link.details?.title ?? parsedUrl.host}
+ {link?.title ?? parsedUrl.host}
</Link>
</ImageCardTitle>
<ImageCardBody className="py-2 overflow-clip">
- {link.tags.map((t) => (
+ {bookmark.tags.map((t) => (
<Badge
variant="default"
className="bg-gray-300 text-gray-500"
@@ -92,7 +93,7 @@ export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
{parsedUrl.host}
</Link>
</div>
- <LinkOptions linkId={link.id} />
+ <LinkOptions linkId={bookmark.id} />
</div>
</ImageCardFooter>
</ImageCard>
diff --git a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx
deleted file mode 100644
index 66f0d766..00000000
--- a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { getServerSession } from "next-auth";
-import { redirect } from "next/navigation";
-import { authOptions } from "@/lib/auth";
-import { getLinks } from "@/lib/services/links";
-import LinkCard from "./LinkCard";
-
-export default async function LinksGrid() {
- const session = await getServerSession(authOptions);
- if (!session) {
- redirect("/");
- }
- const links = await getLinks(session.user.id);
-
- return (
- <div className="container grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
- {links.map((l) => (
- <LinkCard key={l.id} link={l} />
- ))}
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx
index b4158893..b9eabfe8 100644
--- a/packages/web/app/dashboard/bookmarks/page.tsx
+++ b/packages/web/app/dashboard/bookmarks/page.tsx
@@ -1,5 +1,5 @@
import AddLink from "./components/AddLink";
-import LinksGrid from "./components/LinksGrid";
+import BookmarksGrid from "./components/BookmarksGrid";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -13,7 +13,7 @@ export default async function Bookmarks() {
<AddLink />
</div>
<div>
- <LinksGrid />
+ <BookmarksGrid />
</div>
</div>
);
diff --git a/packages/web/lib/api.ts b/packages/web/lib/api.ts
index 56686cde..8ee08601 100644
--- a/packages/web/lib/api.ts
+++ b/packages/web/lib/api.ts
@@ -2,9 +2,9 @@
import { ZodTypeAny, z } from "zod";
import {
- ZNewBookmarkedLinkRequest,
- zGetLinksResponseSchema,
-} from "./types/api/links";
+ ZNewBookmarkRequest,
+ zGetBookmarksResponseSchema,
+} from "./types/api/bookmarks";
import serverConfig from "./config";
@@ -57,24 +57,25 @@ async function doRequest<T>(
}
export default class APIClient {
- static async getLinks() {
- return await doRequest(`/links`, zGetLinksResponseSchema, {
+ static async getBookmarks() {
+ return await doRequest(`/bookmarks`, zGetBookmarksResponseSchema, {
next: { tags: ["links"] },
});
}
static async bookmarkLink(url: string) {
- const body: ZNewBookmarkedLinkRequest = {
+ const body: ZNewBookmarkRequest = {
+ type: "link",
url,
};
- return await doRequest(`/links`, undefined, {
+ return await doRequest(`/bookmarks`, undefined, {
method: "POST",
body: JSON.stringify(body),
});
}
- static async unbookmarkLink(linkId: string) {
- return await doRequest(`/links/${linkId}`, undefined, {
+ static async deleteBookmark(id: string) {
+ return await doRequest(`/bookmarks/${id}`, undefined, {
method: "DELETE",
});
}
diff --git a/packages/web/lib/services/bookmarks.ts b/packages/web/lib/services/bookmarks.ts
new file mode 100644
index 00000000..3c9929bc
--- /dev/null
+++ b/packages/web/lib/services/bookmarks.ts
@@ -0,0 +1,92 @@
+import { LinkCrawlerQueue } from "@remember/shared/queues";
+import prisma from "@remember/db";
+import { ZBookmark, ZBookmarkContent } from "@/lib/types/api/bookmarks";
+
+const defaultBookmarkFields = {
+ id: true,
+ favourited: true,
+ archived: true,
+ createdAt: true,
+ link: {
+ select: {
+ url: true,
+ title: true,
+ description: true,
+ imageUrl: true,
+ favicon: true,
+ },
+ },
+ tags: {
+ include: {
+ tag: true,
+ },
+ },
+};
+
+async function dummyPrismaReturnType() {
+ const x = await prisma.bookmark.findFirstOrThrow({
+ select: defaultBookmarkFields,
+ });
+ return x;
+}
+
+function toZodSchema(
+ bookmark: Awaited<ReturnType<typeof dummyPrismaReturnType>>,
+): ZBookmark {
+ const { tags, link, ...rest } = bookmark;
+
+ let content: ZBookmarkContent;
+ if (link) {
+ content = { type: "link", ...link };
+ } else {
+ throw new Error("Unknown content type");
+ }
+
+ return {
+ tags: tags.map((t) => t.tag),
+ content,
+ ...rest,
+ };
+}
+
+export async function deleteBookmark(bookmarkId: string, userId: string) {
+ await prisma.bookmark.delete({
+ where: {
+ id: bookmarkId,
+ userId,
+ },
+ });
+}
+
+export async function bookmarkLink(url: string, userId: string) {
+ const bookmark = await prisma.bookmark.create({
+ data: {
+ link: {
+ create: {
+ url,
+ },
+ },
+ userId,
+ },
+ select: defaultBookmarkFields,
+ });
+
+ // Enqueue crawling request
+ await LinkCrawlerQueue.add("crawl", {
+ bookmarkId: bookmark.id,
+ url: url,
+ });
+
+ return toZodSchema(bookmark);
+}
+
+export async function getBookmarks(userId: string) {
+ return (
+ await prisma.bookmark.findMany({
+ where: {
+ userId,
+ },
+ select: defaultBookmarkFields,
+ })
+ ).map(toZodSchema);
+}
diff --git a/packages/web/lib/services/links.ts b/packages/web/lib/services/links.ts
deleted file mode 100644
index d273b118..00000000
--- a/packages/web/lib/services/links.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { LinkCrawlerQueue } from "@remember/shared/queues";
-import prisma from "@remember/db";
-import { ZBookmarkedLink } from "@/lib/types/api/links";
-
-const defaultLinkFields = {
- id: true,
- url: true,
- createdAt: true,
- details: {
- select: {
- title: true,
- description: true,
- imageUrl: true,
- favicon: true,
- },
- },
- tags: {
- include: {
- tag: true,
- },
- },
-};
-
-async function dummyPrismaReturnType() {
- return await prisma.bookmarkedLink.findFirstOrThrow({
- select: defaultLinkFields,
- });
-}
-
-function toZodSchema(
- link: Awaited<ReturnType<typeof dummyPrismaReturnType>>,
-): ZBookmarkedLink {
- return {
- id: link.id,
- url: link.url,
- createdAt: link.createdAt,
- details: link.details,
- tags: link.tags.map((t) => t.tag),
- };
-}
-
-export async function unbookmarkLink(linkId: string, userId: string) {
- await prisma.bookmarkedLink.delete({
- where: {
- id: linkId,
- userId,
- },
- });
-}
-
-export async function bookmarkLink(url: string, userId: string) {
- const link = await prisma.bookmarkedLink.create({
- data: {
- url,
- userId,
- },
- select: defaultLinkFields,
- });
-
- // Enqueue crawling request
- await LinkCrawlerQueue.add("crawl", {
- linkId: link.id,
- url: link.url,
- });
-
- return toZodSchema(link);
-}
-
-export async function getLinks(userId: string) {
- return (
- await prisma.bookmarkedLink.findMany({
- where: {
- userId,
- },
- select: defaultLinkFields,
- })
- ).map(toZodSchema);
-}
diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/web/lib/types/api/bookmarks.ts
new file mode 100644
index 00000000..485fbfab
--- /dev/null
+++ b/packages/web/lib/types/api/bookmarks.ts
@@ -0,0 +1,37 @@
+import { z } from "zod";
+import { zBookmarkTagSchema } from "@/lib/types/api/tags";
+
+export const zBookmarkedLinkSchema = z.object({
+ type: z.literal("link"),
+ url: z.string().url(),
+ title: z.string().nullish(),
+ description: z.string().nullish(),
+ imageUrl: z.string().url().nullish(),
+ favicon: z.string().url().nullish(),
+});
+export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
+
+export const zBookmarkContentSchema = z.discriminatedUnion("type", [
+ zBookmarkedLinkSchema,
+]);
+export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>;
+
+export const zBookmarkSchema = z.object({
+ id: z.string(),
+ createdAt: z.coerce.date(),
+ archived: z.boolean(),
+ favourited: z.boolean(),
+ tags: z.array(zBookmarkTagSchema),
+ content: zBookmarkContentSchema,
+});
+export type ZBookmark = z.infer<typeof zBookmarkSchema>;
+
+// POST /v1/bookmarks
+export const zNewBookmarkRequestSchema = zBookmarkContentSchema;
+export type ZNewBookmarkRequest = z.infer<typeof zNewBookmarkRequestSchema>;
+
+// GET /v1/bookmarks
+export const zGetBookmarksResponseSchema = z.object({
+ bookmarks: z.array(zBookmarkSchema),
+});
+export type ZGetBookmarksResponse = z.infer<typeof zGetBookmarksResponseSchema>;
diff --git a/packages/web/lib/types/api/links.ts b/packages/web/lib/types/api/links.ts
deleted file mode 100644
index f84445f6..00000000
--- a/packages/web/lib/types/api/links.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { z } from "zod";
-import { zBookmarkTagSchema } from "@/lib/types/api/tags";
-
-export const zBookmarkedLinkSchema = z.object({
- id: z.string(),
- url: z.string().url(),
- createdAt: z.coerce.date(),
-
- details: z
- .object({
- title: z.string().nullish(),
- description: z.string().nullish(),
- imageUrl: z.string().url().nullish(),
- favicon: z.string().url().nullish(),
- })
- .nullish(),
- tags: z.array(zBookmarkTagSchema),
-});
-export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
-
-// POST /v1/links
-export const zNewBookmarkedLinkRequestSchema = zBookmarkedLinkSchema.pick({
- url: true,
-});
-export type ZNewBookmarkedLinkRequest = z.infer<
- typeof zNewBookmarkedLinkRequestSchema
->;
-
-// GET /v1/links
-export const zGetLinksResponseSchema = z.object({
- links: z.array(zBookmarkedLinkSchema),
-});
-export type ZGetLinksResponse = z.infer<typeof zGetLinksResponseSchema>;
diff --git a/packages/web/lib/types/api/tags.ts b/packages/web/lib/types/api/tags.ts
index f2d2bc18..bcd16f5b 100644
--- a/packages/web/lib/types/api/tags.ts
+++ b/packages/web/lib/types/api/tags.ts
@@ -4,3 +4,4 @@ export const zBookmarkTagSchema = z.object({
id: z.string(),
name: z.string(),
});
+export type ZBookmarkTags = z.infer<typeof zBookmarkTagSchema>;
diff --git a/packages/web/package.json b/packages/web/package.json
index 8a315beb..5762a476 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@next/eslint-plugin-next": "^14.1.0",
+ "@prisma/client": "^5.9.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
diff --git a/packages/workers/crawler.ts b/packages/workers/crawler.ts
index 817bba45..c76b2c42 100644
--- a/packages/workers/crawler.ts
+++ b/packages/workers/crawler.ts
@@ -29,10 +29,10 @@ export default async function runCrawler(job: Job<ZCrawlLinkRequest, void>) {
return;
}
- const { url, linkId } = request.data;
+ const { url, bookmarkId } = request.data;
logger.info(
- `[Crawler][${jobId}] Will crawl "${url}" for link with id "${linkId}"`,
+ `[Crawler][${jobId}] Will crawl "${url}" for link with id "${bookmarkId}"`,
);
// TODO(IMPORTANT): Run security validations on the input URL (e.g. deny localhost, etc)
@@ -46,33 +46,19 @@ export default async function runCrawler(job: Job<ZCrawlLinkRequest, void>) {
await prisma.bookmarkedLink.update({
where: {
- id: linkId,
+ id: bookmarkId,
},
data: {
- details: {
- upsert: {
- create: {
- title: meta.title,
- description: meta.description,
- imageUrl: meta.image,
- favicon: meta.logo,
- },
- update: {
- title: meta.title,
- description: meta.description,
- imageUrl: meta.image,
- favicon: meta.logo,
- },
- },
- },
- },
- include: {
- details: true,
+ title: meta.title,
+ description: meta.description,
+ imageUrl: meta.image,
+ favicon: meta.logo,
+ crawledAt: new Date(),
},
});
// Enqueue openai job
OpenAIQueue.add("openai", {
- linkId,
+ bookmarkId,
});
}
diff --git a/packages/workers/openai.ts b/packages/workers/openai.ts
index 893aa1af..b6044dd3 100644
--- a/packages/workers/openai.ts
+++ b/packages/workers/openai.ts
@@ -1,4 +1,4 @@
-import prisma, { BookmarkedLink, BookmarkedLinkDetails } from "@remember/db";
+import prisma, { BookmarkedLink } from "@remember/db";
import logger from "@remember/shared/logger";
import { ZOpenAIRequest, zOpenAIRequestSchema } from "@remember/shared/queues";
import { Job } from "bullmq";
@@ -9,14 +9,6 @@ const openAIResponseSchema = z.object({
tags: z.array(z.string()),
});
-let openai: OpenAI | undefined;
-
-if (process.env.OPENAI_API_KEY && process.env.OPENAI_ENABLED) {
- openai = new OpenAI({
- apiKey: process.env["OPENAI_API_KEY"], // This is the default and can be omitted
- });
-}
-
function buildPrompt(url: string, description: string) {
return `
You are a bot who given an article, extracts relevant "hashtags" out of them.
@@ -27,24 +19,19 @@ Description: ${description}
`;
}
-async function fetchLink(linkId: string) {
- return await prisma.bookmarkedLink.findUnique({
+async function fetchBookmark(linkId: string) {
+ return await prisma.bookmark.findUnique({
where: {
id: linkId,
},
include: {
- details: true,
+ link: true,
},
});
}
-async function inferTags(
- jobId: string,
- link: BookmarkedLink,
- linkDetails: BookmarkedLinkDetails | null,
- openai: OpenAI,
-) {
- const linkDescription = linkDetails?.description;
+async function inferTags(jobId: string, link: BookmarkedLink, openai: OpenAI) {
+ const linkDescription = link?.description;
if (!linkDescription) {
throw new Error(
`[openai][${jobId}] No description found for link "${link.id}". Skipping ...`,
@@ -119,14 +106,15 @@ async function createTags(tags: string[], userId: string) {
return existingTags.map((t) => t.id).concat(newTagObjects.map((t) => t.id));
}
-async function connectTags(linkId: string, tagIds: string[]) {
+async function connectTags(bookmarkId: string, tagIds: string[]) {
// TODO: Prisma doesn't support createMany in Sqlite
await Promise.all(
tagIds.map((tagId) => {
- return prisma.tagsOnLinks.create({
+ return prisma.tagsOnBookmarks.create({
data: {
tagId,
- linkId,
+ bookmarkId,
+ attachedBy: "ai",
},
});
}),
@@ -136,6 +124,13 @@ async function connectTags(linkId: string, tagIds: string[]) {
export default async function runOpenAI(job: Job<ZOpenAIRequest, void>) {
const jobId = job.id || "unknown";
+ if (!process.env.OPENAI_API_KEY || !process.env.OPENAI_ENABLED) {
+ return;
+ }
+
+ const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+ });
if (!openai) {
logger.debug(
`[openai][${jobId}] OpenAI is not configured, nothing to do now`,
@@ -150,14 +145,22 @@ export default async function runOpenAI(job: Job<ZOpenAIRequest, void>) {
);
}
- const { linkId } = request.data;
- const link = await fetchLink(linkId);
- if (!link) {
- throw new Error(`[openai][${jobId}] link with id ${linkId} was not found`);
+ const { bookmarkId } = request.data;
+ const bookmark = await fetchBookmark(bookmarkId);
+ if (!bookmark) {
+ throw new Error(
+ `[openai][${jobId}] bookmark with id ${bookmarkId} was not found`,
+ );
+ }
+
+ if (!bookmark.link) {
+ throw new Error(
+ `[openai][${jobId}] bookmark with id ${bookmarkId} doesn't have a link`,
+ );
}
- const tags = await inferTags(jobId, link, link.details, openai);
+ const tags = await inferTags(jobId, bookmark.link, openai);
- const tagIds = await createTags(tags, link.userId);
- await connectTags(linkId, tagIds);
+ const tagIds = await createTags(tags, bookmark.userId);
+ await connectTags(bookmarkId, tagIds);
}
diff --git a/yarn.lock b/yarn.lock
index 0d41501a..850c763f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1112,6 +1112,7 @@ __metadata:
"@hookform/resolvers": "npm:^3.3.4"
"@next-auth/prisma-adapter": "npm:^1.0.7"
"@next/eslint-plugin-next": "npm:^14.1.0"
+ "@prisma/client": "npm:^5.9.1"
"@radix-ui/react-dropdown-menu": "npm:^2.0.6"
"@radix-ui/react-label": "npm:^2.0.2"
"@radix-ui/react-slot": "npm:^1.0.2"
@@ -5729,6 +5730,7 @@ __metadata:
"@typescript-eslint/parser": "npm:^6.21.0"
bullmq: "npm:^5.1.9"
class-variance-authority: "npm:^0.7.0"
+ es-errors: "npm:^1.3.0"
eslint: "npm:^8.56.0"
eslint-config-next: "npm:14.1.0"
eslint-config-prettier: "npm:^9.1.0"