diff options
| author | Dhravya <[email protected]> | 2024-06-30 14:07:16 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-30 14:07:16 -0500 |
| commit | 7676ed57065eb61d54d500e68c7a7faae16e558b (patch) | |
| tree | 0d59a7dd9ee1772667abbc03158e268732ff5214 /apps/web | |
| parent | tailwind shadow dom (diff) | |
| download | supermemory-7676ed57065eb61d54d500e68c7a7faae16e558b.tar.xz supermemory-7676ed57065eb61d54d500e68c7a7faae16e558b.zip | |
added things
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/app/(dash)/home/homeVariants.ts | 2 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/page.tsx | 7 | ||||
| -rw-r--r-- | apps/web/app/(dash)/memories/page.tsx | 90 | ||||
| -rw-r--r-- | apps/web/app/(dash)/memories/render-tweet.tsx | 33 | ||||
| -rw-r--r-- | apps/web/app/(dash)/menu.tsx | 21 | ||||
| -rw-r--r-- | apps/web/app/actions/doers.ts | 88 | ||||
| -rw-r--r-- | apps/web/app/actions/fetchers.ts | 14 | ||||
| -rw-r--r-- | apps/web/app/api/store/route.ts | 202 | ||||
| -rw-r--r-- | apps/web/migrations/0001_index_telegram_id.sql | 4 | ||||
| -rw-r--r-- | apps/web/migrations/000_setup.sql (renamed from apps/web/migrations/0000_classy_speed_demon.sql) | 0 | ||||
| -rw-r--r-- | apps/web/migrations/meta/0001_snapshot.json | 698 | ||||
| -rw-r--r-- | apps/web/migrations/meta/_journal.json | 7 | ||||
| -rw-r--r-- | apps/web/server/db/schema.ts | 2 | ||||
| -rw-r--r-- | apps/web/tsconfig.json | 3 |
14 files changed, 1100 insertions, 71 deletions
diff --git a/apps/web/app/(dash)/home/homeVariants.ts b/apps/web/app/(dash)/home/homeVariants.ts index ec24e22b..cc533fc4 100644 --- a/apps/web/app/(dash)/home/homeVariants.ts +++ b/apps/web/app/(dash)/home/homeVariants.ts @@ -44,7 +44,7 @@ export const variants = [ }, { type: "highlighted", - content: " digital treasures.", + content: " digital treasure.", }, ], ]; diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 4f7e4af6..f648923c 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import QueryInput from "./queryinput"; import { homeSearchParamsCache } from "@/lib/searchParams"; -import { getSpaces } from "@/app/actions/fetchers"; +import { getSessionAuthToken, getSpaces } from "@/app/actions/fetchers"; import { useRouter } from "next/navigation"; import { createChatThread, linkTelegramToUser } from "@/app/actions/doers"; import { toast } from "sonner"; @@ -67,6 +67,11 @@ function Page({ }); setShowVariant(Math.floor(Math.random() * variants.length)); + + getSessionAuthToken().then((token) => { + if (typeof window === "undefined") return; + window.postMessage({ token: token.data }, "*"); + }); }, []); return ( diff --git a/apps/web/app/(dash)/memories/page.tsx b/apps/web/app/(dash)/memories/page.tsx index cb3825e7..380a653e 100644 --- a/apps/web/app/(dash)/memories/page.tsx +++ b/apps/web/app/(dash)/memories/page.tsx @@ -1,18 +1,19 @@ "use client"; import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers"; -import { Space } from "@/app/actions/types"; import { Content, StoredSpace } from "@/server/db/schema"; -import { NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons"; +import { MemoriesIcon, NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons"; +import { NotebookIcon, PaperclipIcon } from "lucide-react"; import Image from "next/image"; +import Link from "next/link"; import React, { useEffect, useMemo, useState } from "react"; import Masonry from "react-layout-masonry"; +import { getRawTweet } from "@repo/shared-types/utils"; +import { MyTweet } from "./render-tweet"; function Page() { const [filter, setFilter] = useState("All"); - const [search, setSearch] = useState(""); - const [memoriesAndSpaces, setMemoriesAndSpaces] = useState<{ memories: Content[]; spaces: StoredSpace[]; @@ -55,9 +56,13 @@ function Page() { return ( item.item === "memory" && (item.data as Content).type === "note" ); + if (filter === "Tweet") + return ( + item.item === "memory" && (item.data as Content).type === "tweet" + ); return false; }) - .sort((a, b) => a.date - b.date); + .sort((a, b) => b.date - a.date); }, [memoriesAndSpaces.memories, memoriesAndSpaces.spaces, filter]); useEffect(() => { @@ -69,7 +74,7 @@ function Page() { }, []); return ( - <div className="max-w-3xl min-w-3xl py-36 h-full flex mx-auto w-full flex-col gap-6"> + <div className="px-2 md:px-32 py-36 h-full flex mx-auto w-full flex-col gap-6"> <h2 className="text-white w-full font-medium text-2xl text-left"> My Memories </h2> @@ -79,7 +84,7 @@ function Page() { <Masonry className="mt-6" columns={{ 640: 1, 768: 2, 1024: 3, 1280: 4 }} - gap={4} + gap={12} > {sortedItems.map((item) => { if (item.item === "memory") { @@ -88,12 +93,15 @@ function Page() { type={(item.data as Content).type ?? "note"} content={(item.data as Content).content} title={(item.data as Content).title ?? "Untitled"} - url={(item.data as Content).url} + url={ + (item.data as Content).baseUrl ?? (item.data as Content).url + } image={ (item.data as Content).ogImage ?? (item.data as Content).image ?? "/placeholder-image.svg" // TODO: add this placeholder } + description={(item.data as Content).description ?? ""} /> ); } @@ -123,47 +131,73 @@ function TabComponent({ }) { // TODO: Display the space name and desription which is the number of elemenet in the space return ( - <div className="flex items-center my-6"> - <div> - <div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md"> - {title.slice(0, 2).toUpperCase()} - </div> - </div> - <div className="grow px-4"> - <div className="text-lg text-[#fff]">{title}</div> - <div>{description}</div> + <div className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4 -z-10"> + <div className="flex items-center gap-2 text-xs"> + <Image alt="Spaces icon" src={MemoriesIcon} className="size-3" /> Space </div> - <div> - <Image src={NextIcon} alt="Search icon" /> + <div className="flex items-center"> + <div> + <div className="h-12 w-12 flex justify-center items-center rounded-md"> + {title.slice(0, 2).toUpperCase()} + </div> + </div> + <div className="grow px-4"> + <div className="text-lg text-[#fff] line-clamp-2">{title}</div> + <div>{description}</div> + </div> + <div> + <Image src={NextIcon} alt="Search icon" /> + </div> </div> </div> ); } -function LinkComponent({ +export function LinkComponent({ type, content, title, url, image, + description, }: { type: string; content: string; title: string; url: string; image?: string; + description: string; }) { // TODO: DISPLAY THE ITEM BASED ON `type` being note or page return ( - <div className="w-full"> - <div className="text-lg text-[#fff]">{title}</div> - <div>{content}</div> - <div>{url}</div> - </div> + <Link + href={url} + className={`bg-secondary border-2 border-border w-full rounded-xl ${type === "tweet" ? "" : "p-4"} hover:scale-105 transition duration-200`} + > + {type === "page" ? ( + <> + <div className="flex items-center gap-2 text-xs"> + <PaperclipIcon className="w-3 h-3" /> Page + </div> + <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> + <div>{url}</div> + </> + ) : type === "note" ? ( + <> + <div className="flex items-center gap-2 text-xs"> + <NotebookIcon className="w-3 h-3" /> Note + </div> + <div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div> + <div className="line-clamp-3">{content.replace(title, "")}</div> + </> + ) : type === "tweet" ? ( + <MyTweet tweet={JSON.parse(getRawTweet(content) ?? "{}")} /> + ) : null} + </Link> ); } -const FilterMethods = ["All", "Spaces", "Pages", "Notes"]; +const FilterMethods = ["All", "Spaces", "Pages", "Notes", "Tweet"]; function Filters({ setFilter, filter, @@ -175,12 +209,12 @@ function Filters({ <div className="flex gap-4"> {FilterMethods.map((i) => { return ( - <div + <button onClick={() => setFilter(i)} className={`transition px-6 py-2 rounded-xl bg-border ${i === filter ? " text-[#369DFD]" : "text-[#B3BCC5] bg-secondary hover:bg-secondary hover:text-[#76a3cc]"}`} > {i} - </div> + </button> ); })} </div> diff --git a/apps/web/app/(dash)/memories/render-tweet.tsx b/apps/web/app/(dash)/memories/render-tweet.tsx new file mode 100644 index 00000000..3e1e3746 --- /dev/null +++ b/apps/web/app/(dash)/memories/render-tweet.tsx @@ -0,0 +1,33 @@ +import type { Tweet } from "react-tweet/api"; +import { + type TwitterComponents, + TweetContainer, + TweetHeader, + TweetInReplyTo, + TweetBody, + TweetMedia, + TweetInfo, + QuotedTweet, + enrichTweet, +} from "react-tweet"; + +type Props = { + tweet: Tweet; + components?: TwitterComponents; +}; + +export const MyTweet = ({ tweet: t, components }: Props) => { + const tweet = enrichTweet(t); + return ( + <TweetContainer className="bg-transparent !m-0 !p-0 !z-0"> + <TweetHeader tweet={tweet} components={components} /> + {tweet.in_reply_to_status_id_str && <TweetInReplyTo tweet={tweet} />} + <TweetBody tweet={tweet} /> + {tweet.mediaDetails?.length ? ( + <TweetMedia tweet={tweet} components={components} /> + ) : null} + {tweet.quoted_tweet && <QuotedTweet tweet={tweet.quoted_tweet} />} + <TweetInfo tweet={tweet} /> + </TweetContainer> + ); +}; diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx index 340c7e16..a8ba4172 100644 --- a/apps/web/app/(dash)/menu.tsx +++ b/apps/web/app/(dash)/menu.tsx @@ -65,12 +65,6 @@ function Menu() { disabled: false, }, { - icon: ExploreIcon, - text: "Explore", - url: "/explore", - disabled: true, - }, - { icon: CanvasIcon, text: "Canvas", url: "/canvas", @@ -86,7 +80,9 @@ function Menu() { return "none"; } - if (content.match(/https?:\/\/[\w\.]+\/[\w]+\/[\w]+\/[\d]+/)) { + if ( + content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/) + ) { return "tweet"; } else if (content.match(/https?:\/\/[\w\.]+/)) { return "page"; @@ -136,8 +132,8 @@ function Menu() { <> {/* Desktop Menu */} <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> - <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none"> - <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40"> + <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none z-[39]"> + <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40 z-[99999]"> <div className="border-b border-border pb-4 w-full"> <DialogTrigger className={`flex w-full text-white brightness-75 hover:brightness-125 focus:brightness-125 cursor-pointer items-center gap-3 px-1 duration-200 justify-start`} @@ -180,7 +176,7 @@ function Menu() { </div> </div> - <DialogContent className="sm:max-w-[425px] rounded-2xl bg-[#161f2a]/30 backdrop-blur-md"> + <DialogContent className="sm:max-w-[425px] rounded-2xl bg-[#161f2a]/40 backdrop-blur-md"> <form action={async (e: FormData) => { const content = e.get("content")?.toString(); @@ -202,7 +198,7 @@ function Menu() { Resource (URL or content) </Label> <Textarea - className="bg-[#161f2a] focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2" + className="bg-[#2F353C] focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2" id="content" name="content" placeholder="Start typing a note or paste a URL here. I'll remember it." @@ -287,7 +283,7 @@ function Menu() { } }} placeholder="Save or create space by typing." - className="bg-[#161f2a] h-min rounded-md mt-2 mb-4" + className="bg-[#2F353C] h-min rounded-md mt-2 mb-4" /> <div> @@ -296,6 +292,7 @@ function Menu() { {selectedSpaces.map((x, idx) => ( <button key={x} + type="button" onClick={() => setSelectedSpaces((prev) => prev.filter((y) => y !== x), diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts index 91a40f2c..69e57687 100644 --- a/apps/web/app/actions/doers.ts +++ b/apps/web/app/actions/doers.ts @@ -6,6 +6,7 @@ import { chatHistory, chatThreads, contentToSpace, + sessions, space, storedContent, users, @@ -17,9 +18,11 @@ import { getMetaData } from "@/lib/get-metadata"; import { and, eq, inArray, sql } from "drizzle-orm"; import { LIMITS } from "@/lib/constants"; import { z } from "zod"; -import { ChatHistory } from "@repo/shared-types"; +import { AddFromAPIType, ChatHistory } from "@repo/shared-types"; import { decipher } from "@/server/encrypt"; import { redirect } from "next/navigation"; +import { ensureAuth } from "../api/ensureAuth"; +import { tweetToMd } from "@repo/shared-types/utils"; export const createSpace = async ( input: string | FormData, @@ -61,7 +64,7 @@ export const createSpace = async ( const typeDecider = (content: string) => { // if the content is a URL, then it's a page. if its a URL with https://x.com/user/status/123, then it's a tweet. else, it's a note. // do strict checking with regex - if (content.match(/https?:\/\/[\w\.]+\/[\w]+\/[\w]+\/[\d]+/)) { + if (content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/)) { return "tweet"; } else if (content.match(/https?:\/\/[\w\.]+/)) { return "page"; @@ -72,19 +75,23 @@ const typeDecider = (content: string) => { } }; -export const limit = async (userId: string, type = "page") => { - const count = await db +export const limit = async ( + userId: string, + type = "page", + items: number = 1, +) => { + const countResult = await db .select({ count: sql<number>`count(*)`.mapWith(Number), }) .from(storedContent) .where(and(eq(storedContent.userId, userId), eq(storedContent.type, type))); - if (count[0]!.count > LIMITS[type as keyof typeof LIMITS]) { - return false; - } + const currentCount = countResult[0]?.count || 0; + const totalLimit = LIMITS[type as keyof typeof LIMITS]; + const remainingLimit = totalLimit - currentCount; - return true; + return items <= remainingLimit; }; const getTweetData = async (tweetID: string) => { @@ -133,6 +140,8 @@ export const createMemory = async (input: { }; } + let noteId = 0; + if (type === "page") { const response = await fetch("https://md.dhr.wtf/?url=" + input.content, { headers: { @@ -151,7 +160,7 @@ export const createMemory = async (input: { } } else if (type === "tweet") { const tweet = await getTweetData(input.content.split("/").pop() as string); - pageContent = JSON.stringify(tweet); + pageContent = tweetToMd(tweet); metadata = { baseUrl: input.content, description: tweet.text, @@ -160,7 +169,7 @@ export const createMemory = async (input: { }; } else if (type === "note") { pageContent = input.content; - const noteId = new Date().getTime(); + noteId = new Date().getTime(); metadata = { baseUrl: `https://supermemory.ai/note/${noteId}`, description: `Note created at ${new Date().toLocaleString()}`, @@ -212,23 +221,49 @@ export const createMemory = async (input: { }; } + let contentId: number | undefined; + // Insert into database - const insertResponse = await db - .insert(storedContent) - .values({ - content: pageContent, - title: metadata.title, - description: metadata.description, - url: input.content, - baseUrl: metadata.baseUrl, - image: metadata.image, - savedAt: new Date(), - userId: data.user.id, - type, - }) - .returning({ id: storedContent.id }); + try { + const insertResponse = await db + .insert(storedContent) + .values({ + content: pageContent, + title: metadata.title, + description: metadata.description, + url: input.content, + baseUrl: metadata.baseUrl, + image: metadata.image, + savedAt: new Date(), + userId: data.user.id, + type, + }) + .returning({ id: storedContent.id }); + + contentId = insertResponse[0]?.id; + } catch (e) { + const error = e as Error; + console.log("Error: ", error.message); + + if ( + error.message.includes( + "D1_ERROR: UNIQUE constraint failed: storedContent.baseUrl", + ) + ) { + return { + success: false, + data: 0, + error: "Content already exists", + }; + } + + return { + success: false, + data: 0, + error: "Failed to save to database with error: " + error.message, + }; + } - const contentId = insertResponse[0]?.id; if (!contentId) { return { success: false, @@ -277,6 +312,9 @@ export const createMemory = async (input: { }; } + revalidatePath("/home"); + revalidatePath("/memories"); + return { success: true, data: 1, diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts index 558ef3de..0035c217 100644 --- a/apps/web/app/actions/fetchers.ts +++ b/apps/web/app/actions/fetchers.ts @@ -18,6 +18,7 @@ import { ChatHistory, SourceZod } from "@repo/shared-types"; import { ChatHistory as ChatHistoryType } from "../../server/db/schema"; import { z } from "zod"; import { redirect } from "next/navigation"; +import { cookies, headers } from "next/headers"; export const getSpaces = async (): ServerActionReturnType<StoredSpace[]> => { const data = await auth(); @@ -186,3 +187,16 @@ export const getChatHistory = async (): ServerActionReturnType< }; } }; + +export const getSessionAuthToken = async (): ServerActionReturnType<string> => { + const token = + cookies().get("next-auth.session-token")?.value ?? + cookies().get("__Secure-authjs.session-token")?.value ?? + cookies().get("authjs.session-token")?.value ?? + headers().get("Authorization")?.replace("Bearer ", ""); + + return { + success: true, + data: token, + }; +}; diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts new file mode 100644 index 00000000..8a126a56 --- /dev/null +++ b/apps/web/app/api/store/route.ts @@ -0,0 +1,202 @@ +import { type NextRequest } from "next/server"; +import { addFromAPIType, AddFromAPIType } from "@repo/shared-types"; +import { ensureAuth } from "../ensureAuth"; +import { z } from "zod"; +import { db } from "@/server/db"; +import { contentToSpace, space, storedContent } from "@/server/db/schema"; +import { and, eq, inArray } from "drizzle-orm"; +import { LIMITS } from "@/lib/constants"; +import { limit } from "@/app/actions/doers"; + +export const runtime = "edge"; + +const createMemoryFromAPI = async (input: { + data: AddFromAPIType; + userId: string; +}) => { + if (!(await limit(input.userId, input.data.type))) { + return { + success: false, + data: 0, + error: `You have exceeded the limit of ${LIMITS[input.data.type as keyof typeof LIMITS]} ${input.data.type}s.`, + }; + } + + const vectorSaveResponse = await fetch( + `${process.env.BACKEND_BASE_URL}/api/add`, + { + method: "POST", + body: JSON.stringify({ + pageContent: input.data.pageContent, + title: input.data.title, + description: input.data.description, + url: input.data.url, + spaces: input.data.spaces, + user: input.userId, + type: input.data.type, + }), + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY, + }, + }, + ); + + if (!vectorSaveResponse.ok) { + const errorData = await vectorSaveResponse.text(); + console.error(errorData); + return { + success: false, + data: 0, + error: `Failed to save to vector store. Backend returned error: ${errorData}`, + }; + } + + let contentId: number | undefined; + + // Insert into database + try { + const insertResponse = await db + .insert(storedContent) + .values({ + content: input.data.pageContent, + title: input.data.title, + description: input.data.description, + url: input.data.url, + baseUrl: input.data.url, + image: input.data.image, + savedAt: new Date(), + userId: input.userId, + type: input.data.type, + }) + .returning({ id: storedContent.id }); + + contentId = insertResponse[0]?.id; + } catch (e) { + const error = e as Error; + console.log("Error: ", error.message); + + if ( + error.message.includes( + "D1_ERROR: UNIQUE constraint failed: storedContent.baseUrl", + ) + ) { + return { + success: false, + data: 0, + error: "Content already exists", + }; + } + + return { + success: false, + data: 0, + error: "Failed to save to database with error: " + error.message, + }; + } + + if (!contentId) { + return { + success: false, + data: 0, + error: "Failed to save to database", + }; + } + + if (input.data.spaces.length > 0) { + // Adding the many-to-many relationship between content and spaces + const spaceData = await db + .select() + .from(space) + .where( + and( + inArray( + space.id, + input.data.spaces.map((s) => parseInt(s)), + ), + eq(space.user, input.userId), + ), + ) + .all(); + + await Promise.all( + spaceData.map(async (space) => { + await db + .insert(contentToSpace) + .values({ contentId: contentId, spaceId: space.id }); + }), + ); + } + + try { + const response = await vectorSaveResponse.json(); + + const expectedResponse = z.object({ status: z.literal("ok") }); + + const parsedResponse = expectedResponse.safeParse(response); + + if (!parsedResponse.success) { + return { + success: false, + data: 0, + error: `Failed to save to vector store. Backend returned error: ${parsedResponse.error.message}`, + }; + } + + return { + success: true, + data: 1, + }; + } catch (e) { + return { + success: false, + data: 0, + error: `Failed to save to vector store. Backend returned error: ${e}`, + }; + } +}; + +export async function POST(req: NextRequest) { + const session = await ensureAuth(req); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + if (!process.env.BACKEND_SECURITY_KEY) { + return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 }); + } + + const body = await req.json(); + + const validated = addFromAPIType.safeParse(body); + + if (!validated.success) { + return new Response( + JSON.stringify({ + message: "Invalid request", + error: validated.error, + }), + { status: 400 }, + ); + } + + const data = validated.data; + + const result = await createMemoryFromAPI({ + data, + userId: session.user.id, + }); + + if (!result.success) { + return new Response( + JSON.stringify({ + message: "Failed to save document", + error: result.error, + }), + { status: 500 }, + ); + } + + return new Response("ok", { status: 200 }); +} diff --git a/apps/web/migrations/0001_index_telegram_id.sql b/apps/web/migrations/0001_index_telegram_id.sql index 4b3aa44a..bff9d007 100644 --- a/apps/web/migrations/0001_index_telegram_id.sql +++ b/apps/web/migrations/0001_index_telegram_id.sql @@ -3,7 +3,7 @@ ALTER TABLE `space` ADD COLUMN `numItems` integer NOT NULL DEFAULT 0; --> statement-breakpoint ALTER TABLE `user` ADD COLUMN `telegramId` text; -CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`);--> statement-breakpoint CREATE INDEX `users_email_idx` ON `user` (`email`);--> statement-breakpoint CREATE INDEX `users_telegram_idx` ON `user` (`telegramId`);--> statement-breakpoint -CREATE INDEX `users_id_idx` ON `user` (`id`);
\ No newline at end of file +CREATE INDEX `users_id_idx` ON `user` (`id`); +CREATE UNIQUE INDEX `storedContent_baseUrl_unique` ON `storedContent` (`baseUrl`);
\ No newline at end of file diff --git a/apps/web/migrations/0000_classy_speed_demon.sql b/apps/web/migrations/000_setup.sql index a4855ec9..a4855ec9 100644 --- a/apps/web/migrations/0000_classy_speed_demon.sql +++ b/apps/web/migrations/000_setup.sql diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..6d8e6ebc --- /dev/null +++ b/apps/web/migrations/meta/0001_snapshot.json @@ -0,0 +1,698 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3f58a63e-3416-40c0-b8ef-82aa87eeed77", + "prevId": "6988522d-8117-484d-b52a-94c0fbd75140", + "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": {} + }, + "authenticator": { + "name": "authenticator", + "columns": { + "credentialID": { + "name": "credentialID", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialPublicKey": { + "name": "credentialPublicKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialDeviceType": { + "name": "credentialDeviceType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credentialBackedUp": { + "name": "credentialBackedUp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "authenticator_credentialID_unique": { + "name": "authenticator_credentialID_unique", + "columns": ["credentialID"], + "isUnique": true + } + }, + "foreignKeys": { + "authenticator_userId_user_id_fk": { + "name": "authenticator_userId_user_id_fk", + "tableFrom": "authenticator", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "authenticator_userId_credentialID_pk": { + "columns": ["credentialID", "userId"], + "name": "authenticator_userId_credentialID_pk" + } + }, + "uniqueConstraints": {} + }, + "chatHistory": { + "name": "chatHistory", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "threadId": { + "name": "threadId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answerParts": { + "name": "answerParts", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "answerSources": { + "name": "answerSources", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "answerJustification": { + "name": "answerJustification", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "chatHistory_thread_idx": { + "name": "chatHistory_thread_idx", + "columns": ["threadId"], + "isUnique": false + } + }, + "foreignKeys": { + "chatHistory_threadId_chatThread_id_fk": { + "name": "chatHistory_threadId_chatThread_id_fk", + "tableFrom": "chatHistory", + "tableTo": "chatThread", + "columnsFrom": ["threadId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "chatThread": { + "name": "chatThread", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "firstMessage": { + "name": "firstMessage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "chatThread_user_idx": { + "name": "chatThread_user_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "chatThread_userId_user_id_fk": { + "name": "chatThread_userId_user_id_fk", + "tableFrom": "chatThread", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "contentToSpace": { + "name": "contentToSpace", + "columns": { + "contentId": { + "name": "contentId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spaceId": { + "name": "spaceId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "contentToSpace_contentId_storedContent_id_fk": { + "name": "contentToSpace_contentId_storedContent_id_fk", + "tableFrom": "contentToSpace", + "tableTo": "storedContent", + "columnsFrom": ["contentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contentToSpace_spaceId_space_id_fk": { + "name": "contentToSpace_spaceId_space_id_fk", + "tableFrom": "contentToSpace", + "tableTo": "space", + "columnsFrom": ["spaceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contentToSpace_contentId_spaceId_pk": { + "columns": ["contentId", "spaceId"], + "name": "contentToSpace_contentId_spaceId_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": {} + }, + "space": { + "name": "space", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "user": { + "name": "user", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "numItems": { + "name": "numItems", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "space_name_unique": { + "name": "space_name_unique", + "columns": ["name"], + "isUnique": true + }, + "spaces_name_idx": { + "name": "spaces_name_idx", + "columns": ["name"], + "isUnique": false + }, + "spaces_user_idx": { + "name": "spaces_user_idx", + "columns": ["user"], + "isUnique": false + } + }, + "foreignKeys": { + "space_user_user_id_fk": { + "name": "space_user_user_id_fk", + "tableFrom": "space", + "tableTo": "user", + "columnsFrom": ["user"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "storedContent": { + "name": "storedContent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "savedAt": { + "name": "savedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "baseUrl": { + "name": "baseUrl", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ogImage": { + "name": "ogImage", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'page'" + }, + "image": { + "name": "image", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "storedContent_baseUrl_unique": { + "name": "storedContent_baseUrl_unique", + "columns": ["baseUrl"], + "isUnique": true + }, + "storedContent_url_idx": { + "name": "storedContent_url_idx", + "columns": ["url"], + "isUnique": false + }, + "storedContent_savedAt_idx": { + "name": "storedContent_savedAt_idx", + "columns": ["savedAt"], + "isUnique": false + }, + "storedContent_title_idx": { + "name": "storedContent_title_idx", + "columns": ["title"], + "isUnique": false + }, + "storedContent_user_idx": { + "name": "storedContent_user_idx", + "columns": ["user"], + "isUnique": false + } + }, + "foreignKeys": { + "storedContent_user_user_id_fk": { + "name": "storedContent_user_user_id_fk", + "tableFrom": "storedContent", + "tableTo": "user", + "columnsFrom": ["user"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "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 + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": ["email"], + "isUnique": false + }, + "users_telegram_idx": { + "name": "users_telegram_idx", + "columns": ["telegramId"], + "isUnique": false + }, + "users_id_idx": { + "name": "users_id_idx", + "columns": ["id"], + "isUnique": false + } + }, + "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": {} + } +} diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index 552ae12f..3c6b0ed8 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1719671614406, "tag": "0000_classy_speed_demon", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1719772835765, + "tag": "0001_chubby_vulture", + "breakpoints": true } ] } diff --git a/apps/web/server/db/schema.ts b/apps/web/server/db/schema.ts index e1b94cf3..26a4f22d 100644 --- a/apps/web/server/db/schema.ts +++ b/apps/web/server/db/schema.ts @@ -110,7 +110,7 @@ export const storedContent = createTable( description: text("description", { length: 255 }), url: text("url").notNull(), savedAt: int("savedAt", { mode: "timestamp" }).notNull(), - baseUrl: text("baseUrl", { length: 255 }), + baseUrl: text("baseUrl", { length: 255 }).unique(), ogImage: text("ogImage", { length: 255 }), type: text("type").default("page"), image: text("image", { length: 255 }), diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 34e0a9b0..63996482 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -17,7 +17,8 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "../../packages/ui/" + "../../packages/ui/", + "../../packages/shared-types/utils.ts" ], "exclude": ["node_modules/"] } |