"use server"; import { cookies, headers } from "next/headers"; import { db } from "@/server/db"; import { contentToSpace, sessions, storedContent, users, space, StoredContent, } from "@/server/db/schema"; import { SearchResult } from "@/contexts/MemoryContext"; import { like, eq, and, sql, exists, asc, notExists, inArray, notInArray } from "drizzle-orm"; import { union } from "drizzle-orm/sqlite-core"; import { env } from "@/env"; // @todo: (future) pagination not yet needed export async function searchMemoriesAndSpaces( query: string, opts?: { filter?: { memories?: boolean; spaces?: boolean }; range?: { offset: number; limit: number }; memoriesRelativeToSpace?: { fromSpaces?: number[]; notInSpaces?: number[]; } }, ): Promise { const user = await getUser(); if (!user) { return []; } const defaultWhere = and( eq(storedContent.user, user.id), like(storedContent.title, `%${query}%`), ) const extraWheres = [] if (opts?.memoriesRelativeToSpace) { if (opts.memoriesRelativeToSpace.fromSpaces) { extraWheres.push(exists(db.select().from(contentToSpace).where(and( eq(contentToSpace.contentId, storedContent.id), inArray(contentToSpace.spaceId, opts.memoriesRelativeToSpace.fromSpaces) )))) } if (opts.memoriesRelativeToSpace.notInSpaces) { extraWheres.push(notExists(db.select().from(contentToSpace).where(and( eq(contentToSpace.contentId, storedContent.id), inArray(contentToSpace.spaceId, opts.memoriesRelativeToSpace.notInSpaces) )))) } } try { let searchMemoriesQuery = db .select({ type: sql`'memory'`, space: sql`NULL`, memory: storedContent as any, }) .from(storedContent) .where( extraWheres.length == 2 ? and(and(...extraWheres), defaultWhere) : extraWheres.length == 1 ? and(...extraWheres, defaultWhere) : defaultWhere ) .orderBy(asc(storedContent.savedAt)); let searchSpacesQuery = db .select({ type: sql`'space'`, space: space as any, memory: sql`NULL`, }) .from(space) .where(and(eq(space.user, user.id), like(space.name, `%${query}%`))) .orderBy(asc(space.name)); let queries = []; console.log("adding"); [undefined, true].includes(opts?.filter?.memories) && queries.push(searchMemoriesQuery); [undefined, true].includes(opts?.filter?.spaces) && queries.push(searchSpacesQuery); if (opts?.range) { queries = queries.map((q) => q.offset(opts.range!.offset).limit(opts.range!.limit), ); } else { queries = queries.map((q) => q.all()); } const data = await Promise.all(queries); console.log("resp", data); return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[]; } catch { return []; } } export async function getMemoriesFromUrl(urls: string[]) { const user = await getUser(); if (!user) { return []; } return urls.length > 0 ? await db.select() .from(storedContent) .where(and( inArray(storedContent.url, urls), eq(storedContent.user, user.id) )).all() : [] } async function getUser() { 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 null; } const session = await db .select() .from(sessions) .where(eq(sessions.sessionToken, token!)); if (!session || session.length === 0) { return null; } const [userData] = await db .select() .from(users) .where(eq(users.id, session[0].userId)) .limit(1); if (!userData) { return null; } return userData; } export async function getSpace(id: number) { const user = await getUser(); if (!user) { return null; } return (await db.select() .from(space) .where(and( eq(space.id, id), eq(space.user, user.id) )))[0] } export async function addSpace(name: string, memories: number[]) { const user = await getUser(); if (!user) { return null; } const [addedSpace] = await db .insert(space) .values({ name: name, user: user.id, }) .returning(); const addedMemories = memories.length > 0 ? await db .insert(contentToSpace) .values( memories.map((m) => ({ contentId: m, spaceId: addedSpace.id, })), ) .returning() : []; return { space: addedSpace, addedMemories, }; } export async function fetchContent(id: number) { const user = await getUser(); if (!user) { return null; } const fetchedMemory = await db.select() .from(storedContent) .where(and( eq(storedContent.id, id), eq(storedContent.user, user.id) )); const memory = fetchedMemory.length > 0 ? fetchedMemory[0] : null const spaces = memory ? await db.select() .from(contentToSpace) .where( eq(contentToSpace.contentId, memory.id) ) : [] return { memory, spaces: spaces.map(s => s.spaceId) } } export async function fetchContentForSpace( spaceId: number, range?: { offset: number; limit: number; }, ) { const user = await getUser(); if (!user) { return null; } const query = db .select() .from(storedContent) .where( exists( db .select() .from(contentToSpace) .where( and( and( eq(contentToSpace.spaceId, spaceId), eq(contentToSpace.contentId, storedContent.id), ), exists( db.select() .from(space) .where(and( eq(space.user, user.id), eq(space.id, contentToSpace.spaceId) )) ) ) ), ), ) .orderBy(asc(storedContent.savedAt)); return range ? await query.limit(range.limit).offset(range.offset) : await query.all(); } export async function fetchFreeMemories(range?: { offset: number; limit: number; }) { const user = await getUser(); if (!user) { return []; } try { const query = db .select() .from(storedContent) .where( and( notExists( db .select() .from(contentToSpace) .where(eq(contentToSpace.contentId, storedContent.id)), ), eq(storedContent.user, user.id), ), ) .orderBy(asc(storedContent.savedAt)); return range ? await query.limit(range.limit).offset(range.offset) : await query.all(); } catch { return [] } } export async function updateSpaceTitle(id: number, title: string) { const user = await getUser(); if (!user) { return null; } return (await db.update(space).set({ name: title }).where( and( eq(space.id, id), eq(space.user, user.id) ) ).returning())[0]; } export async function addMemory( content: typeof storedContent.$inferInsert, spaces: number[], ) { const user = await getUser(); if (!user) { return null; } if (!content.content || content.content.trim() === "") { const resp = await fetch( `https://cf-ai-backend.dhravya.workers.dev/getPageContent?url=${content.url}`, { headers: { "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, }, ); const data = await resp.text(); console.log(data); content.content = data; } if (!content.content || content.content == "") { return null; } let [addedMemory] = await db .insert(storedContent) .values({ user: user.id, ...content, }) .returning(); const addedToSpaces = spaces.length > 0 ? await db .insert(contentToSpace) .values( spaces.map((s) => ({ contentId: addedMemory.id, spaceId: s, })), ) .returning() : []; if (content.type === 'note') { addedMemory = (await db.update(storedContent) .set({ url: addedMemory.url + addedMemory.id }) .where(eq(storedContent.id, addedMemory.id)) .returning())[0] } console.log("adding with:", `${addedMemory.url}-${user.email}`) // Add to vectorDB const res = (await Promise.race([ fetch("https://cf-ai-backend.dhravya.workers.dev/add", { method: "POST", headers: { "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, body: JSON.stringify({ pageContent: addedMemory.content, title: addedMemory.title, url: addedMemory.url, user: user.email, }), }), new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 40000), ), ])) as Response; return { memory: addedMemory, addedToSpaces, }; } export async function addContentInSpaces(id: number, contents: number[]) { const user = await getUser(); if (!user) { return null; } const data = contents.length > 0 ? await db.insert(contentToSpace).values(contents.map(i => ({ spaceId: id, contentId: i }))).returning() : [] return data } export async function updateMemory( id: number, { title, content, spaces, removedFromSpaces: removeSpaces }: { title?: string; content?: string; spaces?: number[]; removedFromSpaces?: number[]; } ) { const user = await getUser(); if (!user) { return null; } let updatedMemory: StoredContent | null = null; if (title && content) { const [prev] = await db.select() .from(storedContent) .where(and( eq(storedContent.user, user.id), eq(storedContent.id, id) )); if (!prev) { return null } const newContent = { ...(title ? { title }: {}), ...(content ? { content }: {}), } const updated = { ...newContent, ...prev } console.log("adding with:", `${updated.url}-${user.email}`) // Add to vectorDB const res = (await Promise.race([ fetch("https://cf-ai-backend.dhravya.workers.dev/edit", { method: "POST", headers: { "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, body: JSON.stringify({ pageContent: updated.content, title: updated.title, url: updated.url, user: user.email, uniqueUrl: updated.url, }), }), new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 40000), ), ])) as Response; [updatedMemory] = await db .update(storedContent) .set(newContent) .where(and( eq(storedContent.id, id), eq(storedContent.user, user.id) )) .returning(); console.log(updatedMemory, newContent) } if (!updatedMemory) { [updatedMemory] = await db .select() .from(storedContent) .where(and( eq(storedContent.id, id), eq(storedContent.user, user.id) )) } const removedFromSpaces = removeSpaces ? removeSpaces.length > 0 ? await db.delete(contentToSpace) .where(and( inArray(contentToSpace.spaceId, removeSpaces), eq(contentToSpace.contentId, id) )).returning() : [] : spaces ? spaces.length > 0 ? await db.delete(contentToSpace) .where(and( notInArray(contentToSpace.spaceId, spaces), eq(contentToSpace.contentId, id) )).returning() : await db.delete(contentToSpace) .where( eq(contentToSpace.contentId, id) ) : []; const addedToSpaces = (spaces && spaces.length > 0) ? await db .insert(contentToSpace) .values( spaces.map((s) => ({ contentId: id, spaceId: s, })), ) .onConflictDoNothing() .returning() : []; const resultedSpaces = (await db.select() .from(contentToSpace) .where(eq(contentToSpace.contentId, id)) .all()).map(i => i.spaceId) ?? []; return { memory: updatedMemory, addedToSpaces, removedFromSpaces, resultedSpaces }; } export async function deleteSpace(id: number) { const user = await getUser(); if (!user) { return null; } await db.delete(contentToSpace).where(eq(contentToSpace.spaceId, id)); const [deleted] = await db .delete(space) .where(and(eq(space.user, user.id), eq(space.id, id))) .returning(); return deleted; } export async function deleteMemory(id: number) { const user = await getUser(); if (!user) { return null; } await db.delete(contentToSpace).where(eq(contentToSpace.contentId, id)); const [deleted] = await db .delete(storedContent) .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))) .returning(); if (deleted) { console.log("adding with:", `${deleted.url}-${user.email}`) const res = (await Promise.race([ fetch(`https://cf-ai-backend.dhravya.workers.dev/delete` , { method: "DELETE", headers: { "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, }, body: JSON.stringify({ websiteUrl: deleted.url, user: user.email }) }), new Promise((_, reject) => setTimeout(() => reject(new Error("Request timed out")), 40000), ), ])) as Response; } return deleted; }