diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-09 01:50:35 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-09 01:57:31 +0000 |
| commit | 08a5694e451218f1bcb2ad9eb42fd93250afbb96 (patch) | |
| tree | 2cc2351f26b0ab98268db4bc463c0c3aa3f78a3b | |
| parent | c5bfa5000f178475d0b019b5a960916134b2ecfb (diff) | |
| download | karakeep-08a5694e451218f1bcb2ad9eb42fd93250afbb96.tar.zst | |
[refactor] Extract the bookmark model to be a high level model to support other type of bookmarks
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); } @@ -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" |
