diff options
| author | Dhravya <[email protected]> | 2024-06-14 23:35:19 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-14 23:35:19 -0500 |
| commit | 9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2 (patch) | |
| tree | 372de593c837b0a963c081a1db58447ac4e08084 /apps/web/app | |
| parent | Merge branch 'codetorso' of https://github.com/Dhravya/supermemory into codet... (diff) | |
| download | supermemory-9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2.tar.xz supermemory-9f0fb14d56fcbd9280aee2bdd83d55e8fabbe7d2.zip | |
[MIGRATION REQUIRED]Data fetchers and other server actions, spaces creation and database migrations
Diffstat (limited to 'apps/web/app')
| -rw-r--r-- | apps/web/app/(dash)/actions.ts | 48 | ||||
| -rw-r--r-- | apps/web/app/(dash)/dynamicisland.tsx | 109 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/page.tsx | 11 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/queryinput.tsx | 21 | ||||
| -rw-r--r-- | apps/web/app/(dash)/layout.tsx | 7 | ||||
| -rw-r--r-- | apps/web/app/actions/doers.ts | 43 | ||||
| -rw-r--r-- | apps/web/app/actions/fetchers.ts | 25 | ||||
| -rw-r--r-- | apps/web/app/actions/types.ts | 10 | ||||
| -rw-r--r-- | apps/web/app/helpers/server/auth.ts | 25 | ||||
| -rw-r--r-- | apps/web/app/helpers/server/db/schema.ts | 99 |
10 files changed, 265 insertions, 133 deletions
diff --git a/apps/web/app/(dash)/actions.ts b/apps/web/app/(dash)/actions.ts deleted file mode 100644 index 70c2a567..00000000 --- a/apps/web/app/(dash)/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -"use server"; - -import { cookies, headers } from "next/headers"; -import { db } from "../helpers/server/db"; -import { sessions, users, space } from "../helpers/server/db/schema"; -import { eq } from "drizzle-orm"; -import { redirect } from "next/navigation"; - -export async function ensureAuth() { - 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 ", ""); - - if (!token) { - return undefined; - } - - const sessionData = await db - .select() - .from(sessions) - .innerJoin(users, eq(users.id, sessions.userId)) - .where(eq(sessions.sessionToken, token)); - - if (!sessionData || sessionData.length < 0) { - return undefined; - } - - return { - user: sessionData[0]!.user, - session: sessionData[0]!, - }; -} - -export async function getSpaces() { - const data = await ensureAuth(); - if (!data) { - redirect("/signin"); - } - - const sp = await db - .select() - .from(space) - .where(eq(space.user, data.user.email)); - - return sp; -} diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx index b703d55a..31f76fda 100644 --- a/apps/web/app/(dash)/dynamicisland.tsx +++ b/apps/web/app/(dash)/dynamicisland.tsx @@ -9,6 +9,17 @@ import { motion } from "framer-motion"; import { Label } from "@repo/ui/shadcn/label"; import { Input } from "@repo/ui/shadcn/input"; import { Textarea } from "@repo/ui/shadcn/textarea"; +import { createSpace } from "../actions/doers"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/ui/shadcn/select"; +import { Space } from "../actions/types"; +import { getSpaces } from "../actions/fetchers"; +import { toast } from "sonner"; export function DynamicIsland() { const { scrollYProgress } = useScroll(); @@ -80,7 +91,7 @@ function DynamicIslandContent() { {show ? ( <div onClick={() => setshow(!show)} - className="bg-[#1F2428] px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer" + className="bg-secondary px-3 w-[2.23rem] overflow-hidden hover:w-[9.2rem] whitespace-nowrap py-2 rounded-3xl transition-[width] cursor-pointer" > <div className="flex gap-4 items-center"> <Image src={AddIcon} alt="Add icon" /> @@ -99,7 +110,25 @@ function DynamicIslandContent() { const fakeitems = ["spaces", "page", "note"]; function ToolBar({ cancelfn }: { cancelfn: () => void }) { + const [spaces, setSpaces] = useState<Space[]>([]); + const [index, setIndex] = useState(0); + + useEffect(() => { + (async () => { + let spaces = await getSpaces(); + + if (!spaces.success || !spaces.data) { + toast.warning("Unable to get spaces", { + richColors: true, + }); + setSpaces([]); + return; + } + setSpaces(spaces.data); + })(); + }, []); + return ( <AnimatePresence mode="wait"> <motion.div @@ -120,7 +149,7 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) { }} className="flex flex-col items-center" > - <div className="bg-[#1F2428] py-[.35rem] px-[.6rem] rounded-2xl"> + <div className="bg-secondary py-[.35rem] px-[.6rem] rounded-2xl"> <HoverEffect items={fakeitems} index={index} @@ -130,9 +159,9 @@ function ToolBar({ cancelfn }: { cancelfn: () => void }) { {index === 0 ? ( <SpaceForm cancelfn={cancelfn} /> ) : index === 1 ? ( - <PageForm cancelfn={cancelfn} /> + <PageForm cancelfn={cancelfn} spaces={spaces} /> ) : ( - <NoteForm cancelfn={cancelfn} /> + <NoteForm cancelfn={cancelfn} spaces={spaces} /> )} </motion.div> </AnimatePresence> @@ -182,7 +211,10 @@ export const HoverEffect = ({ function SpaceForm({ cancelfn }: { cancelfn: () => void }) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <form + action={createSpace} + className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3" + > <div> <Label className="text-[#858B92]" htmlFor="name"> Name @@ -190,34 +222,55 @@ function SpaceForm({ cancelfn }: { cancelfn: () => void }) { <Input className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" id="name" + name="name" /> </div> <div className="flex justify-between"> <a className="text-blue-500" href=""> pull from store </a> - <div + {/* <div onClick={cancelfn} className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer" > cancel - </div> + </div> */} + <button + type="submit" + className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer" + > + Submit + </button> </div> - </div> + </form> ); } -function PageForm({ cancelfn }: { cancelfn: () => void }) { +function PageForm({ + cancelfn, + spaces, +}: { + cancelfn: () => void; + spaces: Space[]; +}) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> <div> - <Label className="text-[#858B92]" htmlFor="name"> + <Label className="text-[#858B92]" htmlFor="space"> Space </Label> - <Input - className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" - id="name" - /> + <Select> + <SelectTrigger> + <SelectValue placeholder="Space" /> + </SelectTrigger> + <SelectContent className="bg-secondary text-white"> + {spaces.map((space) => ( + <SelectItem key={space.id} value={space.id.toString()}> + {space.name} + </SelectItem> + ))} + </SelectContent> + </Select> </div> <div> <Label className="text-[#858B92]" htmlFor="name"> @@ -240,17 +293,31 @@ function PageForm({ cancelfn }: { cancelfn: () => void }) { ); } -function NoteForm({ cancelfn }: { cancelfn: () => void }) { +function NoteForm({ + cancelfn, + spaces, +}: { + cancelfn: () => void; + spaces: Space[]; +}) { return ( - <div className="bg-[#1F2428] px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> + <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"> <div> <Label className="text-[#858B92]" htmlFor="name"> Space </Label> - <Input - className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0" - id="name" - /> + <Select> + <SelectTrigger> + <SelectValue placeholder="Space" /> + </SelectTrigger> + <SelectContent className="bg-secondary text-white"> + {spaces.map((space) => ( + <SelectItem key={space.id} value={space.id.toString()}> + {space.name} + </SelectItem> + ))} + </SelectContent> + </Select> </div> <div> <Label className="text-[#858B92]" htmlFor="name"> diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 0c75e457..b4bafb38 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -3,7 +3,7 @@ import Menu from "../menu"; import Header from "../header"; import QueryInput from "./queryinput"; import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams"; -import { getSpaces } from "../actions"; +import { getSpaces } from "@/app/actions/fetchers"; async function Page({ searchParams, @@ -13,7 +13,12 @@ async function Page({ // TODO: use this to show a welcome page/modal const { firstTime } = homeSearchParamsCache.parse(searchParams); - const spaces = await getSpaces(); + let spaces = await getSpaces(); + + if (!spaces.success) { + // TODO: handle this error properly. + spaces.data = []; + } return ( <div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col"> @@ -21,7 +26,7 @@ async function Page({ {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */} <div className="w-full h-96"> - <QueryInput initialSpaces={spaces} /> + <QueryInput initialSpaces={spaces.data} /> </div> </div> ); diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx index 4cb1fdb2..d0c27b8d 100644 --- a/apps/web/app/(dash)/home/queryinput.tsx +++ b/apps/web/app/(dash)/home/queryinput.tsx @@ -2,10 +2,11 @@ import { ArrowRightIcon } from "@repo/ui/icons"; import Image from "next/image"; -import React, { useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import Divider from "@repo/ui/shadcn/divider"; import { MultipleSelector, Option } from "@repo/ui/shadcn/combobox"; import { useRouter } from "next/navigation"; +import { getSpaces } from "@/app/actions/fetchers"; function QueryInput({ initialQuery = "", @@ -13,7 +14,10 @@ function QueryInput({ disabled = false, }: { initialQuery?: string; - initialSpaces?: { user: string | null; id: number; name: string }[]; + initialSpaces?: { + id: number; + name: string; + }[]; disabled?: boolean; }) { const [q, setQ] = useState(initialQuery); @@ -41,10 +45,14 @@ function QueryInput({ return newQ; }; - const options = initialSpaces.map((x) => ({ - label: x.name, - value: x.id.toString(), - })); + const options = useMemo( + () => + initialSpaces.map((x) => ({ + label: x.name, + value: x.id.toString(), + })), + [initialSpaces], + ); return ( <div> @@ -82,6 +90,7 @@ function QueryInput({ {/* selected sources */} <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]"> <MultipleSelector + key={options.length} disabled={disabled} defaultOptions={options} onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))} diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx index 85f8476e..b879a2f5 100644 --- a/apps/web/app/(dash)/layout.tsx +++ b/apps/web/app/(dash)/layout.tsx @@ -1,10 +1,11 @@ import Header from "./header"; import Menu from "./menu"; -import { ensureAuth } from "./actions"; import { redirect } from "next/navigation"; +import { auth } from "../helpers/server/auth"; +import { Toaster } from "@repo/ui/shadcn/sonner"; async function Layout({ children }: { children: React.ReactNode }) { - const info = await ensureAuth(); + const info = await auth(); if (!info) { return redirect("/signin"); @@ -17,6 +18,8 @@ async function Layout({ children }: { children: React.ReactNode }) { <Menu /> {children} + + <Toaster /> </main> ); } diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts new file mode 100644 index 00000000..c8a1f3b4 --- /dev/null +++ b/apps/web/app/actions/doers.ts @@ -0,0 +1,43 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { db } from "../helpers/server/db"; +import { space } from "../helpers/server/db/schema"; +import { ServerActionReturnType } from "./types"; +import { auth } from "../helpers/server/auth"; + +export const createSpace = async ( + input: string | FormData, +): ServerActionReturnType<number> => { + const data = await auth(); + + if (!data || !data.user) { + return { error: "Not authenticated", success: false }; + } + + if (typeof input === "object") { + input = (input as FormData).get("name") as string; + } + + try { + const resp = await db + .insert(space) + .values({ name: input, user: data.user.id }); + + revalidatePath("/home"); + return { success: true, data: 1 }; + } catch (e: unknown) { + const error = e as Error; + if ( + error.message.includes("D1_ERROR: UNIQUE constraint failed: space.name") + ) { + return { success: false, data: 0, error: "Space already exists" }; + } else { + return { + success: false, + data: 0, + error: "Failed to create space with error: " + error.message, + }; + } + } +}; diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts new file mode 100644 index 00000000..9c2527f0 --- /dev/null +++ b/apps/web/app/actions/fetchers.ts @@ -0,0 +1,25 @@ +"use server"; + +import { eq } from "drizzle-orm"; +import { db } from "../helpers/server/db"; +import { users } from "../helpers/server/db/schema"; +import { ServerActionReturnType, Space } from "./types"; +import { auth } from "../helpers/server/auth"; + +export const getSpaces = async (): ServerActionReturnType<Space[]> => { + const data = await auth(); + + if (!data || !data.user) { + return { error: "Not authenticated", success: false }; + } + + const spaces = await db.query.space.findMany({ + where: eq(users, data.user.id), + }); + + const spacesWithoutUser = spaces.map((space) => { + return { ...space, user: undefined }; + }); + + return { success: true, data: spacesWithoutUser }; +}; diff --git a/apps/web/app/actions/types.ts b/apps/web/app/actions/types.ts new file mode 100644 index 00000000..fbf669e2 --- /dev/null +++ b/apps/web/app/actions/types.ts @@ -0,0 +1,10 @@ +export type Space = { + id: number; + name: string; +}; + +export type ServerActionReturnType<T> = Promise<{ + error?: string; + success: boolean; + data?: T; +}>; diff --git a/apps/web/app/helpers/server/auth.ts b/apps/web/app/helpers/server/auth.ts index 73119d87..c4e426d4 100644 --- a/apps/web/app/helpers/server/auth.ts +++ b/apps/web/app/helpers/server/auth.ts @@ -2,6 +2,7 @@ import NextAuth, { NextAuthResult } from "next-auth"; import Google from "next-auth/providers/google"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { db } from "./db"; +import { accounts, sessions, users, verificationTokens } from "./db/schema"; export const { handlers: { GET, POST }, @@ -10,16 +11,20 @@ export const { auth, } = NextAuth({ secret: process.env.BACKEND_SECURITY_KEY, - callbacks: { - session: ({ session, token, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: DrizzleAdapter(db), + // callbacks: { + // session: ({ session, token, user }) => ({ + // ...session, + // user: { + // ...session.user, + // }, + // }), + // }, + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }), providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID, diff --git a/apps/web/app/helpers/server/db/schema.ts b/apps/web/app/helpers/server/db/schema.ts index c4616eb2..e3e789c6 100644 --- a/apps/web/app/helpers/server/db/schema.ts +++ b/apps/web/app/helpers/server/db/schema.ts @@ -7,75 +7,88 @@ import { text, integer, } from "drizzle-orm/sqlite-core"; +import type { AdapterAccountType } from "next-auth/adapters"; export const createTable = sqliteTableCreator((name) => `${name}`); export const users = createTable("user", { - id: text("id", { length: 255 }).notNull().primaryKey(), - name: text("name", { length: 255 }), - email: text("email", { length: 255 }).notNull(), - emailVerified: int("emailVerified", { mode: "timestamp" }).default( - sql`CURRENT_TIMESTAMP`, - ), - image: text("image", { length: 255 }), + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text("name"), + email: text("email").notNull(), + emailVerified: integer("emailVerified", { mode: "timestamp_ms" }), + image: text("image"), }); export type User = typeof users.$inferSelect; -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - export const accounts = createTable( "account", { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - userId: text("userId", { length: 255 }) + userId: text("userId") .notNull() .references(() => users.id, { onDelete: "cascade" }), - type: text("type", { length: 255 }).notNull(), - provider: text("provider", { length: 255 }).notNull(), - providerAccountId: text("providerAccountId", { length: 255 }).notNull(), + type: text("type").$type<AdapterAccountType>().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("providerAccountId").notNull(), refresh_token: text("refresh_token"), access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: text("token_type", { length: 255 }), - scope: text("scope", { length: 255 }), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), id_token: text("id_token"), - session_state: text("session_state", { length: 255 }), - oauth_token_secret: text("oauth_token_secret"), - oauth_token: text("oauth_token"), + session_state: text("session_state"), }, (account) => ({ - userIdIdx: index("account_userId_idx").on(account.userId), + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), }), ); -export const sessions = createTable( - "session", +export const sessions = createTable("session", { + sessionToken: text("sessionToken").primaryKey(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), +}); + +export const verificationTokens = createTable( + "verificationToken", { - id: integer("id").notNull().primaryKey({ autoIncrement: true }), - sessionToken: text("sessionToken", { length: 255 }).notNull(), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - expires: int("expires", { mode: "timestamp" }).notNull(), + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: integer("expires", { mode: "timestamp_ms" }).notNull(), }, - (session) => ({ - userIdIdx: index("session_userId_idx").on(session.userId), + (verificationToken) => ({ + compositePk: primaryKey({ + columns: [verificationToken.identifier, verificationToken.token], + }), }), ); -export const verificationTokens = createTable( - "verificationToken", +export const authenticators = createTable( + "authenticator", { - identifier: text("identifier", { length: 255 }).notNull(), - token: text("token", { length: 255 }).notNull(), - expires: int("expires", { mode: "timestamp" }).notNull(), + credentialID: text("credentialID").notNull().unique(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + providerAccountId: text("providerAccountId").notNull(), + credentialPublicKey: text("credentialPublicKey").notNull(), + counter: integer("counter").notNull(), + credentialDeviceType: text("credentialDeviceType").notNull(), + credentialBackedUp: integer("credentialBackedUp", { + mode: "boolean", + }).notNull(), + transports: text("transports"), }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + (authenticator) => ({ + compositePK: primaryKey({ + columns: [authenticator.userId, authenticator.credentialID], + }), }), ); @@ -94,7 +107,7 @@ export const storedContent = createTable( "page", ), image: text("image", { length: 255 }), - user: text("user", { length: 255 }).references(() => users.id, { + userId: int("user").references(() => users.id, { onDelete: "cascade", }), }, @@ -102,7 +115,7 @@ export const storedContent = createTable( urlIdx: index("storedContent_url_idx").on(sc.url), savedAtIdx: index("storedContent_savedAt_idx").on(sc.savedAt), titleInx: index("storedContent_title_idx").on(sc.title), - userIdx: index("storedContent_user_idx").on(sc.user), + userIdx: index("storedContent_user_idx").on(sc.userId), }), ); |