diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index cdf03b4..bea8eca 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -152,9 +152,16 @@ async function attachChatCreatorLabels( } // GET /projects +// Pass ?include=documents to also receive each project's documents in the +// same response. The directory pickers (useDirectoryData) previously fanned +// out one GET /projects/:id per project to obtain those documents; with N +// projects that burst — auth check plus several DB queries per request — +// could overwhelm the Supabase gateway. Batching keeps it at one request +// and a fixed number of queries regardless of project count. projectsRouter.get("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; const userEmail = res.locals.userEmail as string | undefined; + const includeDocuments = req.query.include === "documents"; const db = createServerSupabase(); const { data, error } = await db.rpc("get_projects_overview", { @@ -163,7 +170,45 @@ projectsRouter.get("/", requireAuth, async (req, res) => { }); if (error) return void res.status(500).json({ detail: error.message }); - res.json(data ?? []); + const projects = (data ?? []) as { id: string }[]; + if (!includeDocuments || projects.length === 0) { + return void res.json(projects); + } + + const { data: docs, error: docsError } = await db + .from("documents") + .select("*") + .in( + "project_id", + projects.map((p) => p.id), + ) + .order("created_at", { ascending: true }); + if (docsError) + return void res.status(500).json({ detail: docsError.message }); + + const docsTyped = (docs ?? []) as unknown as { + id: string; + project_id?: string | null; + user_id?: string | null; + current_version_id?: string | null; + }[]; + await attachLatestVersionNumbers(db, docsTyped); + await attachActiveVersionPaths(db, docsTyped); + await attachDocumentOwnerLabels(db, docsTyped); + + const docsByProject = new Map(); + for (const doc of docsTyped) { + if (!doc.project_id) continue; + const bucket = docsByProject.get(doc.project_id); + if (bucket) bucket.push(doc); + else docsByProject.set(doc.project_id, [doc]); + } + res.json( + projects.map((p) => ({ + ...p, + documents: docsByProject.get(p.id) ?? [], + })), + ); }); // POST /projects diff --git a/frontend/src/app/components/shared/useDirectoryData.ts b/frontend/src/app/components/shared/useDirectoryData.ts index 87e23b9..7d4a500 100644 --- a/frontend/src/app/components/shared/useDirectoryData.ts +++ b/frontend/src/app/components/shared/useDirectoryData.ts @@ -1,11 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { - getLibrary, - getProject, - listProjects, -} from "@/app/lib/mikeApi"; +import { getLibrary, listProjects } from "@/app/lib/mikeApi"; import type { Document, LibraryFolder, Project } from "./types"; export type DirectoryTab = "files" | "templates" | "projects"; @@ -45,17 +41,14 @@ async function loadTemplates() { } async function loadProjects() { - const projects = await listProjects(); - const fullProjects = await Promise.all( - projects.map((project) => getProject(project.id)), - ); - const projectCounts = new Map( - projects.map((project) => [project.id, project.document_count ?? 0]), - ); - return fullProjects.map((project) => ({ + // One batched request. Fanning out getProject(id) per project caused an + // N+1 burst on every directory-modal open that could overwhelm the + // Supabase gateway once an account had accumulated projects. + const projects = await listProjects({ includeDocuments: true }); + return projects.map((project) => ({ ...project, document_count: - project.documents?.length ?? projectCounts.get(project.id) ?? 0, + project.documents?.length ?? project.document_count ?? 0, })); } diff --git a/frontend/src/app/lib/mikeApi.ts b/frontend/src/app/lib/mikeApi.ts index ad3959c..6dbaa1e 100644 --- a/frontend/src/app/lib/mikeApi.ts +++ b/frontend/src/app/lib/mikeApi.ts @@ -163,8 +163,11 @@ async function toApiError(response: Response, path: string) { // Projects // --------------------------------------------------------------------------- -export async function listProjects(): Promise { - return apiRequest("/projects"); +export async function listProjects(options?: { + includeDocuments?: boolean; +}): Promise { + const query = options?.includeDocuments ? "?include=documents" : ""; + return apiRequest(`/projects${query}`); } export async function createProject(