diff --git a/docs/Ai-converstion.md b/docs/Ai-converstion.md index a335b20..d1b60c1 100644 --- a/docs/Ai-converstion.md +++ b/docs/Ai-converstion.md @@ -2178,3 +2178,4197 @@ Generate the code for src/data-access/file-nodes.ts based on your existing queri Generate the code for src/services/onedrive.ts to centralize the token handling? +Can you create the file-node.ts with the following functions for getAllFileNodes, getFileNodeById, updateFileNode, and deleteFileNode + +To begin Phase 2 of your refactor, we will create the Data Access Layer (DAL) for your file nodes. This file centralizes all interaction with your PostgreSQL database via Prisma. + +As part of the Data Access Layer (DAL) pattern, we use the server-only package to ensure these functions—which contain direct database queries—can never be called from the client-side, keeping your database credentials and logic secure. + +src/data-access/file-nodes.ts +TypeScript +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Retrieve all nodes for the dashboard. + * Centralizing this here allows us to change sort order or filters + * in one place for the entire application. + */ +export async function getAllFileNodes() { + return await prisma.fileNode.findMany({ + orderBy: { + updatedAt: 'desc', + }, + }); +} + +/** + * FETCH: Get a single node by ID. + * Used by the Download route and Update pages to verify a file exists. + */ +export async function getFileNodeById(id: string) { + return await prisma.fileNode.findUnique({ + where: { id }, + }); +} + +/** + * UPDATE: Modify metadata, name, or virtual location. + * This function accepts the data object to keep the DAL flexible. + */ +export async function updateFileNode(id: string, data: any) { + return await prisma.fileNode.update({ + where: { id }, + data: { + ...data, + updatedAt: new Date(), + }, + }); +} + +/** + * DELETE: Remove the record from the database. + * Cloud deletion should be handled by the Service Layer before calling this. + */ +export async function deleteFileNode(id: string) { + return await prisma.fileNode.delete({ + where: { id }, + }); +} +Integration Strategy +By moving these functions into the DAL, we solve several issues highlighted in your project summary: + +Logic Isolation: Your Server Actions in src/app/dashboard/actions.ts no longer need to know about Prisma's internal syntax (like where or orderBy). + +Consistency: The getAllFileNodes function ensures that every part of your app sees the same "latest version" of the file list. + +BigInt Handling: Since your size field is a BigInt (as seen in your actions.ts), having a central DAL allows us to safely cast or format these values before they hit the UI. + +Next Step +Now that the database layer is ready, the next step in our Refactor Action Plan is to create the Service Layer (src/services/onedrive.ts). This will centralize your getFreshAccessToken calls so you can stop duplicating that logic in every action. + +Would you like me to generate the src/services/onedrive.ts file next? + +I am doing the Audit now + +New function + +export async function getAllFileNodes() { + +return await prisma.fileNode.findMany({ + +orderBy: { + +updatedAt: 'desc', + +}, + +}); + +} + +Old function + +export async function getFileNodes() { + +try { + +const nodes = await prisma.fileNode.findMany({ + +orderBy: { + +updatedAt: 'desc', + +}, + +}); + +return nodes; + +} catch (error) { + +console.error("Error fetching file nodes:", error); + +return []; + +} + +} + +should we not have a try catch in the async function + +That is an excellent observation during your audit. The question of where to handle errors (in the DAL vs. the Server Action) is a key part of the Data Access Layer (DAL) strategy. + +The short answer is: No, typically you should not have a try/catch in the DAL function. + +Here is the reasoning behind this strategy: + +1. Let the Caller Decide +The DAL's only job is to communicate with the database. If the database is down or the query fails, the DAL should throw that error up to the "Orchestrator" (your Server Action or API Route). + +The Server Action knows how to talk to the UI (e.g., returning an error message to a toast notification). + +The API Route knows how to talk to the browser (e.g., returning a 500 Internal Server Error status code). + +If the DAL catches the error and returns an empty array [], the UI might think the folder is just empty, which is misleading. + +2. Avoiding "Silent Failures" +In your old function, if the database crashed, it returned []. This is a silent failure. By removing the try/catch from the DAL, you ensure that if something goes wrong, the application developer is forced to handle it intentionally at the higher level. + +3. Cleaner Code +Your DAL stays "pure" and focused on the data structure. It makes the code much more scannable and easier to maintain. + +Comparison: How it looks in practice +The New DAL (src/data-access/file-nodes.ts): + +TypeScript +export async function getAllFileNodes() { + // We just return the promise. If it fails, it throws. + return await prisma.fileNode.findMany({ + orderBy: { updatedAt: 'desc' }, + }); +} +The New Server Action (src/app/dashboard/actions.ts): + +TypeScript +export async function getFileNodesAction() { + try { + const nodes = await getAllFileNodes(); // Call the DAL + return { success: true, data: nodes }; + } catch (error) { + console.error("Dashboard Fetch Error:", error); + return { success: false, error: "Could not load library." }; + } +} +Audit Recommendation +As you continue your audit, follow these "DAL Rules": + +Throw, Don't Catch: Remove try/catch from the data-access files. + +Return Data, Not Responses: Don't return NextResponse or alert messages from the DAL. Just return the Prisma result. + +Specific Queries: If you find yourself needing to filter files by a specific user often, create a specific DAL function like getFileNodesByUser(userId: string). + +Since you are auditing the actions.ts file, would you like me to prepare the code for the Service Layer (src/services/onedrive.ts) so you can see how the external API calls will be extracted next? + +can you create onedrive.ts file + +To complete Phase 3 of your refactor, we will create the Service Layer for OneDrive. This file will be the "single source of truth" for all interactions with the Microsoft Graph API. + +The primary goal of this file is to solve the token duplication issue by automatically handling the getFreshAccessToken logic before making any external calls. + +src/services/onedrive.ts +TypeScript +import "server-only"; +import { getFreshAccessToken } from "@/lib/auth-utils"; + +/** + * PRIVATE HELPER: graphRequest + * This internal function handles the heavy lifting of fetching tokens + * and making the actual HTTP call to Microsoft. + */ +async function graphRequest(userId: string, endpoint: string, options: RequestInit = {}) { + // 1. Automatically handle token refresh logic + const token = await getFreshAccessToken(userId); + const baseUrl = "https://graph.microsoft.com/v1.0"; + + const res = await fetch(`${baseUrl}${endpoint}`, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${token}`, + }, + }); + + // 2. Centralized Error Handling for OneDrive + if (!res.ok) { + const errorData = await res.text(); + console.error(`OneDrive API Error [${endpoint}]:`, errorData); + throw new Error(`OneDrive API failed: ${res.statusText}`); + } + + return res; +} + +/** + * SERVICE: Download File Content + * Returns the raw binary stream from OneDrive. + */ +export async function getOneDriveContentStream(userId: string, oneDriveId: string) { + return await graphRequest(userId, `/me/drive/items/${oneDriveId}/content`); +} + +/** + * SERVICE: Get File Metadata + * Used to get the @microsoft.graph.downloadUrl or driveItem properties. + */ +export async function getOneDriveItem(userId: string, oneDriveId: string) { + const res = await graphRequest(userId, `/me/drive/items/${oneDriveId}`); + return res.json(); +} + +/** + * SERVICE: Upload File + * Handles the PUT request to OneDrive for new or updated files. + */ +export async function uploadToOneDrive(userId: string, file: File, oneDriveId?: string) { + // If oneDriveId exists, we update. Otherwise, we'd use a path (needs expansion for new files). + const endpoint = oneDriveId + ? `/me/drive/items/${oneDriveId}/content` + : `/me/drive/root:/${file.name}:/content`; + + return await graphRequest(userId, endpoint, { + method: "PUT", + headers: { "Content-Type": file.type }, + body: Buffer.from(await file.arrayBuffer()), + }); +} + +/** + * SERVICE: Delete from Cloud + */ +export async function deleteFromOneDrive(userId: string, oneDriveId: string) { + return await graphRequest(userId, `/me/drive/items/${oneDriveId}`, { + method: "DELETE", + }); +} +Key Improvements in this Design +Zero Token Management in Actions: Your Server Actions in actions.ts will no longer need to import getFreshAccessToken. They just pass the userId to these service functions. + +Standardized API Base: If Microsoft ever updates their API version (e.g., to v2.0), you only change the baseUrl string in this one file. + +Automatic Buffer Handling: The uploadToOneDrive function handles the complex task of converting a browser File object into a Buffer that the Microsoft Graph API accepts. + +Next Steps in the Refactor Plan +With file-nodes.ts (DAL) and onedrive.ts (Service) both created on your new branch, you are now ready for Phase 4: The Clean Up. + +You can now go into src/app/dashboard/actions.ts and replace blocks of 20+ lines of code with simple 1-line calls like: const cloudRes = await deleteFromOneDrive(userId, fileNode.oneDriveId); + +Would you like me to help you rewrite one of the specific functions in your actions.ts (like deleteFileNodeAction) using these new layers? + +This Microsoft Graph and Next.js Authentication Guide provides additional context on handling session tokens and API requests within a similar architecture. This video is relevant because it demonstrates how to integrate Microsoft services with modern Next.js patterns, complementing the service layer refactor we are implementing. + +can you generate actions.ts I assume src/app/dashboard/actions.ts + +Here is the refactored src/app/dashboard/actions.ts. By leveraging the Data Access Layer (DAL) and the Service Layer we created, the code is now much cleaner, significantly shorter, and free of duplicated token-refresh logic. + +src/app/dashboard/actions.ts +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { + getAllFileNodes, + getFileNodeById, + updateFileNode, + deleteFileNode +} from "@/data-access/file-nodes"; +import { + getOneDriveItem, + deleteFromOneDrive, + uploadToOneDrive +} from "@/services/onedrive"; + +/** + * 1. FETCH: Get all file nodes + * Now simply calls the DAL. Error handling is left to the caller (the UI). + */ +export async function getFileNodes() { + return await getAllFileNodes(); +} + +/** + * 2. DOWNLOAD: Generates the authenticated OneDrive URL + * Orchestrates the session check, DAL lookup, and Service call. + */ +export async function getDownloadUrlAction(id: string) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const file = await getFileNodeById(id); + if (!file || !file.oneDriveId) throw new Error("File not found or missing cloud ID"); + + // Service handles token refresh and graph request internally + const data = await getOneDriveItem(session.user.id, file.oneDriveId); + const downloadUrl = data["@microsoft.graph.downloadUrl"]; + + if (!downloadUrl) throw new Error("OneDrive did not provide a download URL"); + return downloadUrl; +} + +/** + * 3. DELETE: Removes from both Cloud and Database + */ +export async function deleteFileNodeAction(id: string) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + const file = await getFileNodeById(id); + if (!file) throw new Error("File record not found"); + + // Phase 1: Cloud Deletion + if (file.oneDriveId) { + await deleteFromOneDrive(session.user.id, file.oneDriveId); + } + + // Phase 2: Database Deletion + await deleteFileNode(id); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error) { + console.error("Delete Error:", error); + return { success: false, error: "Failed to delete file" }; + } +} + +/** + * 4. UPDATE: Modify record and optionally sync new content to OneDrive + */ +export async function updateFileNodeAction(id: string, formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const parentIdRaw = formData.get("parentId") as string; + const metadataStr = formData.get("metadata") as string; + const newFile = formData.get("file") as File | null; + + const parentId = parentIdRaw === "root" ? null : parentIdRaw; + let metadata = JSON.parse(metadataStr); + + try { + const node = await getFileNodeById(id); + + // If a new file is uploaded, push it to OneDrive first + if (newFile && newFile.size > 0 && node?.oneDriveId) { + await uploadToOneDrive(session.user.id, newFile, node.oneDriveId); + + metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; + metadata.mimeType = newFile.type; + } + + // Update the database via DAL + await updateFileNode(id, { + name, + description, + parentId, + metadata, + size: newFile ? BigInt(newFile.size) : undefined, + }); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error) { + console.error("Update Error:", error); + return { success: false, error: "Failed to update record" }; + } +} +What Changed? +Removed prisma imports: All database communication is now handled by the DAL functions. + +Removed getFreshAccessToken: This is now encapsulated within the onedrive.ts service. + +Reduced Boilerplate: The updateFileNodeAction and deleteFileNodeAction are much easier to read because the "how" (fetch headers, token logic) is hidden, leaving only the "what" (the business logic). + +Next Steps: You should now perform a similar refactor for sync-actions.ts and upload-actions.ts to ensure your entire dashboard follows this new, clean architecture. + +Mastering Next.js Server Actions and Mutations + +This video provides a deep dive into using Server Actions for data mutations and how they integrate with Next.js caching, which is essential for understanding the revalidation logic used in your refactored actions. + +we also have src/app/dashboard/sync-action.ts + +where do we allocate it's functionality ? + +'use server'; + +import { auth } from "@/auth"; + +import { prisma } from "@/lib/prisma"; + +import { revalidatePath } from "next/cache"; + +import { getFreshAccessToken } from "@/lib/auth-utils"; + + + +export async function syncOneDrive() { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +try { + +const accessToken = await getFreshAccessToken(session.user.id); + +const response = await fetch("https://graph.microsoft.com/v1.0/me/drive/root:/WebCalibre:/children", { + +headers: { Authorization: `Bearer ${accessToken}` }, + +}); + + +if (!response.ok) return { success: true, count: 0 }; + + + +const data = await response.json(); + +let syncedCount = 0; + +const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + + +for (const item of data.value) { + +const isFolder = !!item.folder; + + + +// Only skip if it's a folder AND it's a UUID (storage container) + +// If a user named a file with a UUID, we still want it. + +if (isFolder && uuidRegex.test(item.name)) { + +continue; + +} + + + +const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); + + + +await prisma.fileNode.upsert({ + +where: { oneDriveId: item.id }, // Primary match + +update: { + +name: item.name, + +size: BigInt(item.size || 0), + +isFolder: isFolder, + +path: item.parentReference?.path + '/' + item.name, + +updatedAt: new Date(), + +}, + +create: { + +id: crypto.randomUUID(), + +oneDriveId: item.id, + +name: item.name, + +size: BigInt(item.size || 0), + +isFolder: isFolder, + +path: item.parentReference?.path + '/' + item.name, + +ownerId: session.user.id, + +metadata: { type: extension, mimeType: item.file?.mimeType || null }, + +} + +}); + +syncedCount++; + +} + + +revalidatePath('/dashboard'); + +return { success: true, count: syncedCount }; + +} catch (error: any) { + +throw new Error(error.message); + +} + +} + +In a proper Data Access Layer (DAL) and Service Layer architecture, functionality is split by responsibility, not by file name. + +For your syncOneDrive logic, the functionality should be allocated as follows: + +Service Layer (src/services/onedrive.ts): Handles the Microsoft Graph API call to list the children of the WebCalibre folder. + +Data Access Layer (src/data-access/file-nodes.ts): Handles the upsert logic (the "Database work"). + +Server Action (src/app/dashboard/sync-actions.ts): Orchestrates the two. It calls the Service to get the data, loops through it, calls the DAL to save it, and revalidates the UI. + +Here is how to refactor each piece: + +1. Update the Service Layer (src/services/onedrive.ts) +Add a function to fetch the children of your specific library folder. + +TypeScript +/** + * SERVICE: List Children of the WebCalibre folder + */ +export async function getWebCalibreChildren(userId: string) { + const res = await graphRequest(userId, "/me/drive/root:/WebCalibre:/children"); + const data = await res.json(); + return data.value; // Returns the array of driveItems +} +2. Update the Data Access Layer (src/data-access/file-nodes.ts) +Add a dedicated upsert function so your database logic remains centralized. + +TypeScript +/** + * UPSERT: Create or Update a file node based on OneDrive ID + */ +export async function upsertFileNode(oneDriveId: string, data: any) { + return await prisma.fileNode.upsert({ + where: { oneDriveId }, + update: { + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + updatedAt: new Date(), + }, + create: { + id: crypto.randomUUID(), + oneDriveId: oneDriveId, + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + ownerId: data.ownerId, + metadata: data.metadata, + } + }); +} +3. Refactor the Server Action (src/app/dashboard/sync-actions.ts) +The Action is now much cleaner. It focuses on the Business Logic (the UUID regex filter) rather than the low-level API details. + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { upsertFileNode } from "@/data-access/file-nodes"; +import { getWebCalibreChildren } from "@/services/onedrive"; + +export async function syncOneDrive() { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + // 1. Call Service to get cloud data (token refresh handled inside service) + const items = await getWebCalibreChildren(session.user.id); + + let syncedCount = 0; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + for (const item of items) { + const isFolder = !!item.folder; + + // Business Logic: Skip UUID storage folders + if (isFolder && uuidRegex.test(item.name)) continue; + + const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); + + // 2. Call DAL to save to database + await upsertFileNode(item.id, { + name: item.name, + size: BigInt(item.size || 0), + isFolder: isFolder, + path: item.parentReference?.path + '/' + item.name, + ownerId: session.user.id, + metadata: { type: extension, mimeType: item.file?.mimeType || null }, + }); + + syncedCount++; + } + + revalidatePath('/dashboard'); + return { success: true, count: syncedCount }; + } catch (error: any) { + console.error("Sync Error:", error.message); + throw new Error("Failed to sync with OneDrive"); + } +} +Why this is better: +Decoupling: If you ever switch from OneDrive to Google Drive, you only change the Service Layer. The syncOneDrive action and the upsertFileNode DAL remain exactly the same. + +Error Handling: If the OneDrive API is down, the Service Layer throws an error, the Action catches it and logs it, and the user gets a clean error message instead of a crash. + +Type Safety: All your BigInt conversions and crypto.randomUUID() calls are now tucked away in the layers where they belong. + +Does this structure make sense for the rest of your upload-actions.ts and other sync logic? + +we also have src/app/dashboard/upload-actions.ts + +where do we allocate it's functionality + +'use server'; + + + +import { auth } from "@/auth"; + +import { getFreshAccessToken } from "@/lib/auth-utils"; + +import { prisma } from "@/lib/prisma"; + +import { revalidatePath } from "next/cache"; + + + +export async function uploadFileAction(formData: FormData) { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +const file = formData.get("file") as File; + +const folderName = "WebCalibre"; + +const accessToken = await getFreshAccessToken(session.user.id); + + + +// 1. Create/Check WebCalibre Folder + +const folderPath = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}`; + +const folderCheck = await fetch(folderPath, { + +headers: { Authorization: `Bearer ${accessToken}` } + +}); + + + +if (folderCheck.status === 404) { + +await fetch(`https://graph.microsoft.com/v1.0/me/drive/root/children`, { + +method: "POST", + +headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, + +body: JSON.stringify({ name: folderName, folder: {} }) + +}); + +} + + + +// 2. Create Upload Session (Supports files > 4MB) + +const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}/${file.name}:/createUploadSession`; + +const sessionRes = await fetch(sessionUrl, { + +method: "POST", + +headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, + +body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } }) + +}); + + + +const { uploadUrl } = await sessionRes.json(); + + + +// 3. Upload File Data + +const buffer = Buffer.from(await file.arrayBuffer()); + +const uploadRes = await fetch(uploadUrl, { + +method: "PUT", + +headers: { + +"Content-Length": `${file.size}`, + +"Content-Range": `bytes 0-${file.size - 1}/${file.size}` + +}, + +body: buffer + +}); + + + +const driveItem = await uploadRes.json(); + + + +// 4. Record in PostgreSQL + +await prisma.fileNode.create({ + +data: { + +oneDriveId: driveItem.id, + +name: file.name, + +size: BigInt(file.size), + +isFolder: false, + +path: `/${folderName}/${file.name}`, + +ownerId: session.user.id, + +metadata: { type: file.name.split('.').pop()?.toUpperCase() } + +} + +}); + + + +revalidatePath("/dashboard"); + +return { success: true }; + +} + +For the uploadFileAction, we are dealing with a more complex multi-step process: checking/creating a folder, creating an upload session (for large files), and finally saving to the database. + +Following the DAL + Service Layer pattern, here is where we allocate this functionality: + +1. Update the Service Layer (src/services/onedrive.ts) +We move the heavy lifting of the Microsoft Graph logic here. Notice how the Service Layer now handles the "logic" of ensuring the folder exists and managing the upload session. + +TypeScript +/** + * SERVICE: Ensure a specific folder exists in OneDrive + * Returns the folder ID + */ +export async function ensureOneDriveFolder(userId: string, folderName: string) { + try { + const res = await graphRequest(userId, `/me/drive/root:/${folderName}`); + const data = await res.json(); + return data.id; + } catch (error) { + // If 404, create it + const createRes = await graphRequest(userId, `/me/drive/root/children`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: folderName, folder: {} }) + }); + const data = await createRes.json(); + return data.id; + } +} + +/** + * SERVICE: Upload Large File via Session + * This replaces the basic PUT for better reliability + */ +export async function uploadLargeFile(userId: string, file: File, folderName: string) { + // 1. Create Upload Session + const sessionRes = await graphRequest(userId, `/me/drive/root:/${folderName}/${file.name}:/createUploadSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } }) + }); + const { uploadUrl } = await sessionRes.json(); + + // 2. Upload the data to the provided URL (No Authorization header needed for the uploadUrl itself) + const buffer = Buffer.from(await file.arrayBuffer()); + const uploadRes = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": `${file.size}`, + "Content-Range": `bytes 0-${file.size - 1}/${file.size}` + }, + body: buffer + }); + + if (!uploadRes.ok) throw new Error("Upload session failed"); + return await uploadRes.json(); // Returns the DriveItem +} +2. Update the Data Access Layer (src/data-access/file-nodes.ts) +We add a clean function to create a new record. + +TypeScript +/** + * CREATE: Add a new file node record + */ +export async function createFileNode(data: { + oneDriveId: string; + name: string; + size: bigint; + isFolder: boolean; + path: string; + ownerId: string; + metadata: any; +}) { + return await prisma.fileNode.create({ data }); +} +3. Refactor the Server Action (src/app/dashboard/upload-actions.ts) +Now, your Server Action becomes a high-level orchestrator. It describes the "Story" of the upload without getting bogged down in HTTP headers or URL strings. + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { createFileNode } from "@/data-access/file-nodes"; +import { ensureOneDriveFolder, uploadLargeFile } from "@/services/onedrive"; + +export async function uploadFileAction(formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const file = formData.get("file") as File; + const folderName = "WebCalibre"; + + try { + // 1. Logic: Ensure destination exists + await ensureOneDriveFolder(session.user.id, folderName); + + // 2. Logic: Perform the cloud upload + const driveItem = await uploadLargeFile(session.user.id, file, folderName); + + // 3. Logic: Save the result to our DB + await createFileNode({ + oneDriveId: driveItem.id, + name: file.name, + size: BigInt(file.size), + isFolder: false, + path: `/${folderName}/${file.name}`, + ownerId: session.user.id, + metadata: { + type: file.name.split('.').pop()?.toUpperCase(), + mimeType: file.type + } + }); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error: any) { + console.error("Upload Action Error:", error); + return { success: false, error: error.message || "Upload failed" }; + } +} +Summary of Benefits for the Refactor: +Large File Support: The uploadLargeFile service now handles files bigger than 4MB properly using Microsoft's recommended uploadSession pattern. + +Separation of Concerns: If you ever want to change the folder name or the metadata structure, you know exactly which layer to touch. + +Readability: Your Server Action is now only ~20 lines long, compared to the ~50 lines it was previously. + +You have now successfully mapped out the refactor for all major dashboard actions! Do you want to review the src/app/api/download/route.ts refactor to finish the set? + +please create the updated src/app/api/download/route.ts that should finish of dashboard + +To finish the dashboard refactor, we will update the Route Handler to use our new Service Layer and Data Access Layer. + +By moving the logic into these layers, the route.ts file becomes much more resilient. It no longer needs to worry about how to refresh a Microsoft token or how to structure a Prisma query; it simply handles the HTTP request and pipes the response. + +src/app/api/download/route.ts +TypeScript +import { NextRequest, NextResponse } from 'next/server'; +import { auth } from "@/auth"; +import { getFileNodeById } from "@/data-access/file-nodes"; +import { getOneDriveContentStream } from "@/services/onedrive"; + +export async function GET(request: NextRequest) { + try { + // 1. Authenticate the user session + const session = await auth(); + if (!session?.user?.id) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + // 2. Extract parameters from URL + const { searchParams } = new URL(request.url); + const fileId = searchParams.get('id'); + const mode = searchParams.get('mode') === 'inline' ? 'inline' : 'attachment'; + + if (!fileId) { + return new NextResponse("File ID is required", { status: 400 }); + } + + // 3. DAL: Fetch file metadata from local database + const fileNode = await getFileNodeById(fileId); + + if (!fileNode || !fileNode.oneDriveId) { + return new NextResponse("File not found", { status: 404 }); + } + + // 4. SERVICE: Get the binary stream from Microsoft Graph + // The service layer automatically handles the 'getFreshAccessToken' logic + const graphResponse = await getOneDriveContentStream(session.user.id, fileNode.oneDriveId); + + // 5. Stream the response directly to the client + // We pass the graphResponse.body (ReadableStream) directly to NextResponse + return new NextResponse(graphResponse.body, { + status: 200, + headers: { + 'Content-Type': fileNode.mimeType || 'application/octet-stream', + // encodeURIComponent handles special characters in filenames + 'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`, + 'Cache-Control': 'no-store, max-age=0', + }, + }); + + } catch (error: any) { + console.error('Download Route Error:', error); + return new NextResponse( + JSON.stringify({ error: "Internal Server Error", message: error.message }), + { status: 500 } + ); + } +} +Why this completes the Dashboard Refactor: +Uniformity: Every file in your src/app/dashboard folder and your API routes now follows the same "Three-Tier" architecture (UI -> Service/Action -> DAL). + +Memory Efficiency: By piping graphResponse.body directly into the NextResponse, your Ubuntu server doesn't have to load the entire file into RAM. It flows through the server like water through a pipe, which is critical for larger PDF or media files. + +Clean Error Handling: If the token refresh fails or OneDrive is down, the onedrive.ts service will throw an error, which this route catches and reports as a 500 error instead of crashing the process. + +Final Checklist before Testing +Now that you have refactored the code on your feature/dal-refactor branch: + +Restart the Dev Server: npm run dev (to ensure all new imports are indexed). + +Fresh Login: Log out and log back in to ensure your session has a fresh token. + +Test Download: Click the "Download" button on a file in your dashboard. + +Check Logs: Watch your terminal for any Prisma or Graph API errors. + +Since the backend is now fully refactored, would you like me to help you merge this branch back into your main line, or should we look at the Update Page actions next? + +I will try but don't forget we need to refactor settings,update[id] and upload + +You are absolutely right. To maintain the integrity of the DAL + Service Layer architecture, we cannot leave "islands" of old code in your other features. If settings, update, and upload still use direct Prisma calls and manual token fetching, you will end up with two different ways of doing the same thing, which leads to bugs. + +Let's complete the refactor by hitting these three areas. + +1. The Update Feature: src/app/update/[id]/_actions.ts +Your update logic currently duplicates a lot of what we just refactored. We will simplify it by calling the functions already present in your new file-nodes.ts and onedrive.ts. + +Updated src/app/update/[id]/_actions.ts + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes"; +import { uploadToOneDrive } from "@/services/onedrive"; + +export async function updateFileNodeAction(id: string, formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const parentIdRaw = formData.get("parentId") as string; + const metadataStr = formData.get("metadata") as string; + const newFile = formData.get("file") as File | null; + + const parentId = parentIdRaw === "root" ? null : parentIdRaw; + let metadata = metadataStr ? JSON.parse(metadataStr) : {}; + + try { + const node = await getFileNodeById(id); + if (!node) throw new Error("Record not found"); + + // If a replacement file is provided, update OneDrive + if (newFile && newFile.size > 0 && node.oneDriveId) { + await uploadToOneDrive(session.user.id, newFile, node.oneDriveId); + metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; + metadata.mimeType = newFile.type; + } + + // Update DB via DAL + await updateFileNode(id, { + name, + description, + parentId, + metadata, + size: (newFile && newFile.size > 0) ? BigInt(newFile.size) : undefined, + }); + + revalidatePath("/dashboard"); + revalidatePath(`/update/${id}`); + return { success: true }; + } catch (error: any) { + console.error("Update Action Error:", error); + return { success: false, error: error.message || "Failed to update" }; + } +} +2. The Settings Feature: src/app/settings/actions.ts +Settings usually involves managing Users. We need to add a User DAL to keep the pattern consistent. + +Step A: Create src/data-access/users.ts + +TypeScript +import "server-only"; +import { prisma } from "@/lib/prisma"; + +export async function getAllUsers() { + return await prisma.user.findMany({ + orderBy: { name: 'asc' } + }); +} + +export async function updateUserRole(id: string, role: string) { + return await prisma.user.update({ + where: { id }, + data: { role } + }); +} +Step B: Updated src/app/settings/actions.ts + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { getAllUsers, updateUserRole } from "@/data-access/users"; + +export async function changeUserRoleAction(userId: string, newRole: string) { + const session = await auth(); + // Authorization check: Only ADMINs can change roles + if (session?.user?.role !== "ADMIN") { + throw new Error("Forbidden: Admin access required"); + } + + try { + await updateUserRole(userId, newRole); + revalidatePath("/settings"); + return { success: true }; + } catch (error) { + return { success: false, error: "Failed to update user role" }; + } +} +3. The Upload Feature: src/app/upload/page.tsx & _actions.ts +Since we already built ensureOneDriveFolder and uploadLargeFile in our src/services/onedrive.ts, the upload actions become extremely slim. + +Updated src/app/upload/_actions.ts + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { createFileNode } from "@/data-access/file-nodes"; +import { ensureOneDriveFolder, uploadLargeFile } from "@/services/onedrive"; + +export async function standaloneUploadAction(formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const file = formData.get("file") as File; + + try { + await ensureOneDriveFolder(session.user.id, "WebCalibre"); + const driveItem = await uploadLargeFile(session.user.id, file, "WebCalibre"); + + await createFileNode({ + oneDriveId: driveItem.id, + name: file.name, + size: BigInt(file.size), + isFolder: false, + path: `/WebCalibre/${file.name}`, + ownerId: session.user.id, + metadata: { + type: file.name.split('.').pop()?.toUpperCase(), + mimeType: file.type + } + }); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } +} +Refactor Summary +Module Old State New State +Update Manual fetch/Prisma calls Uses getFileNodeById and uploadToOneDrive +Settings Inline prisma.user calls Uses src/data-access/users.ts +Upload 50+ lines of fetch headers Orchestrates ensureFolder -> uploadLargeFile +This architecture ensures that if you ever need to change your database (e.g., from Postgres to MongoDB) or your cloud provider (e.g., from OneDrive to AWS S3), you only change the code in the src/data-access or src/services folders. The rest of your app stays exactly the same. + +You've now covered all the primary functional areas. Should we look at merging this into your main branch, or would you like to verify the TypeScript types for the new User DAL? + +here is the current //src/app/update/[id]/_actions.ts + +'use server'; + + + +//src/app/update/[id]/_actions.ts + + + +import { auth } from "@/auth"; + +import { prisma } from "@/lib/prisma"; + +import { revalidatePath } from "next/cache"; + + + +export async function updateFileAction(formData: FormData) { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +const id = formData.get("id") as string; + +const name = formData.get("name") as string; + +const description = formData.get("description") as string; + +const parentIdRaw = formData.get("parentId") as string; + +const customMetadataRaw = formData.get("customMetadata") as string; + + + +const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + +const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + + +try { + +// 1. Get existing record to preserve system metadata (like mimeType) + +const existing = await prisma.fileNode.findUnique({ where: { id } }); + +const existingMetadata = (existing?.metadata as Record) || {}; + + + +// 2. Update the record + +await prisma.fileNode.update({ + +where: { id }, + +data: { + +name, + +description, + +parentId, + +metadata: { + +...customMetadata, // User's new keys + +type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", + +mimeType: existingMetadata.mimeType // Preserve the original mimeType + +} + +} + +}); + + + +revalidatePath("/dashboard"); + +revalidatePath(`/update/${id}`); + +return { success: true }; + +} catch (error: any) { + +console.error("Update error:", error); + +return { success: false, message: error.message }; + +} + +} + +To wrap up the refactor for the Update feature, we will apply the DAL (Data Access Layer) and Service Layer patterns to src/app/update/[id]/_actions.ts. + +In this version, we remove the direct dependency on prisma and replace the manual logic for merging metadata with cleaner calls to the central file-nodes.ts DAL. This ensures that if you ever change your database schema, you only have to update it in one place. + +src/app/update/[id]/_actions.ts +TypeScript +'use server'; + +// src/app/update/[id]/_actions.ts + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes"; + +/** + * SERVER ACTION: Updates file metadata and organizational data. + * This refactored version uses the Data Access Layer (DAL) to + * ensure separation of concerns. + */ +export async function updateFileAction(formData: FormData) { + const session = await auth(); + + // 1. Authorization Guard + if (!session?.user?.id) throw new Error("Unauthorized"); + + // 2. Data Extraction + const id = formData.get("id") as string; + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const parentIdRaw = formData.get("parentId") as string; + const customMetadataRaw = formData.get("customMetadata") as string; + + const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + try { + // 3. DAL: Fetch existing record to safely merge metadata + // This replaces the direct prisma.fileNode.findUnique call + const existing = await getFileNodeById(id); + if (!existing) throw new Error("File record not found"); + + const existingMetadata = (existing.metadata as Record) || {}; + + // 4. Logic: Prepare the updated metadata object + const updatedMetadata = { + ...customMetadata, // Apply new user keys + type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", + mimeType: existingMetadata.mimeType // Ensure system metadata isn't overwritten + }; + + // 5. DAL: Perform the update + // This replaces the direct prisma.fileNode.update call + await updateFileNode(id, { + name, + description, + parentId, + metadata: updatedMetadata, + }); + + // 6. Cache Invalidation + revalidatePath("/dashboard"); + revalidatePath(`/update/${id}`); + + return { success: true }; + } catch (error: any) { + console.error("Update action error:", error); + return { + success: false, + message: error.message || "An unexpected error occurred during update" + }; + } +} +Key Refactor Notes: +Removed Prisma Import: The action is now completely decoupled from the database client. + +Used getFileNodeById: By using the DAL function, we benefit from any global ordering or default includes defined in the DAL. + +Used updateFileNode: This ensures that system-wide "updated at" timestamps or logging added to the DAL are automatically applied here. + +Error Handling: The try/catch block now provides a more user-friendly error message while still logging the technical details to the server console. + +Final Verification for your feature/dal-refactor branch: +Since we have now refactored: + +src/app/dashboard/actions.ts + +src/app/dashboard/sync-actions.ts + +src/app/dashboard/upload-actions.ts + +src/app/api/download/route.ts + +src/app/update/[id]/_actions.ts + +Created src/data-access/file-nodes.ts + +Created src/services/onedrive.ts + +You are now ready to test the full flow. If you haven't yet refactored the Settings (User management), that would be the very last piece of the puzzle to ensure 100% project-wide DAL compliance. + +Would you like me to provide the refactored code for the Settings user-role actions as well? + +Here is the setting src/app/settings/action.ts + +'use server'; + + + +import { auth } from "@/auth"; + +import { prisma } from "@/lib/prisma"; + +import { revalidatePath } from "next/cache"; + + + +/** + +* Toggles a user's role between 'ADMIN' and 'USER'. + +* * Security Logic: + +* 1. Checks if the caller is the Bootstrap Admin (via .env). + +* 2. Checks if the caller has the 'ADMIN' role in the database. + +* 3. Prevents the Bootstrap Admin from being demoted to 'USER'. + +*/ + +export async function toggleUserRoleAction(targetUserId: string) { + +const session = await auth(); + +const callerEmail = session?.user?.email; + + + +if (!callerEmail) { + +throw new Error("Unauthorized: No session found."); + +} + + + +// 1. Authorization: Who is trying to change the role? + +const isBootstrap = callerEmail === process.env.INITIAL_ADMIN_EMAIL; + + +const callerDbRecord = await prisma.user.findUnique({ + +where: { email: callerEmail }, + +select: { role: true } + +}); + + + +const isAdmin = isBootstrap || callerDbRecord?.role === "ADMIN"; + + + +if (!isAdmin) { + +throw new Error("Forbidden: You do not have permission to manage roles."); + +} + + + +// 2. Fetch the target user to be modified + +const targetUser = await prisma.user.findUnique({ + +where: { id: targetUserId }, + +select: { id: true, email: true, role: true } + +}); + + + +if (!targetUser) { + +throw new Error("User not found."); + +} + + + +// 3. Protection: Prevent demoting the primary bootstrap admin + +// This ensures you don't accidentally lock yourself out of the settings page. + +if (targetUser.email === process.env.INITIAL_ADMIN_EMAIL && targetUser.role === "ADMIN") { + +throw new Error("Security Restriction: The primary Bootstrap Admin role cannot be removed."); + +} + + + +// 4. Determine new role + +const newRole = targetUser.role === "ADMIN" ? "USER" : "ADMIN"; + + + +// 5. Execute Update + +await prisma.user.update({ + +where: { id: targetUserId }, + +data: { role: newRole } + +}); + + + +// 6. Refresh the data on the Settings page + +revalidatePath("/settings"); + + + +return { + +success: true, + +message: `User ${targetUser.email} is now a ${newRole}` + +}; + +} + +To finish your project-wide refactor, we need to create a User Data Access Layer (DAL) and then update your settings action to use it. This ensures that even user management follows the same architectural pattern as your file management. + +Step 1: Create the User DAL (src/data-access/users.ts) +This file centralizes all database interactions for the User table. + +TypeScript +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Get user by Email + * Used for authorization checks in actions. + */ +export async function getUserByEmail(email: string) { + return await prisma.user.findUnique({ + where: { email }, + select: { id: true, email: true, role: true } + }); +} + +/** + * FETCH: Get user by ID + */ +export async function getUserById(id: string) { + return await prisma.user.findUnique({ + where: { id }, + select: { id: true, email: true, role: true } + }); +} + +/** + * UPDATE: Update user role + */ +export async function updateUserRole(id: string, role: "ADMIN" | "USER") { + return await prisma.user.update({ + where: { id }, + data: { role } + }); +} + +/** + * FETCH: List all users (for the settings table) + */ +export async function getAllUsers() { + return await prisma.user.findMany({ + orderBy: { email: 'asc' } + }); +} +Step 2: Refactor Settings Action (src/app/settings/actions.ts) +Now we rewrite the action to use the DAL. Notice how the "Business Rules" (bootstrap admin protection) stay in the action, but the "Data Fetching" moves to the DAL. + +TypeScript +'use server'; + +// src/app/settings/actions.ts + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { getUserByEmail, getUserById, updateUserRole } from "@/data-access/users"; + +/** + * Toggles a user's role between 'ADMIN' and 'USER' using the DAL pattern. + */ +export async function toggleUserRoleAction(targetUserId: string) { + const session = await auth(); + const callerEmail = session?.user?.email; + + if (!callerEmail) { + throw new Error("Unauthorized: No session found."); + } + + // 1. Authorization: Verify caller's permissions via DAL + const isBootstrap = callerEmail === process.env.INITIAL_ADMIN_EMAIL; + const callerDbRecord = await getUserByEmail(callerEmail); + const isAdmin = isBootstrap || callerDbRecord?.role === "ADMIN"; + + if (!isAdmin) { + throw new Error("Forbidden: You do not have permission to manage roles."); + } + + // 2. Fetch target user via DAL + const targetUser = await getUserById(targetUserId); + + if (!targetUser) { + throw new Error("User not found."); + } + + // 3. Protection: Prevent demoting the primary bootstrap admin + if (targetUser.email === process.env.INITIAL_ADMIN_EMAIL && targetUser.role === "ADMIN") { + throw new Error("Security Restriction: The primary Bootstrap Admin role cannot be removed."); + } + + // 4. Logic: Determine new role + const newRole = targetUser.role === "ADMIN" ? "USER" : "ADMIN"; + + // 5. Execute Update via DAL + await updateUserRole(targetUserId, newRole); + + // 6. UI Invalidation + revalidatePath("/settings"); + + return { + success: true, + message: `User ${targetUser.email} is now a ${newRole}` + }; +} +Refactor Complete! +You have now successfully migrated your entire application to a professional Data Access Layer (DAL) + Service Layer architecture. + +Summary of what you've achieved on this new Git branch: + +Centralized DB Logic: Every Prisma query is now in src/data-access/. If you change your schema, you only edit those files. + +Centralized API Logic: All Microsoft Graph calls and the getFreshAccessToken logic are hidden inside src/services/onedrive.ts. + +Clean Server Actions: Your actions in dashboard, update, upload, and settings are now high-level "orchestrators" that are easy to read and maintain. + +Security: The use of server-only ensures that your database and API secrets can never leak to the browser. + +Final Steps: + +Run npm run dev and perform a full "smoke test" (Upload, Sync, Update, Download, and Role Toggle). + +Once confirmed, you can merge this branch: + +Bash +git checkout main +git merge feature/dal-refactor +Is there any specific part of the flow you'd like to double-check or add error-logging to before you merge? + +I think we need to do src/app/upload/_actions.ts _actions + +'use server'; + + + +import { auth } from "@/auth"; + +import { getFreshAccessToken } from "@/lib/auth-utils"; + +import { prisma } from "@/lib/prisma"; + +import { revalidatePath } from "next/cache"; + + + +/** + +* 1. CREATE FOLDER: Virtual Only + +* Logic: User-created organizational folders exist ONLY in the database. + +* No call to OneDrive is made here. + +*/ + +export async function createFolderAction(name: string, parentId?: string | null) { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +try { + +const internalId = crypto.randomUUID(); + + + +const newNode = await prisma.fileNode.create({ + +data: { + +id: internalId, + +oneDriveId: null, // Virtual folders do not have a cloud ID + +name: name, + +isFolder: true, + +path: `virtual:/${name}`, + +ownerId: session.user.id, + +parentId: parentId || null, + +metadata: { type: "FOLDER" } + +} + +}); + + + +revalidatePath("/upload"); + +revalidatePath("/dashboard"); + + +return { success: true, node: newNode }; + +} catch (error: any) { + +console.error("Folder creation error:", error); + +throw new Error(error.message || "Failed to create virtual folder"); + +} + +} + + + +/** + +* 2. UPLOAD FILE: Physical Container + +* Logic: Creates a physical folder (UUID) on OneDrive to hold the file. + +* This ensures every file has a unique storage space in the cloud. + +*/ + +export async function uploadFileAction(formData: FormData) { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +const file = formData.get("file") as File; + +const description = formData.get("description") as string || ""; + +const parentIdRaw = formData.get("parentId") as string | null; + +const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + + +const customMetadataRaw = formData.get("customMetadata") as string; + +const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + + +if (!file) throw new Error("No file selected"); + + + +const accessToken = await getFreshAccessToken(session.user.id); + +const rootFolder = "WebCalibre"; + +const internalId = crypto.randomUUID(); // This UUID will be the OneDrive folder name + + + +// 1. Create the Physical Storage Folder on OneDrive + +const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { + +method: "POST", + +headers: { + +Authorization: `Bearer ${accessToken}`, + +"Content-Type": "application/json" + +}, + +body: JSON.stringify({ + +name: internalId, + +folder: {}, + +"@microsoft.graph.conflictBehavior": "fail" + +}) + +}); + + + +if (!createSubFolderRes.ok) { + +const errorData = await createSubFolderRes.json(); + +throw new Error(errorData.error?.message || "Storage directory creation failed"); + +} + +const subFolderData = await createSubFolderRes.json(); + + + +// 2. Create Upload Session inside the new Physical Folder + +const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`; + +const sessionRes = await fetch(sessionUrl, { + +method: "POST", + +headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, + +body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) + +}); + + +const { uploadUrl } = await sessionRes.json(); + +const buffer = Buffer.from(await file.arrayBuffer()); + + + +// 3. PUT the file binary + +const uploadRes = await fetch(uploadUrl, { + +method: "PUT", + +headers: { + +"Content-Length": `${file.size}`, + +"Content-Range": `bytes 0-${file.size - 1}/${file.size}` + +}, + +body: buffer + +}); + + + +const uploadedFileData = await uploadRes.json(); + +const oneDriveId = uploadedFileData.id; + +const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN"; + + + +// 4. Create record in Database + +// Link it to the VIRTUAL folder via parentId + +await prisma.fileNode.create({ + +data: { + +id: internalId, + +oneDriveId: oneDriveId, + +name: file.name, + +description: description, + +size: BigInt(file.size), + +isFolder: false, + +path: `/${rootFolder}/${internalId}/${file.name}`, + +ownerId: session.user.id, + +parentId: parentId, + +metadata: { + +...customMetadata, + +type: extension, + +mimeType: file.type + +} + +} + +}); + + + +revalidatePath("/dashboard"); + +revalidatePath("/upload"); + +return { success: true }; + +} + +This is the final piece of the logic refactor. This specific file is interesting because it introduces Virtual Folders (Database only) versus Physical Storage (OneDrive UUID folders). + +By moving these into the DAL and Service Layer, we clean up the complex fetch calls while maintaining that unique "UUID-per-file" storage logic. + +1. Update the Service Layer (src/services/onedrive.ts) +Add the specific logic for creating subfolders and handling the upload session inside them. + +TypeScript +/** + * SERVICE: Create a folder by name inside a parent path + */ +export async function createOneDriveFolder(userId: string, parentPath: string, folderName: string) { + return await graphRequest(userId, `/me/drive/root:/${parentPath}:/children`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: folderName, + folder: {}, + "@microsoft.graph.conflictBehavior": "fail" + }) + }); +} + +/** + * SERVICE: Upload to a specific folder ID (using session) + */ +export async function uploadToFolderId(userId: string, file: File, folderId: string) { + const sessionRes = await graphRequest(userId, `/me/drive/items/${folderId}:/${encodeURIComponent(file.name)}:/createUploadSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) + }); + + const { uploadUrl } = await sessionRes.json(); + const buffer = Buffer.from(await file.arrayBuffer()); + + const uploadRes = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": `${file.size}`, + "Content-Range": `bytes 0-${file.size - 1}/${file.size}` + }, + body: buffer + }); + + if (!uploadRes.ok) throw new Error("Upload failed"); + return await uploadRes.json(); +} +2. Update the Data Access Layer (src/data-access/file-nodes.ts) +Ensure we have a create method that allows passing a specific id (since you use the internalId UUID for both the DB and OneDrive folder name). + +TypeScript +/** + * CREATE: Add a new node with a pre-generated ID + */ +export async function createNode(data: { + id: string; + oneDriveId: string | null; + name: string; + description?: string; + isFolder: boolean; + path: string; + ownerId: string; + parentId: string | null; + size?: bigint; + metadata: any; +}) { + return await prisma.fileNode.create({ data }); +} +3. Refactor the Server Action (src/app/upload/_actions.ts) +Now the orchestrator is clean and handles the "Virtual vs. Physical" logic elegantly. + +TypeScript +'use server'; + +// src/app/upload/_actions.ts + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +import { createNode } from "@/data-access/file-nodes"; +import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; + +/** + * 1. CREATE VIRTUAL FOLDER + */ +export async function createFolderAction(name: string, parentId?: string | null) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + const internalId = crypto.randomUUID(); + const newNode = await createNode({ + id: internalId, + oneDriveId: null, + name, + isFolder: true, + path: `virtual:/${name}`, + ownerId: session.user.id, + parentId: parentId || null, + metadata: { type: "FOLDER" } + }); + + revalidatePath("/dashboard"); + return { success: true, node: newNode }; + } catch (error: any) { + throw new Error(error.message || "Failed to create virtual folder"); + } +} + +/** + * 2. UPLOAD FILE (Physical UUID Folder) + */ +export async function uploadFileAction(formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const file = formData.get("file") as File; + const description = formData.get("description") as string || ""; + const parentIdRaw = formData.get("parentId") as string | null; + const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + + const customMetadataRaw = formData.get("customMetadata") as string; + const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + if (!file) throw new Error("No file selected"); + + const rootFolder = "WebCalibre"; + const internalId = crypto.randomUUID(); // Used for both DB ID and OneDrive Folder Name + + try { + // A. Ensure root exists + await ensureOneDriveFolder(session.user.id, rootFolder); + + // B. Create the physical UUID folder on OneDrive + const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId); + const subFolderData = await subFolderRes.json(); + + // C. Upload the file binary into that specific folder + const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id); + + // D. Create record in Database + await createNode({ + id: internalId, + oneDriveId: uploadedFileData.id, + name: file.name, + description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId, + metadata: { + ...customMetadata, + type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", + mimeType: file.type + } + }); + + revalidatePath("/dashboard"); + revalidatePath("/upload"); + return { success: true }; + } catch (error: any) { + console.error("Upload refactor error:", error); + return { success: false, error: error.message }; + } +} +Final Architecture Review +You have now completed the migration for: + +Dashboard Actions (Sync, Delete, List) + +Update Actions (Metadata merge) + +Settings Actions (User role management) + +Upload Actions (Virtual Folders and UUID Physical uploads) + +API Routes (Streaming downloads) + +Everything is now centralized. Next Steps: Since you are on your feature/dal-refactor branch, I recommend running npx prisma generate just to ensure your client is synced, then performing one full test of the "Upload -> Sync -> Update -> Download" lifecycle. + +Are you ready to merge this into your main branch? + +There seems to be problem in the src/app/upload/_actions.ts + +import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; as it cannot find onedrive + +I have printed out tree -I node_modules so you can check + +tree -I "node_modules" + +. + +├── README.md + +├── docs + +│ ├── Ai-converstion.md + +│ ├── Ai-converstion.pdf + +│ ├── images + +│ │ ├── Certicates_secrets.png + +│ │ └── PermissionsNeeded.png + +│ ├── notes.md + +│ └── notes.pdf + +├── errors.txt + +├── eslint.config.mjs + +├── next-env.d.ts + +├── next.config.ts + +├── package-lock.json + +├── package.json + +├── prisma + +│ ├── migrations + +│ │ ├── 20260107011428_setup_auth_and_files + +│ │ │ └── migration.sql + +│ │ ├── 20260107012848_add_auth_tables + +│ │ │ └── migration.sql + +│ │ └── migration_lock.toml + +│ └── schema.prisma + +├── prisma.config.ts + +├── public + +│ ├── file.svg + +│ ├── globe.svg + +│ ├── next.svg + +│ ├── vercel.svg + +│ └── window.svg + +├── src + +│ ├── app + +│ │ ├── api + +│ │ │ ├── auth + +│ │ │ │ └── [...nextauth] + +│ │ │ │ └── route.ts + +│ │ │ └── download + +│ │ │ └── route.ts + +│ │ ├── dashboard + +│ │ │ ├── actions.ts + +│ │ │ ├── dashboard-view.tsx + +│ │ │ ├── page.tsx + +│ │ │ ├── sync-actions.ts + +│ │ │ ├── upload-actions.ts + +│ │ │ └── upload.tsx + +│ │ ├── favicon.ico + +│ │ ├── globals.css + +│ │ ├── layout.tsx + +│ │ ├── layout.tsx-bak + +│ │ ├── page.module.css + +│ │ ├── page.tsx + +│ │ ├── profile + +│ │ │ └── page.tsx + +│ │ ├── settings + +│ │ │ ├── actions.ts + +│ │ │ ├── page.tsx + +│ │ │ └── user-row.tsx + +│ │ ├── update + +│ │ │ └── [id] + +│ │ │ ├── _actions.ts + +│ │ │ ├── page.tsx + +│ │ │ └── update-view.tsx + +│ │ └── upload + +│ │ ├── _actions.ts + +│ │ ├── page.tsx + +│ │ └── upload-view.tsx + +│ ├── auth.config.ts + +│ ├── auth.ts + +│ ├── components + +│ │ ├── ThemeRegistry.tsx + +│ │ ├── layout + +│ │ │ ├── AppShell.tsx + +│ │ │ └── Navbar.tsx + +│ │ └── providers.tsx + +│ ├── data-access + +│ │ ├── file-nodes.ts + +│ │ └── users.ts + +│ ├── lib + +│ │ ├── auth-utils.ts + +│ │ ├── prisma.ts + +│ │ └── theme.ts + +│ ├── proxy.ts + +│ ├── services + +│ │ └── onedrive.ts + +│ └── types + +│ └── next-auth.d.ts + +└── tsconfig.json + +The issue is clear from your file tree: you have created the file at src/services/onedrive.ts, but in your code, you are likely trying to import it using a path that TypeScript or Next.js can't resolve. + +Based on your project structure, there are two ways to fix this: + +Option 1: Use the Alias (Recommended) +Since you are using a standard Next.js setup with a tsconfig.json, you likely have the @ alias configured to point to the src directory. This is the cleanest way to import. + +Change your import in src/app/upload/_actions.ts to: + +TypeScript +import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; +Option 2: Use the Relative Path +If you prefer not to use the alias, you must go "up" enough directory levels to reach the services folder from the upload folder. + +In src/app/upload/_actions.ts, the relative path would be: + +TypeScript +import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "../../services/onedrive"; +(Explanation: ../ takes you to app, the second ../ takes you to src, then you enter services/onedrive). + +Why "it cannot find onedrive" might still happen +If you have updated the import path and it still fails, check these two things: + +Check the File Extension: Ensure the file is named exactly onedrive.ts (lowercase o) in the src/services/ folder. Linux/Nginx environments are case-sensitive, so OneDrive.ts vs onedrive.ts matters. + +Verify the Exports: Ensure that inside src/services/onedrive.ts, you have used the export keyword before each function: + +TypeScript +// src/services/onedrive.ts +export async function createOneDriveFolder(...) { ... } +export async function uploadToFolderId(...) { ... } +export async function ensureOneDriveFolder(...) { ... } +Try updating the import to the @/services/onedrive alias first, as that is the standard for the rest of your project. Does the error persist after that change? + +here is the tsconfig.json + +{ + +"compilerOptions": { + +"target": "ES2017", + +"lib": ["dom", "dom.iterable", "esnext"], + +"allowJs": true, + +"skipLibCheck": true, + +"strict": true, + +"noEmit": true, + +"esModuleInterop": true, + +"module": "esnext", + +"moduleResolution": "bundler", + +"resolveJsonModule": true, + +"isolatedModules": true, + +"jsx": "react-jsx", + +"incremental": true, + +"plugins": [ + +{ + +"name": "next" + +} + +], + +"paths": { + +"@/*": ["./src/*"] + +} + +}, + +"include": [ + +"next-env.d.ts", + +"**/*.ts", + +"**/*.tsx", + +".next/types/**/*.ts", + +".next/dev/types/**/*.ts", + +"**/*.mts" + +], + +"exclude": ["node_modules"] + +} + +Your tsconfig.json is correctly configured with the path alias: "@/*": ["./src/*"] + +This confirms that using import { ... } from "@/services/onedrive" is the correct way to handle imports. + +If VS Code or the Next.js compiler still says it "cannot find onedrive", it is usually due to one of three "silent" issues. Please check these in order: + +1. The "Extra Folder" Typo +Looking at your tree output: src/services/onedrive.ts Double-check that you didn't accidentally name the file one-drive.ts or oneDrive.ts (with a capital D). In your import, you are using onedrive (all lowercase). If the filename has a hyphen or a capital letter, the alias will fail to resolve. + +2. Missing export keywords +Open src/services/onedrive.ts. If you forgot to put the word export before the function definitions, the file is seen as a "module with no members," and the import in _actions.ts will fail. + +It should look like this: + +TypeScript +// src/services/onedrive.ts +import "server-only"; +import { getFreshAccessToken } from "@/lib/auth-utils"; + +export async function graphRequest(...) { ... } // MUST HAVE EXPORT +export async function createOneDriveFolder(...) { ... } // MUST HAVE EXPORT +export async function uploadToFolderId(...) { ... } // MUST HAVE EXPORT +export async function ensureOneDriveFolder(...) { ... } // MUST HAVE EXPORT +3. Restart the TS Server (Common VS Code bug) +Sometimes VS Code's TypeScript engine gets stuck and doesn't "see" newly created folders like src/services or src/data-access. + +In VS Code, press Ctrl+Shift+P (or Cmd+Shift+P on Mac). + +Type "Restart TS Server" and hit Enter. + +This forces VS Code to re-read your tsconfig.json and file tree. + +Final Code for src/app/upload/_actions.ts +With your tsconfig.json paths confirmed, this is the exact import block that should work: + +TypeScript +'use server'; + +import { auth } from "@/auth"; +import { revalidatePath } from "next/cache"; +// Use the @ alias pointing to your new folders +import { createNode } from "@/data-access/file-nodes"; +import { + createOneDriveFolder, + uploadToFolderId, + ensureOneDriveFolder +} from "@/services/onedrive"; + +// ... rest of your functions +Summary of the fix: + +Use the @/services/onedrive path. + +Ensure every function in onedrive.ts starts with the export keyword. + +Restart the TypeScript server in your editor. + +Does the red squiggly line go away after restarting the TS Server? + +While just checking src/services/onedrive.ts I just noticed + +/** + +* UPSERT: Create or Update a file node based on OneDrive ID + +*/ + +export async function upsertFileNode(oneDriveId: string, data: any) { + +return await prisma.fileNode.upsert({ + +The prisma has sqiggly red line under it and it can find it. + +here is the current src/services/onedrive.ts + +// src/services/onedrive.ts + +import "server-only"; + +import { getFreshAccessToken } from "@/lib/auth-utils"; + + + +/** + +* PRIVATE HELPER: graphRequest + +* This internal function handles the heavy lifting of fetching tokens + +* and making the actual HTTP call to Microsoft. + +*/ + +async function graphRequest(userId: string, endpoint: string, options: RequestInit = {}) { + +// 1. Automatically handle token refresh logic + +const token = await getFreshAccessToken(userId); + +const baseUrl = "https://graph.microsoft.com/v1.0"; + + +const res = await fetch(`${baseUrl}${endpoint}`, { + +...options, + +headers: { + +...options.headers, + +Authorization: `Bearer ${token}`, + +}, + +}); + + + +// 2. Centralized Error Handling for OneDrive + +if (!res.ok) { + +const errorData = await res.text(); + +console.error(`OneDrive API Error [${endpoint}]:`, errorData); + +throw new Error(`OneDrive API failed: ${res.statusText}`); + +} + + + +return res; + +} + + + +/** + +* SERVICE: Download File Content + +* Returns the raw binary stream from OneDrive. + +*/ + +export async function getOneDriveContentStream(userId: string, oneDriveId: string) { + +return await graphRequest(userId, `/me/drive/items/${oneDriveId}/content`); + +} + + + +/** + +* SERVICE: Get File Metadata + +* Used to get the @microsoft.graph.downloadUrl or driveItem properties. + +*/ + +export async function getOneDriveItem(userId: string, oneDriveId: string) { + +const res = await graphRequest(userId, `/me/drive/items/${oneDriveId}`); + +return res.json(); + +} + + + +/** + +* SERVICE: Upload File + +* Handles the PUT request to OneDrive for new or updated files. + +*/ + +export async function uploadToOneDrive(userId: string, file: File, oneDriveId?: string) { + +// If oneDriveId exists, we update. Otherwise, we'd use a path (needs expansion for new files). + +const endpoint = oneDriveId + +? `/me/drive/items/${oneDriveId}/content` + +: `/me/drive/root:/${file.name}:/content`; + + + +return await graphRequest(userId, endpoint, { + +method: "PUT", + +headers: { "Content-Type": file.type }, + +body: Buffer.from(await file.arrayBuffer()), + +}); + +} + + + +/** + +* SERVICE: Delete from Cloud + +*/ + +export async function deleteFromOneDrive(userId: string, oneDriveId: string) { + +return await graphRequest(userId, `/me/drive/items/${oneDriveId}`, { + +method: "DELETE", + +}); + +} + + + +/** + +* SERVICE: List Children of the WebCalibre folder + +*/ + +export async function getWebCalibreChildren(userId: string) { + +const res = await graphRequest(userId, "/me/drive/root:/WebCalibre:/children"); + +const data = await res.json(); + +return data.value; // Returns the array of driveItems + +} + +/** + +* UPSERT: Create or Update a file node based on OneDrive ID + +*/ + +export async function upsertFileNode(oneDriveId: string, data: any) { + +return await prisma.fileNode.upsert({ + +where: { oneDriveId }, + +update: { + +name: data.name, + +size: data.size, + +isFolder: data.isFolder, + +path: data.path, + +updatedAt: new Date(), + +}, + +create: { + +id: crypto.randomUUID(), + +oneDriveId: oneDriveId, + +name: data.name, + +size: data.size, + +isFolder: data.isFolder, + +path: data.path, + +ownerId: data.ownerId, + +metadata: data.metadata, + +} + +}); + +} + + + +/** + +* SERVICE: Ensure a specific folder exists in OneDrive + +* Returns the folder ID + +*/ + +export async function ensureOneDriveFolder(userId: string, folderName: string) { + +try { + +const res = await graphRequest(userId, `/me/drive/root:/${folderName}`); + +const data = await res.json(); + +return data.id; + +} catch (error) { + +// If 404, create it + +const createRes = await graphRequest(userId, `/me/drive/root/children`, { + +method: "POST", + +headers: { "Content-Type": "application/json" }, + +body: JSON.stringify({ name: folderName, folder: {} }) + +}); + +const data = await createRes.json(); + +return data.id; + +} + +} + + + +/** + +* SERVICE: Upload Large File via Session + +* This replaces the basic PUT for better reliability + +*/ + +export async function uploadLargeFile(userId: string, file: File, folderName: string) { + +// 1. Create Upload Session + +const sessionRes = await graphRequest(userId, `/me/drive/root:/${folderName}/${file.name}:/createUploadSession`, { + +method: "POST", + +headers: { "Content-Type": "application/json" }, + +body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } }) + +}); + +const { uploadUrl } = await sessionRes.json(); + + + +// 2. Upload the data to the provided URL (No Authorization header needed for the uploadUrl itself) + +const buffer = Buffer.from(await file.arrayBuffer()); + +const uploadRes = await fetch(uploadUrl, { + +method: "PUT", + +headers: { + +"Content-Length": `${file.size}`, + +"Content-Range": `bytes 0-${file.size - 1}/${file.size}` + +}, + +body: buffer + +}); + + + +if (!uploadRes.ok) throw new Error("Upload session failed"); + +return await uploadRes.json(); // Returns the DriveItem + +} + +/** + +* SERVICE: Create a folder by name inside a parent path + +*/ + +export async function createOneDriveFolder(userId: string, parentPath: string, folderName: string) { + +return await graphRequest(userId, `/me/drive/root:/${parentPath}:/children`, { + +method: "POST", + +headers: { "Content-Type": "application/json" }, + +body: JSON.stringify({ + +name: folderName, + +folder: {}, + +"@microsoft.graph.conflictBehavior": "fail" + +}) + +}); + +} + + + +/** + +* SERVICE: Upload to a specific folder ID (using session) + +*/ + +export async function uploadToFolderId(userId: string, file: File, folderId: string) { + +const sessionRes = await graphRequest(userId, `/me/drive/items/${folderId}:/${encodeURIComponent(file.name)}:/createUploadSession`, { + +method: "POST", + +headers: { "Content-Type": "application/json" }, + +body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) + +}); + + +const { uploadUrl } = await sessionRes.json(); + +const buffer = Buffer.from(await file.arrayBuffer()); + + + +const uploadRes = await fetch(uploadUrl, { + +method: "PUT", + +headers: { + +"Content-Length": `${file.size}`, + +"Content-Range": `bytes 0-${file.size - 1}/${file.size}` + +}, + +body: buffer + +}); + + + +if (!uploadRes.ok) throw new Error("Upload failed"); + +return await uploadRes.json(); + +} + +Ah, you've caught a very important detail! The reason prisma has a red squiggly line is because of the Separation of Concerns principle we are implementing. + +The src/services/onedrive.ts file is part of the Service Layer, which should only handle external API logic (Microsoft Graph). It should not contain any direct database queries (prisma). Direct database work belongs exclusively in the Data Access Layer (DAL). + +The function upsertFileNode was accidentally placed in the OneDrive service file during our discussion. To fix the error and follow the pattern correctly, we need to move that function. + +The Fix +1. Move the function to src/data-access/file-nodes.ts +Open src/data-access/file-nodes.ts and add the upsertFileNode function there. Make sure prisma is imported at the top of that file. + +TypeScript +// src/data-access/file-nodes.ts +import "server-only"; +import { prisma } from "@/lib/prisma"; // Ensure this is here + +// ... other functions (getAllFileNodes, etc) + +/** + * UPSERT: Create or Update a file node based on OneDrive ID + * Moved here because it interacts with the Database. + */ +export async function upsertFileNode(oneDriveId: string, data: any) { + return await prisma.fileNode.upsert({ + where: { oneDriveId }, + update: { + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + updatedAt: new Date(), + }, + create: { + id: crypto.randomUUID(), + oneDriveId: oneDriveId, + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + ownerId: data.ownerId, + metadata: data.metadata, + } + }); +} +2. Clean up src/services/onedrive.ts +Delete the upsertFileNode function from src/services/onedrive.ts entirely. This file should have no mention of prisma. + +Why this matters +By keeping them separate, you maintain a clean flow of data: + +Actions decide what to do. + +Services talk to the cloud. + +DAL talks to the database. + +Next Step +After you move that function, you might see an error in src/app/dashboard/sync-actions.ts saying it can't find upsertFileNode. Simply update the import at the top of that file to point to the DAL instead: + +TypeScript +// src/app/dashboard/sync-actions.ts +import { upsertFileNode } from "@/data-access/file-nodes"; // Change this from /services/onedrive +Does moving that function clear up the red squiggly lines for you? + +// src/app/dashboard/sync-actions.tshad this in corrently + +import { upsertFileNode } from "@/data-access/file-nodes"; + +Exactly. That is the correct way to link those layers. + +To make sure your Data Access Layer (DAL) is fully optimized and complete, let's look at the final version of src/data-access/file-nodes.ts. Since we've moved several functions here during our audit (including upsertFileNode and createNode), this file is now the powerhouse of your file management system. + +The Final src/data-access/file-nodes.ts +TypeScript +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Retrieve all nodes for the dashboard. + */ +export async function getAllFileNodes() { + return await prisma.fileNode.findMany({ + orderBy: { updatedAt: 'desc' }, + }); +} + +/** + * FETCH: Get a single node by ID. + */ +export async function getFileNodeById(id: string) { + return await prisma.fileNode.findUnique({ + where: { id }, + }); +} + +/** + * CREATE: Add a new node (used by upload-actions) + */ +export async function createNode(data: { + id: string; + oneDriveId: string | null; + name: string; + description?: string; + isFolder: boolean; + path: string; + ownerId: string; + parentId: string | null; + size?: bigint; + metadata: any; +}) { + return await prisma.fileNode.create({ data }); +} + +/** + * UPSERT: Create or Update based on OneDrive ID (used by sync-actions) + */ +export async function upsertFileNode(oneDriveId: string, data: any) { + return await prisma.fileNode.upsert({ + where: { oneDriveId }, + update: { + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + updatedAt: new Date(), + }, + create: { + id: crypto.randomUUID(), + oneDriveId: oneDriveId, + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + ownerId: data.ownerId, + metadata: data.metadata, + } + }); +} + +/** + * UPDATE: Generic update for metadata, names, or parents. + */ +export async function updateFileNode(id: string, data: any) { + return await prisma.fileNode.update({ + where: { id }, + data: { + ...data, + updatedAt: new Date(), + }, + }); +} + +/** + * DELETE: Remove from DB. + */ +export async function deleteFileNode(id: string) { + return await prisma.fileNode.delete({ + where: { id }, + }); +} +Architecture Flow Checklist +Now that the imports are fixed, your logic flow for a Sync operation looks like this: + +User Clicks Sync in dashboard-view.tsx. + +syncOneDrive (Server Action) is triggered. + +Action calls getWebCalibreChildren (Service) to fetch cloud data. + +Action loops through the cloud items. + +Action calls upsertFileNode (DAL) to save each item into PostgreSQL. + +Action calls revalidatePath to refresh the UI. + +Next High-Value Step +Since we've cleaned up the code significantly, we should verify the BigInt handling. In your sync-actions.ts and upload-actions.ts, you are passing BigInt(item.size). + +Would you like me to check if your FileNode type in the UI needs a small adjustment to handle these BigInt values when rendering the file size? (Standard JSON serialization in Next.js often requires converting BigInt to a string before it hits the client). + +src/data-access/file-nodes.ts has squiggly red line in the following import + +import { prisma } from "@/lib/prisma"; + +here is the current src/data-access/file-nodes.ts + +// src/data-access/file-nodes.ts + +import "server-only"; + +import { prisma } from "@/lib/prisma"; + + + +/** + +* FETCH: Retrieve all nodes for the dashboard. + +* Centralizing this here allows us to change sort order or filters + +* in one place for the entire application. + +*/ + +export async function getAllFileNodes() { + +return await prisma.fileNode.findMany({ + +orderBy: { + +updatedAt: 'desc', + +}, + +}); + +} + + + +/** + +* FETCH: Get a single node by ID. + +* Used by the Download route and Update pages to verify a file exists. + +*/ + +export async function getFileNodeById(id: string) { + +return await prisma.fileNode.findUnique({ + +where: { id }, + +}); + +} + + + +/** + +* UPDATE: Modify metadata, name, or virtual location. + +* This function accepts the data object to keep the DAL flexible. + +*/ + +export async function updateFileNode(id: string, data: any) { + +return await prisma.fileNode.update({ + +where: { id }, + +data: { + +...data, + +updatedAt: new Date(), + +}, + +}); + +} + + + +/** + +* DELETE: Remove the record from the database. + +* Cloud deletion should be handled by the Service Layer before calling this. + +*/ + +export async function deleteFileNode(id: string) { + +return await prisma.fileNode.delete({ + +where: { id }, + +}); + +} + + + +/** + +* CREATE: Add a new file node record + +*/ + +export async function createFileNode(data: { + +oneDriveId: string; + +name: string; + +size: bigint; + +isFolder: boolean; + +path: string; + +ownerId: string; + +metadata: any; + +}) { + +return await prisma.fileNode.create({ data }); + +} + +/** + +* CREATE: Add a new node with a pre-generated ID + +*/ + +export async function createNode(data: { + +id: string; + +oneDriveId: string | null; + +name: string; + +description?: string; + +isFolder: boolean; + +path: string; + +ownerId: string; + +parentId: string | null; + +size?: bigint; + +metadata: any; + +}) { + +return await prisma.fileNode.create({ data }); + +} + + + +// src/data-access/file-nodes.ts + +import "server-only"; + +import { prisma } from "@/lib/prisma"; // Ensure this is here + + + +// ... other functions (getAllFileNodes, etc) + + + +/** + +* UPSERT: Create or Update a file node based on OneDrive ID + +* Moved here because it interacts with the Database. + +*/ + +export async function upsertFileNode(oneDriveId: string, data: any) { + +return await prisma.fileNode.upsert({ + +where: { oneDriveId }, + +update: { + +name: data.name, + +size: data.size, + +isFolder: data.isFolder, + +path: data.path, + +updatedAt: new Date(), + +}, + +create: { + +id: crypto.randomUUID(), + +oneDriveId: oneDriveId, + +name: data.name, + +size: data.size, + +isFolder: data.isFolder, + +path: data.path, + +ownerId: data.ownerId, + +metadata: data.metadata, + +} + +}); + +} + +The squiggly red line under import { prisma } from "@/lib/prisma"; is likely occurring because you have duplicate imports and a double-declaration of the same logic in that file. + +If you look at the code you shared, the top half starts with the imports, and then halfway down, the file restarts with import "server-only" and import { prisma } from "@/lib/prisma" again. TypeScript gets confused when you redeclare variables or imports in the same scope. + +Here is the clean, consolidated version of src/data-access/file-nodes.ts. Replace your entire file with this: + +TypeScript +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Retrieve all nodes for the dashboard. + */ +export async function getAllFileNodes() { + return await prisma.fileNode.findMany({ + orderBy: { + updatedAt: 'desc', + }, + }); +} + +/** + * FETCH: Get a single node by ID. + */ +export async function getFileNodeById(id: string) { + return await prisma.fileNode.findUnique({ + where: { id }, + }); +} + +/** + * CREATE: Add a new file node record (Generic) + */ +export async function createFileNode(data: { + oneDriveId: string; + name: string; + size: bigint; + isFolder: boolean; + path: string; + ownerId: string; + metadata: any; +}) { + return await prisma.fileNode.create({ data }); +} + +/** + * CREATE: Add a new node with a pre-generated ID (For Virtual Folders) + */ +export async function createNode(data: { + id: string; + oneDriveId: string | null; + name: string; + description?: string; + isFolder: boolean; + path: string; + ownerId: string; + parentId: string | null; + size?: bigint; + metadata: any; +}) { + return await prisma.fileNode.create({ data }); +} + +/** + * UPSERT: Create or Update a file node based on OneDrive ID. + * Primarily used during synchronization. + */ +export async function upsertFileNode(oneDriveId: string, data: any) { + return await prisma.fileNode.upsert({ + where: { oneDriveId }, + update: { + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + updatedAt: new Date(), + }, + create: { + id: crypto.randomUUID(), + oneDriveId: oneDriveId, + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + ownerId: data.ownerId, + metadata: data.metadata, + } + }); +} + +/** + * UPDATE: Modify metadata, name, or virtual location. + */ +export async function updateFileNode(id: string, data: any) { + return await prisma.fileNode.update({ + where: { id }, + data: { + ...data, + updatedAt: new Date(), + }, + }); +} + +/** + * DELETE: Remove the record from the database. + */ +export async function deleteFileNode(id: string) { + return await prisma.fileNode.delete({ + where: { id }, + }); +} +Why the error happened: +Duplicate Imports: You had import { prisma } twice in one file. + +Scope Pollution: Re-importing a module halfway through a file violates the ES Module specification that Next.js uses. + +Clean-up: I merged the "Generic Create" and "Pre-generated ID Create" functions so they sit neatly alongside the upsert logic. + +Troubleshooting the @ Alias +If the squiggly line stays even after cleaning the file, check your src/lib/prisma.ts file. + +Does it have an export const prisma = ...? + +If you named the export db instead of prisma, the import will fail. + +Does this consolidated version clear the red line for you? + +there is squiggly red line under data in the return await prisma.fileNode.create({ data }); + +with the following error + +Type '{ oneDriveId: string; name: string; size: bigint; isFolder: boolean; path: string; ownerId: string; metadata: any; }' is not assignable to type '(Without & FileNodeUncheckedCreateInput) | (Without<...> & FileNodeCreateInput)'. + +Type '{ oneDriveId: string; name: string; size: bigint; isFolder: boolean; path: string; ownerId: string; metadata: any; }' is not assignable to type 'Without & FileNodeUncheckedCreateInput'. + +Property 'id' is missing in type '{ oneDriveId: string; name: string; size: bigint; isFolder: boolean; path: string; ownerId: string; metadata: any; }' but required in type 'FileNodeUncheckedCreateInput'.ts(2322) + +index.d.ts(6716, 5): 'id' is declared here. + +index.d.ts(5654, 5): The expected type comes from property 'data' which is declared here on type '{ select?: FileNodeSelect | null | undefined; omit?: FileNodeOmit | + +/** + +* CREATE: Add a new file node record + +*/ + +export async function createFileNode(data: { + +oneDriveId: string; + +name: string; + +size: bigint; + +isFolder: boolean; + +path: string; + +ownerId: string; + +metadata: any; + +}) { + +return await prisma.fileNode.create({ data }); + +} + +This error is happening because your Prisma Schema defines the id field as a required field that is not auto-generated by the database (e.g., it doesn't have @default(uuid()) or @default(autoincrement()) in the .prisma file). + +Because of this, Prisma requires you to provide a unique string id manually every time you create a record. + +The Fix: Generate a UUID in the DAL +We can solve this by adding id: crypto.randomUUID() directly inside the createFileNode function. This keeps your Server Actions "clean" while satisfying Prisma's requirement. + +Update the function in src/data-access/file-nodes.ts to this: + +TypeScript +/** + * CREATE: Add a new file node record + * We add crypto.randomUUID() here because the database schema + * requires a manual ID. + */ +export async function createFileNode(data: { + oneDriveId: string; + name: string; + size: bigint; + isFolder: boolean; + path: string; + ownerId: string; + metadata: any; +}) { + return await prisma.fileNode.create({ + data: { + ...data, + id: crypto.randomUUID(), // This satisfies the 'id' required error + } + }); +} +Why this is happening +In your schema.prisma, your FileNode model likely looks like this: id String @id + +When Prisma sees that there is no @default attribute, it marks id as a mandatory property in the FileNodeCreateInput TypeScript type. By adding it in the DAL, you ensure that every record created through this function gets a valid unique identifier. + +A Quick Optimization +Since createFileNode and createNode now do almost exactly the same thing (creating a record with a generated ID), you could actually simplify your DAL by using just one robust creation function. + +Would you like me to merge those two creation functions into one "Master Create" function to keep your DAL even shorter? + +Yes please two creation functions into one "Master Create" function to keep your DAL even shorter + +I assume we have to find where one is being used so we can change its name + +Exactly. To consolidate these, we need to create one flexible function that accepts an optional id and parentId. If the id isn't provided (like in a standard upload), we generate one. If it is provided (like in your virtual folder logic), we use the one passed in. + +1. The "Master Create" Function +Replace both createFileNode and createNode in src/data-access/file-nodes.ts with this single function: + +TypeScript +/** + * MASTER CREATE: Handles both standard uploads and virtual folders. + * If no ID is provided, it generates a fresh UUID. + */ +export async function createFileNode(data: { + id?: string; // Optional: used for virtual folders/UUID storage + oneDriveId: string | null; + name: string; + description?: string; + isFolder: boolean; + path: string; + ownerId: string; + parentId?: string | null; // Optional: for nested structures + size?: bigint; + metadata: any; +}) { + return await prisma.fileNode.create({ + data: { + ...data, + id: data.id ?? crypto.randomUUID(), // Use provided ID or generate new one + } + }); +} +2. Audit & Update Callers +Now we must update the two places where these were used to ensure they match the new name and structure. + +A. In src/app/dashboard/upload-actions.ts +This was likely using the old createFileNode. It still works, but ensure you aren't passing an id here so it generates a fresh one. + +B. In src/app/upload/_actions.ts +You were likely using createNode here. Change it to createFileNode and ensure the arguments match: + +TypeScript +// src/app/upload/_actions.ts + +// ... inside uploadFileAction or createFolderAction ... +await createFileNode({ + id: internalId, // We pass the ID here so DB ID matches OneDrive folder name + oneDriveId: uploadedFileData.id, + name: file.name, + description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId: parentId, + metadata: { ... } +}); +Why this is better +Dry Code: You no longer have two nearly identical blocks of code maintaining the prisma.fileNode.create logic. + +Type Safety: The id?: string syntax tells TypeScript that the ID is optional at the call site, but the DAL logic guarantees it will exist before hitting the database. + +Consistency: Every file record in your system now goes through the exact same "gatekeeper" function. + +Does your src/app/upload/_actions.ts show any errors after renaming createNode to createFileNode? + +in // src/app/upload/_actions.ts + +I also fixed the import + +import { createFileNode } from "@/data-access/file-nodes"; + +can with createFileNode( + +createNode( is being used + +/** + +* 1. CREATE VIRTUAL FOLDER + +*/ + +export async function createFolderAction(name: string, parentId?: string | null) { + +const session = await auth(); + +if (!session?.user?.id) throw new Error("Unauthorized"); + + + +try { + +const internalId = crypto.randomUUID(); + +const newNode = await createNode({ + +id: internalId, + +oneDriveId: null, + +name, + +isFolder: true, + +path: `virtual:/${name}`, + +ownerId: session.user.id, + +parentId: parentId || null, + +metadata: { type: "FOLDER" } + +}); + +Perfect. Now that you've updated the import, just swap the function name in those two spots in src/app/upload/_actions.ts. Because we made id and parentId optional in our "Master Create" function, the transition is seamless. + +Here is how the refactored sections in your src/app/upload/_actions.ts should look: + +1. Updated createFolderAction +TypeScript +export async function createFolderAction(name: string, parentId?: string | null) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + const internalId = crypto.randomUUID(); + // Swapped createNode for createFileNode + const newNode = await createFileNode({ + id: internalId, + oneDriveId: null, + name, + isFolder: true, + path: `virtual:/${name}`, + ownerId: session.user.id, + parentId: parentId || null, + metadata: { type: "FOLDER" } + }); + + revalidatePath("/dashboard"); + return { success: true, node: newNode }; + } catch (error: any) { + throw new Error(error.message || "Failed to create virtual folder"); + } +} +2. Updated uploadFileAction +(Ensure the call at the bottom of the upload logic also uses the new name): + +TypeScript + // ... inside uploadFileAction after OneDrive work is done ... + + // Swapped createNode for createFileNode + await createFileNode({ + id: internalId, + oneDriveId: uploadedFileData.id, + name: file.name, + description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId, + metadata: { + ...customMetadata, + type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", + mimeType: file.type + } + }); +Final Clean-up Check +Now that you have one "Master" creation function, you can delete the older, redundant functions from src/data-access/file-nodes.ts if any are left. Your file nodes DAL should now contain: + +getAllFileNodes + +getFileNodeById + +createFileNode (The Master Create) + +upsertFileNode + +updateFileNode + +deleteFileNode + +Is everything compiling correctly now with no red lines in your upload/_actions.ts? If so, you've successfully unified the data entry point for the entire app! + +There is only a squiggly blue line where ever onedrive is used but think that a spelling warning + +That blue squiggly line is almost certainly just VS Code's spellchecker (Cody, Spell Right, or the built-in one) flagging "onedrive" as a non-dictionary word. Since it's blue (and not red), it won't prevent your code from compiling or running. + +If you want to get rid of it, you can usually hover over the word and select "Add 'onedrive' to dictionary." + +Final Project Architecture Audit +With all the refactoring we’ve done today, your project structure is now a textbook example of a clean Next.js 15+ Enterprise Architecture. Here is a final look at how your data flows: + +Layer Responsibility Example File +UI Displaying data & triggering actions dashboard-view.tsx +Server Actions Orchestration & Revalidation dashboard/actions.ts +Service Layer Talking to the Cloud (Microsoft Graph) services/onedrive.ts +DAL Talking to the Database (Prisma) data-access/file-nodes.ts +How to verify everything is perfect +To ensure there are no "ghost" errors before you merge your feature/dal-refactor branch, run this command in your terminal: + +Bash +npx tsc --noEmit +If it returns nothing: Your project is 100% type-safe and ready for production. + +If it returns errors: It will list exactly which files still have issues (sometimes VS Code hides errors until you open the specific file). + +What's Next? +Now that the backend is clean, you have a solid foundation. + +Would you like me to help you create a "Storage Usage" component for the settings page? Since we have the getAllFileNodes DAL, we could easily calculate how much space WebCalibre is using versus your total OneDrive quota. + +dashboard-view.tsx + +has an error in + +import { deleteFileAction } from "./actions"; + +That error is occurring because we renamed the function to deleteFileNodeAction in the refactored src/app/dashboard/actions.ts to be more descriptive and consistent with our new naming convention. + +To fix the red squiggly line in dashboard-view.tsx, you just need to update the import and the function call. + +The Fix in src/app/dashboard/dashboard-view.tsx +Update the Import: Find the line where you import the action and change it: + +TypeScript +// Change this: +import { deleteFileAction } from "./actions"; + +// To this: +import { deleteFileNodeAction } from "./actions"; +Update the Component Call: Search for where deleteFileAction is called (likely in a handleDelete function or a button's onClick) and update it to deleteFileNodeAction. + +Quick Audit of Action Names +Since we unified the naming during the refactor, here is a quick reference for your dashboard imports to ensure everything matches your actions.ts: + +Old Name (likely) New Refactored Name Purpose +getFileNodes getFileNodes Fetches list from DAL +getDownloadUrl getDownloadUrlAction Gets OneDrive link +deleteFileAction deleteFileNodeAction Cloud + DB removal +updateFileAction updateFileNodeAction Metadata + Cloud sync +Why renaming matters +By naming it deleteFileNodeAction, it is immediately clear to anyone reading the code that this is a Server Action (suffix Action) interacting with a FileNode (the entity name). + +Is that the last red line in your Dashboard? If so, you are officially ready to run npx tsc --noEmit and confirm the whole project is healthy. Would you like to do a quick double-check of the sync-actions.ts import in that same view as well? + +It found an error + +npx tsc --noEmit + +src/app/upload/_actions.ts:84:19 - error TS1109: Expression expected. + + + +84 metadata: { ... } + + ~ + + + + + +Found 1 error in src/app/upload/_actions.ts:84 + +within the section + +// ... inside uploadFileAction or createFolderAction ... + +await createFileNode({ + +id: internalId, // We pass the ID here so DB ID matches OneDrive folder name + +oneDriveId: uploadedFileData.id, + +name: file.name, + +description, + +size: BigInt(file.size), + +isFolder: false, + +path: `/${rootFolder}/${internalId}/${file.name}`, + +ownerId: session.user.id, + +parentId: parentId, + +metadata: { ... } + +}); + +That error TS1109: Expression expected is happening because the placeholder metadata: { ... } I used in the previous explanation isn't valid JavaScript. It was meant as a "fill in the blank" for your custom metadata, but the TypeScript compiler is trying to read the literal ... as code. + +To fix this, we need to provide the actual metadata object logic we defined earlier. + +The Fix for src/app/upload/_actions.ts +Find the block around line 84 and replace the metadata section with the logic that extracts the file extension and mimeType. + +TypeScript +// ... inside uploadFileAction after OneDrive work is done ... + +await createFileNode({ + id: internalId, + oneDriveId: uploadedFileData.id, + name: file.name, + description: description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId: parentId, + metadata: { + ...customMetadata, // User's custom keys from the form + type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", + mimeType: file.type + } +}); +What to check for in the rest of that file: +Variables: Ensure customMetadata is defined earlier in the function (usually via JSON.parse(formData.get("customMetadata") as string)). + +ParentId: Ensure parentId is correctly handled (e.g., parentIdRaw === "root" ? null : parentIdRaw). + +Final Verification +After you paste the actual object into that metadata field, run the check again: + +Bash +npx tsc --noEmit +If that was the only error, the command should now return zero errors. + +Is the "Expression expected" error gone now? If so, you've successfully completed the refactor and your project is 100% type-safe! Would you like to do a final check of the createFolderAction in that same file to ensure it also has the correct metadata? + +It now found 18 error when I reran it + +tephenlohning@Scotty 124_webcalibre2 % npx tsc --noEmit + +src/app/upload/_actions.ts:84:19 - error TS1109: Expression expected. + + + +84 metadata: { ... } + + ~ + + + + + +Found 1 error in src/app/upload/_actions.ts:84 + + + +stephenlohning@Scotty 124_webcalibre2 % npx tsc --noEmit + +prisma.config.ts:10:5 - error TS2322: Type 'string | undefined' is not assignable to type 'string'. + + Type 'undefined' is not assignable to type 'string'. + + + +10 url: process.env.DATABASE_URL, + + ~~~ + + + + node_modules/@prisma/config/dist/index.d.ts:499:5 + + 499 url: string; + + ~~~ + + The expected type comes from property 'url' which is declared here on type 'SchemaEngineConfigClassicDatasource' + + + +src/app/api/download/route.ts:41:34 - error TS2339: Property 'mimeType' does not exist on type '{ path: string; id: string; name: string; createdAt: Date; updatedAt: Date; size: bigint | null; isFolder: boolean; oneDriveId: string | null; orderIndex: number; metadata: JsonValue; description: string | null; ownerId: string; parentId: string | null; }'. + + + +41 'Content-Type': fileNode.mimeType || 'application/octet-stream', + + ~~~~~~~~ + + + +src/app/dashboard/dashboard-view.tsx:43:14 - error TS2322: Type '{ children: Element[]; sx: { p: number; display: string; justifyContent: string; alignItems: string; }; }' is not assignable to type 'IntrinsicAttributes & HTMLAttributes & { render?: RenderProp & Pick<...>> | undefined; } & RefAttributes<...>'. + + Property 'sx' does not exist on type 'IntrinsicAttributes & HTMLAttributes & { render?: RenderProp & Pick<...>> | undefined; } & RefAttributes<...>'. + + + +43 + + ~~ + + + +src/app/dashboard/dashboard-view.tsx:47:20 - error TS2322: Type '{ children: Element; sx: { display: string; alignItems: string; }; }' is not assignable to type 'IntrinsicAttributes & Omit, "className"> & { parser?: ((input: string) => any[]) | undefined; ... 6 more ...; onExpandedChange?: ((expanded: boolean) => void) | undefined; }'. + + Property 'sx' does not exist on type 'IntrinsicAttributes & Omit, "className"> & { parser?: ((input: string) => any[]) | undefined; ... 6 more ...; onExpandedChange?: ((expanded: boolean) => void) | undefined; }'. + + + +47 + + ~~ + + + +src/app/dashboard/dashboard-view.tsx:124:13 - error TS2552: Cannot find name 'deleteFileAction'. Did you mean 'deleteFileNodeAction'? + + + +124 await deleteFileAction(id); + + ~~~~~~~~~~~~~~~~ + + + +src/app/dashboard/upload.tsx:6:10 - error TS2305: Module '"./upload-actions"' has no exported member 'uploadFileToOneDrive'. + + + +6 import { uploadFileToOneDrive } from "./upload-actions"; + + ~~~~~~~~~~~~~~~~~~~~ + + + +src/app/profile/page.tsx:2:26 - error TS2307: Cannot find module '@/components/LogoutButton' or its corresponding type declarations. + + + +2 import LogoutButton from "@/components/LogoutButton"; + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + +src/app/update/[id]/update-view.tsx:119:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +119 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/app/update/[id]/update-view.tsx:130:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +130 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/app/update/[id]/update-view.tsx:141:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +141 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/app/upload/upload-view.tsx:211:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +211 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/app/upload/upload-view.tsx:217:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +217 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/app/upload/upload-view.tsx:223:18 - error TS2769: No overload matches this call. + + Overload 1 of 2, '(props: { component: ElementType; } & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps<...> & Omit<...>): Element | null', gave the following error. + + Property 'component' is missing in type '{ children: Element; item: true; xs: number; }' but required in type '{ component: ElementType; }'. + + Overload 2 of 2, '(props: DefaultComponentProps>): Element | null', gave the following error. + + Type '{ children: Element; item: true; xs: number; }' is not assignable to type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + Property 'item' does not exist on type 'IntrinsicAttributes & GridBaseProps & { sx?: SxProps | undefined; } & SystemProps & Omit<...>'. + + + +223 + + ~~~~ + + + + node_modules/@mui/types/esm/index.d.ts:64:5 + + 64 component: C; + + ~~~~~~~~~ + + 'component' is declared here. + + + +src/components/providers.tsx:8:8 - error TS2304: Cannot find name 'AppRouterCacheProvider'. + + + +8 + + ~~~~~~~~~~~~~~~~~~~~~~ + + + +src/components/providers.tsx:9:10 - error TS2304: Cannot find name 'ThemeProvider'. + + + +9 + + ~~~~~~~~~~~~~ + + + +src/components/providers.tsx:9:31 - error TS2304: Cannot find name 'theme'. + + + +9 + + ~~~~~ + + + +src/components/providers.tsx:11:11 - error TS2304: Cannot find name 'ThemeProvider'. + + + +11 + + ~~~~~~~~~~~~~ + + + +src/components/providers.tsx:12:9 - error TS2304: Cannot find name 'AppRouterCacheProvider'. + + + +12 + + ~~~~~~~~~~~~~~~~~~~~~~ + + + + + +Found 18 errors in 8 files. + + + +Errors Files + + 1 prisma.config.ts:10 + + 1 src/app/api/download/route.ts:41 + + 3 src/app/dashboard/dashboard-view.tsx:43 + + 1 src/app/dashboard/upload.tsx:6 + + 1 src/app/profile/page.tsx:2 + + 3 src/app/update/[id]/update-view.tsx:119 + + 3 src/app/upload/upload-view.tsx:211 + + 5 src/components/providers.tsx:8 + +This is normal during a major refactor! When we move logic into the DAL and Service Layers, the old "glue" in the UI components often snaps. + +Let's knock these out by category to get you back to zero errors. + +1. The "Prisma URL" Fix (prisma.config.ts) +TypeScript is worried that DATABASE_URL might be missing. Fix: Add a non-null assertion (!). + +TypeScript +// prisma.config.ts +url: process.env.DATABASE_URL!, +2. The "MimeType" Fix (src/app/api/download/route.ts) +The database stores mimeType inside a JSON field called metadata, not as a top-level column. Fix: Extract it from metadata. + +TypeScript +// src/app/api/download/route.ts +const metadata = fileNode.metadata as any; +const contentType = metadata?.mimeType || 'application/octet-stream'; + +return new NextResponse(graphResponse.body, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`, + }, +}); +3. The "MUI Grid & SX" Fix (dashboard-view.tsx, update-view.tsx, upload-view.tsx) +It looks like you've upgraded to MUI v6, which changed how Grid and some components work. Grid item is now just Grid. Fix: 1. Replace with . 2. For the Toolbar and QuickFilter errors, ensure you are importing them from @mui/material and not a third-party library. + +4. The "Renamed Action" Fix (dashboard-view.tsx) +As we suspected, the UI is looking for the old name. Fix: In src/app/dashboard/dashboard-view.tsx, find deleteFileAction and rename it to deleteFileNodeAction in both the import and the function call. + +5. The "Upload.tsx" Import Fix (src/app/dashboard/upload.tsx) +You renamed the action in the backend but didn't update this specific component. Fix: + +TypeScript +// Change this: +import { uploadFileToOneDrive } from "./upload-actions"; +// To this (the new refactored name): +import { uploadFileAction } from "./upload-actions"; +6. The "Providers" & "Logout" Fix (src/components/providers.tsx) +This happens if imports were moved or deleted during the shuffle. Fix: Ensure your providers.tsx has these imports at the top: + +TypeScript +import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; +import { ThemeProvider } from '@mui/material/styles'; +import { theme } from '@/lib/theme'; +(And for the Profile page, check if LogoutButton.tsx exists in src/components/ or if it was accidentally renamed). + +Action Plan +I recommend fixing the API Download route and the Renamed Actions first, as those are the core of your new architecture. The MUI errors (Grid size, SX) are styling-related but must be fixed for the build to pass. + +Would you like me to provide the full, corrected code for one of the MUI View files (like dashboard-view.tsx) so you can see the v6 Grid syntax? diff --git a/docs/Ai-converstion.pdf b/docs/Ai-converstion.pdf index b4f963f..7f3ca86 100644 Binary files a/docs/Ai-converstion.pdf and b/docs/Ai-converstion.pdf differ diff --git a/package-lock.json b/package-lock.json index 734993c..d737edb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,8 @@ "next-auth": "^5.0.0-beta.30", "pg": "^8.16.3", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "server-only": "^0.0.1" }, "devDependencies": { "@types/node": "^20", @@ -32,7 +33,7 @@ "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "16.1.1", - "prisma": "^7.2.0", + "prisma": "^6.19.2", "typescript": "^5" } }, @@ -345,73 +346,6 @@ "node": ">=6.9.0" } }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", - "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", - "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/types": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", - "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", - "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@electric-sql/pglite": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", - "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@electric-sql/pglite-socket": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz", - "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "pglite-server": "dist/scripts/server.js" - }, - "peerDependencies": { - "@electric-sql/pglite": "0.3.2" - } - }, - "node_modules/@electric-sql/pglite-tools": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz", - "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==", - "devOptional": true, - "license": "Apache-2.0", - "peerDependencies": { - "@electric-sql/pglite": "0.3.2" - } - }, "node_modules/@emnapi/core": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.0.tgz", @@ -741,19 +675,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@hono/node-server": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.6.tgz", - "integrity": "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1318,20 +1239,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mrleebo/prisma-ast": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz", - "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chevrotain": "^10.5.0", - "lilconfig": "^2.1.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz", @@ -1960,9 +1867,9 @@ "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.2.0.tgz", - "integrity": "sha512-qmvSnfQ6l/srBW1S7RZGfjTQhc44Yl3ldvU6y3pgmuLM+83SBDs6UQVgMtQuMRe9J3gGqB0RF8wER6RlXEr6jQ==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -1978,32 +1885,6 @@ "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", "license": "Apache-2.0" }, - "node_modules/@prisma/dev": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.17.0.tgz", - "integrity": "sha512-6sGebe5jxX+FEsQTpjHLzvOGPn6ypFQprcs3jcuIWv1Xp/5v6P/rjfdvAwTkP2iF6pDx2tCd8vGLNWcsWzImTA==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "@electric-sql/pglite": "0.3.2", - "@electric-sql/pglite-socket": "0.0.6", - "@electric-sql/pglite-tools": "0.2.7", - "@hono/node-server": "1.19.6", - "@mrleebo/prisma-ast": "0.12.1", - "@prisma/get-platform": "6.8.2", - "@prisma/query-plan-executor": "6.18.0", - "foreground-child": "3.3.1", - "get-port-please": "3.1.2", - "hono": "4.10.6", - "http-status-codes": "2.3.0", - "pathe": "2.0.3", - "proper-lockfile": "4.1.2", - "remeda": "2.21.3", - "std-env": "3.9.0", - "valibot": "1.2.0", - "zeptomatch": "2.0.2" - } - }, "node_modules/@prisma/driver-adapter-utils": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz", @@ -2014,94 +1895,69 @@ } }, "node_modules/@prisma/engines": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.2.0.tgz", - "integrity": "sha512-HUeOI/SvCDsHrR9QZn24cxxZcujOjcS3w1oW/XVhnSATAli5SRMOfp/WkG3TtT5rCxDA4xOnlJkW7xkho4nURA==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0", - "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "@prisma/fetch-engine": "7.2.0", - "@prisma/get-platform": "7.2.0" + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" } }, "node_modules/@prisma/engines-version": { - "version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3.tgz", - "integrity": "sha512-KezsjCZDsbjNR7SzIiVlUsn9PnLePI7r5uxABlwL+xoerurZTfgQVbIjvjF2sVr3Uc0ZcsnREw3F84HvbggGdA==", + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "node_modules/@prisma/engines/node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.2.0" - } + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.2.0.tgz", - "integrity": "sha512-Z5XZztJ8Ap+wovpjPD2lQKnB8nWFGNouCrglaNFjxIWAGWz0oeHXwUJRiclIoSSXN/ptcs9/behptSk8d0Yy6w==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0", - "@prisma/engines-version": "7.2.0-4.0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", - "@prisma/get-platform": "7.2.0" + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" } }, - "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.2.0" - } + "license": "Apache-2.0" }, "node_modules/@prisma/get-platform": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", - "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.8.2" + "@prisma/debug": "6.19.2" } }, "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", - "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/query-plan-executor": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz", - "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/studio-core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.9.0.tgz", - "integrity": "sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g==", - "devOptional": true, - "license": "Apache-2.0", - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3023,16 +2879,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/axe-core": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", @@ -3280,21 +3126,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chevrotain": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", - "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -3407,7 +3238,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3562,16 +3393,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -4529,23 +4350,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4586,16 +4390,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4641,13 +4435,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port-please": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", - "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", - "devOptional": true, - "license": "MIT" - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4767,20 +4554,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/grammex": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", - "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4900,40 +4673,6 @@ "react-is": "^16.7.0" } }, - "node_modules/hono": { - "version": "4.10.6", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.6.tgz", - "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5260,13 +4999,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, - "license": "MIT" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5423,7 +5155,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -5594,16 +5326,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5626,13 +5348,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5640,13 +5355,6 @@ "dev": true, "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, - "license": "Apache-2.0" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5669,22 +5377,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lru.min": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", - "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5748,40 +5440,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6187,7 +5845,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6389,20 +6047,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postgres": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", - "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, - "license": "Unlicense", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" - } - }, "node_modules/postgres-array": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", @@ -6472,34 +6116,26 @@ } }, "node_modules/prisma": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.2.0.tgz", - "integrity": "sha512-jSdHWgWOgFF24+nRyyNRVBIgGDQEsMEF8KPHvhBBg3jWyR9fUAK0Nq9ThUmiGlNgq2FA7vSk/ZoCvefod+a8qg==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.2.0", - "@prisma/dev": "0.17.0", - "@prisma/engines": "7.2.0", - "@prisma/studio-core": "0.9.0", - "mysql2": "3.15.3", - "postgres": "3.4.7" + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": "^20.19 || ^22.12 || >=24.0" + "node": ">=18.18" }, "peerDependencies": { - "better-sqlite3": ">=9.0.0", - "typescript": ">=5.4.0" + "typescript": ">=5.1.0" }, "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, "typescript": { "optional": true } @@ -6516,25 +6152,6 @@ "react-is": "^16.13.1" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, - "license": "ISC" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6674,13 +6291,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "devOptional": true, - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6702,16 +6312,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/remeda": { - "version": "2.21.3", - "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz", - "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "type-fest": "^4.39.1" - } - }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -6757,16 +6357,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6857,13 +6447,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -6880,11 +6463,11 @@ "semver": "bin/semver.js" } }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -6997,7 +6580,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7010,7 +6593,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7092,19 +6675,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7132,16 +6702,6 @@ "node": ">= 10.x" } }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7149,13 +6709,6 @@ "dev": true, "license": "MIT" }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "devOptional": true, - "license": "MIT" - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -7489,19 +7042,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "devOptional": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7729,26 +7269,11 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/valibot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", - "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7897,16 +7422,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zeptomatch": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", - "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "grammex": "^3.1.10" - } - }, "node_modules/zod": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz", diff --git a/package.json b/package.json index e60e9a8..78a931c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "next-auth": "^5.0.0-beta.30", "pg": "^8.16.3", "react": "19.2.3", - "react-dom": "19.2.3" + "react-dom": "19.2.3", + "server-only": "^0.0.1" }, "devDependencies": { "@types/node": "^20", @@ -36,7 +37,7 @@ "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "16.1.1", - "prisma": "^7.2.0", + "prisma": "^6.19.2", "typescript": "^5" } } diff --git a/prisma.config.ts b/prisma.config.ts index 1173c5d..999e934 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ schema: "prisma/schema.prisma", datasource: { // This is now the ONLY place where the DB connection string is defined - url: process.env.DATABASE_URL, + // prisma.config.ts + url: process.env.DATABASE_URL!, }, }); \ No newline at end of file diff --git a/src/app/api/download/route.ts b/src/app/api/download/route.ts index dbdec41..b12a156 100644 --- a/src/app/api/download/route.ts +++ b/src/app/api/download/route.ts @@ -1,66 +1,57 @@ // src/app/api/download/route.ts -"use server" import { NextRequest, NextResponse } from 'next/server'; -import { auth } from "@/auth"; // Import the auth function from your central config -import { prisma } from '@/lib/prisma'; +import { auth } from "@/auth"; +import { getFileNodeById } from "@/data-access/file-nodes"; +import { getOneDriveContentStream } from "@/services/onedrive"; export async function GET(request: NextRequest) { try { - // 1. Check Authentication using the V5 auth() helper + // 1. Authenticate the user session const session = await auth(); - - // In V5, tokens are usually handled in the session callback - if (!session || !session.accessToken) { - return new NextResponse("Unauthorized - No Access Token found", { status: 401 }); + if (!session?.user?.id) { + return new NextResponse("Unauthorized", { status: 401 }); } - // 2. Get parameters from URL + // 2. Extract parameters from URL const { searchParams } = new URL(request.url); - const fileNodeId = searchParams.get('id'); + const fileId = searchParams.get('id'); const mode = searchParams.get('mode') === 'inline' ? 'inline' : 'attachment'; - if (!fileNodeId) { + if (!fileId) { return new NextResponse("File ID is required", { status: 400 }); } - // 3. Find the file in your Postgres FileNode table - const fileNode = await prisma.fileNode.findUnique({ - where: { id: fileNodeId } - }); + // 3. DAL: Fetch file metadata from local database + const fileNode = await getFileNodeById(fileId); if (!fileNode || !fileNode.oneDriveId) { - return new NextResponse("File not found in database", { status: 404 }); + return new NextResponse("File not found", { status: 404 }); } - // 4. Fetch the file stream from Microsoft Graph - const graphResponse = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${fileNode.oneDriveId}/content`, - { - headers: { - Authorization: `Bearer ${session.accessToken}`, - }, - } - ); - - if (!graphResponse.ok) { - const errorText = await graphResponse.text(); - console.error('MS Graph Error:', errorText); - return new NextResponse(`OneDrive Error: ${graphResponse.statusText}`, { status: graphResponse.status }); - } + // 4. SERVICE: Get the binary stream from Microsoft Graph + // The service layer automatically handles the 'getFreshAccessToken' logic + const graphResponse = await getOneDriveContentStream(session.user.id, fileNode.oneDriveId); // 5. Stream the response directly to the client - return new NextResponse(graphResponse.body, { - status: 200, - headers: { - 'Content-Type': fileNode.mimeType || 'application/octet-stream', - // Note: Using encodeURIComponent for filename to handle special characters - 'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`, - }, - }); + // We pass the graphResponse.body (ReadableStream) directly to NextResponse + // src/app/api/download/route.ts +const metadata = fileNode.metadata as any; +const contentType = metadata?.mimeType || 'application/octet-stream'; - } catch (error) { - console.error('Download error:', error); - return new NextResponse("Internal Server Error", { status: 500 }); +return new NextResponse(graphResponse.body, { + status: 200, + headers: { + 'Content-Type': contentType, + 'Content-Disposition': `${mode}; filename="${encodeURIComponent(fileNode.name)}"`, + }, +}); + + } catch (error: any) { + console.error('Download Route Error:', error); + return new NextResponse( + JSON.stringify({ error: "Internal Server Error", message: error.message }), + { status: 500 } + ); } } \ No newline at end of file diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts index 38eea6f..db8b54c 100644 --- a/src/app/dashboard/actions.ts +++ b/src/app/dashboard/actions.ts @@ -1,138 +1,82 @@ +// src/app/dashboard/actions.ts + 'use server'; import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; -import { getFreshAccessToken } from "@/lib/auth-utils"; +import { + getAllFileNodes, + getFileNodeById, + updateFileNode, + deleteFileNode +} from "@/data-access/file-nodes"; +import { + getOneDriveItem, + deleteFromOneDrive, + uploadToOneDrive +} from "@/services/onedrive"; /** - * 1. FETCH: Get all file nodes for the Dashboard + * 1. FETCH: Get all file nodes + * Now simply calls the DAL. Error handling is left to the caller (the UI). */ export async function getFileNodes() { - try { - const nodes = await prisma.fileNode.findMany({ - orderBy: { - updatedAt: 'desc', - }, - }); - return nodes; - } catch (error) { - console.error("Error fetching file nodes:", error); - return []; - } + return await getAllFileNodes(); } /** * 2. DOWNLOAD: Generates the authenticated OneDrive URL + * Orchestrates the session check, DAL lookup, and Service call. */ export async function getDownloadUrlAction(id: string) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - const file = await prisma.fileNode.findUnique({ where: { id } }); + const file = await getFileNodeById(id); if (!file || !file.oneDriveId) throw new Error("File not found or missing cloud ID"); - const accessToken = await getFreshAccessToken(session.user.id); - const res = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${file.oneDriveId}`, - { headers: { Authorization: `Bearer ${accessToken}` } } - ); - - if (!res.ok) throw new Error("Failed to contact OneDrive"); - - const data = await res.json(); + // Service handles token refresh and graph request internally + const data = await getOneDriveItem(session.user.id, file.oneDriveId); const downloadUrl = data["@microsoft.graph.downloadUrl"]; - if (!downloadUrl) throw new Error("OneDrive did not provide a download link"); - - return { downloadUrl }; + if (!downloadUrl) throw new Error("OneDrive did not provide a download URL"); + return downloadUrl; } /** - * 3. DELETE: Remove from OneDrive (via ID) and Database - * Folders are virtual (DB only), so cloud deletion is skipped if oneDriveId is null. + * 3. DELETE: Removes from both Cloud and Database */ -export async function deleteFileAction(fileId: string) { +export async function deleteFileNodeAction(id: string) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - const node = await prisma.fileNode.findUnique({ - where: { id: fileId }, - }); - - if (!node) { - revalidatePath("/dashboard"); - return { success: true }; - } - - // @ts-ignore - const isAdmin = session.user.role === "ADMIN"; - const isOwner = node.ownerId === session.user.id; - - if (!isAdmin && !isOwner) { - throw new Error("Permission Denied."); - } - try { - const accessToken = await getFreshAccessToken(session.user.id); - - // Only attempt cloud deletion if it's a file/storage with a oneDriveId. - // Virtual folders created in the DB have no oneDriveId and are skipped. - if (accessToken && node.oneDriveId) { - const onedriveRes = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}`, - { - method: "DELETE", - headers: { Authorization: `Bearer ${accessToken}` }, - } - ); + const file = await getFileNodeById(id); + if (!file) throw new Error("File record not found"); - if (!onedriveRes.ok && onedriveRes.status !== 404) { - console.warn("OneDrive Deletion Warning: Cloud record might still exist."); - } + // Phase 1: Cloud Deletion + if (file.oneDriveId) { + await deleteFromOneDrive(session.user.id, file.oneDriveId); } - } catch (cloudError) { - console.error("Cloud cleanup failed:", cloudError); - } - try { - await prisma.fileNode.delete({ where: { id: fileId } }); - revalidatePath("/dashboard"); - revalidatePath("/upload"); - return { success: true }; - } catch (dbError) { - throw new Error("Failed to remove the record from the database."); - } -} + // Phase 2: Database Deletion + await deleteFileNode(id); -/** - * 4. MOVE: Assign file to folder or folder to another folder (Virtual Move) - */ -export async function moveNodeAction(nodeId: string, newParentId: string | null) { - const session = await auth(); - if (!session?.user?.id) throw new Error("Unauthorized"); - if (nodeId === newParentId) throw new Error("Cannot move to self."); - - try { - await prisma.fileNode.update({ - where: { id: nodeId }, - data: { parentId: newParentId } - }); revalidatePath("/dashboard"); return { success: true }; } catch (error) { - throw new Error("Move failed."); + console.error("Delete Error:", error); + return { success: false, error: "Failed to delete file" }; } } /** - * 5. UPDATE & REPLACE: Full update of metadata and OneDrive content + * 4. UPDATE: Modify record and optionally sync new content to OneDrive */ -export async function updateFileFullAction(formData: FormData) { +export async function updateFileNodeAction(id: string, formData: FormData) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - const id = formData.get("id") as string; const name = formData.get("name") as string; const description = formData.get("description") as string; const parentIdRaw = formData.get("parentId") as string; @@ -143,44 +87,29 @@ export async function updateFileFullAction(formData: FormData) { let metadata = JSON.parse(metadataStr); try { - const accessToken = await getFreshAccessToken(session.user.id); - const node = await prisma.fileNode.findUnique({ where: { id } }); + const node = await getFileNodeById(id); - // Update physical file content only if a new file is uploaded and we have a target oneDriveId + // If a new file is uploaded, push it to OneDrive first if (newFile && newFile.size > 0 && node?.oneDriveId) { - const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/items/${node.oneDriveId}/content`; - - const uploadRes = await fetch(onedrivePath, { - method: "PUT", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": newFile.type - }, - body: Buffer.from(await newFile.arrayBuffer()), - }); - - if (!uploadRes.ok) throw new Error("OneDrive content update failed"); + await uploadToOneDrive(session.user.id, newFile, node.oneDriveId); metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; metadata.mimeType = newFile.type; } - await prisma.fileNode.update({ - where: { id }, - data: { - name, - description, - parentId, - metadata, - size: newFile ? BigInt(newFile.size) : undefined, - updatedAt: new Date(), - } + // Update the database via DAL + await updateFileNode(id, { + name, + description, + parentId, + metadata, + size: newFile ? BigInt(newFile.size) : undefined, }); revalidatePath("/dashboard"); return { success: true }; - } catch (error: any) { - console.error("Full Update Failure:", error); - throw new Error(error.message || "Failed to update record."); + } catch (error) { + console.error("Update Error:", error); + return { success: false, error: "Failed to update record" }; } } \ No newline at end of file diff --git a/src/app/dashboard/dashboard-view.tsx b/src/app/dashboard/dashboard-view.tsx index 0807ddb..ff5ad7e 100644 --- a/src/app/dashboard/dashboard-view.tsx +++ b/src/app/dashboard/dashboard-view.tsx @@ -1,7 +1,5 @@ 'use client'; -// src/app/dashboard/dashboard-view.tsx - import { useState } from "react"; import { Button, @@ -13,15 +11,15 @@ import { Stack, TextField, InputAdornment, - Tooltip + Tooltip, } from "@mui/material"; import { DataGrid, GridColDef, - Toolbar, - QuickFilter, - QuickFilterControl, - QuickFilterClear, + GridToolbarContainer, + // Using the new non-deprecated QuickFilter components + QuickFilter, + QuickFilterControl, } from "@mui/x-data-grid"; import SyncIcon from "@mui/icons-material/Sync"; import RefreshIcon from "@mui/icons-material/Refresh"; @@ -30,51 +28,47 @@ import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import SearchIcon from '@mui/icons-material/Search'; -import CancelIcon from '@mui/icons-material/Cancel'; import DownloadIcon from '@mui/icons-material/Download'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { syncOneDrive } from "./sync-actions"; -import { deleteFileAction } from "./actions"; +import { deleteFileNodeAction } from "./actions"; import { useRouter } from "next/navigation"; +/** + * UPDATED TOOLBAR: Uses the new QuickFilter structure to avoid deprecation + */ function CustomToolbar() { return ( - + Library - - ( - - - - ), - endAdornment: state.value ? ( - - - - - - ) : null, - }, - }} - /> - )} - /> - - + + + ( + + + + ), + }, + }} + /> + )} + /> + + + ); } @@ -121,21 +115,18 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps const handleDelete = async (id: string, name: string) => { if (!confirm(`Are you sure you want to delete "${name}"?`)) return; try { - await deleteFileAction(id); + await deleteFileNodeAction(id); router.refresh(); } catch (error: any) { alert(error.message || "Failed to delete file"); } }; - // --- NEW DOWNLOAD FUNCTIONS --- const handleDownload = (id: string) => { - // Triggers local folder download via Content-Disposition: attachment window.location.href = `/api/download?id=${id}&mode=attachment`; }; const handleViewInTab = (id: string) => { - // Opens in a new tab via Content-Disposition: inline window.open(`/api/download?id=${id}&mode=inline`, '_blank'); }; @@ -146,7 +137,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps flex: 1.5, minWidth: 250, renderCell: (params) => ( - + {params.row.isFolder ? : } {params.value} @@ -179,14 +170,14 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps { field: "actions", headerName: "Actions", - width: 180, // Increased width to accommodate new buttons + width: 180, align: 'right', renderCell: (params) => { const isOwner = params.row.ownerId === user?.id; const isFolder = params.row.isFolder; return ( - + {!isFolder && ( <> @@ -227,28 +218,25 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps ); } - }, - { - field: "metadata_search", - headerName: "Metadata Search", - width: 0, - valueGetter: (value, row) => { - if (!row.metadata) return ""; - return Object.entries(row.metadata) - .filter(([k]) => k !== 'type' && k !== 'mimeType') - .map(([k, v]) => `${k}:${v}`) - .join(" "); - } } ]; return ( - - - - @@ -258,19 +246,11 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps rows={initialFiles} columns={columns} slots={{ toolbar: CustomToolbar }} - showToolbar disableRowSelectionOnClick - initialState={{ - columns: { - columnVisibilityModel: { - metadata_search: false, - }, - }, - }} sx={{ border: 'none', - '& .MuiDataGrid-columnHeaders': { bgcolor: '#f8f9fa' }, - '& .MuiDataGrid-toolbarContainer': { borderBottom: '1px solid #eee' } + '& .MuiDataGrid-columnHeader': { bgcolor: '#f8f9fa' }, + '& .MuiDataGrid-footerContainer': { borderTop: '1px solid #eee' } }} /> diff --git a/src/app/dashboard/sync-actions.ts b/src/app/dashboard/sync-actions.ts index afcebfc..11fdfe8 100644 --- a/src/app/dashboard/sync-actions.ts +++ b/src/app/dashboard/sync-actions.ts @@ -1,62 +1,49 @@ +// src/app/dashboard/sync-actions.ts 'use server'; + import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; -import { getFreshAccessToken } from "@/lib/auth-utils"; +import { upsertFileNode } from "@/data-access/file-nodes"; +import { getWebCalibreChildren } from "@/services/onedrive"; +// src/app/dashboard/sync-actions.ts +//import { upsertFileNode } from "@/data-access/file-nodes"; // Change this from /services/onedrive export async function syncOneDrive() { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); try { - const accessToken = await getFreshAccessToken(session.user.id); - const response = await fetch("https://graph.microsoft.com/v1.0/me/drive/root:/WebCalibre:/children", { - headers: { Authorization: `Bearer ${accessToken}` }, - }); + // 1. Call Service to get cloud data (token refresh handled inside service) + const items = await getWebCalibreChildren(session.user.id); - if (!response.ok) return { success: true, count: 0 }; - - const data = await response.json(); let syncedCount = 0; const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - for (const item of data.value) { + for (const item of items) { const isFolder = !!item.folder; - // Only skip if it's a folder AND it's a UUID (storage container) - // If a user named a file with a UUID, we still want it. - if (isFolder && uuidRegex.test(item.name)) { - continue; - } + // Business Logic: Skip UUID storage folders + if (isFolder && uuidRegex.test(item.name)) continue; const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); - await prisma.fileNode.upsert({ - where: { oneDriveId: item.id }, // Primary match - update: { - name: item.name, - size: BigInt(item.size || 0), - isFolder: isFolder, - path: item.parentReference?.path + '/' + item.name, - updatedAt: new Date(), - }, - create: { - id: crypto.randomUUID(), - oneDriveId: item.id, - name: item.name, - size: BigInt(item.size || 0), - isFolder: isFolder, - path: item.parentReference?.path + '/' + item.name, - ownerId: session.user.id, - metadata: { type: extension, mimeType: item.file?.mimeType || null }, - } + // 2. Call DAL to save to database + await upsertFileNode(item.id, { + name: item.name, + size: BigInt(item.size || 0), + isFolder: isFolder, + path: item.parentReference?.path + '/' + item.name, + ownerId: session.user.id, + metadata: { type: extension, mimeType: item.file?.mimeType || null }, }); + syncedCount++; } revalidatePath('/dashboard'); return { success: true, count: syncedCount }; } catch (error: any) { - throw new Error(error.message); + console.error("Sync Error:", error.message); + throw new Error("Failed to sync with OneDrive"); } } \ No newline at end of file diff --git a/src/app/dashboard/upload-actions.ts b/src/app/dashboard/upload-actions.ts index 743ff88..3cf6f45 100644 --- a/src/app/dashboard/upload-actions.ts +++ b/src/app/dashboard/upload-actions.ts @@ -1,9 +1,9 @@ 'use server'; import { auth } from "@/auth"; -import { getFreshAccessToken } from "@/lib/auth-utils"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { createFileNode } from "@/data-access/file-nodes"; +import { ensureOneDriveFolder, uploadLargeFile } from "@/services/onedrive"; export async function uploadFileAction(formData: FormData) { const session = await auth(); @@ -11,58 +11,32 @@ export async function uploadFileAction(formData: FormData) { const file = formData.get("file") as File; const folderName = "WebCalibre"; - const accessToken = await getFreshAccessToken(session.user.id); - // 1. Create/Check WebCalibre Folder - const folderPath = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}`; - const folderCheck = await fetch(folderPath, { - headers: { Authorization: `Bearer ${accessToken}` } - }); + try { + // 1. Logic: Ensure destination exists + await ensureOneDriveFolder(session.user.id, folderName); - if (folderCheck.status === 404) { - await fetch(`https://graph.microsoft.com/v1.0/me/drive/root/children`, { - method: "POST", - headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, - body: JSON.stringify({ name: folderName, folder: {} }) - }); - } + // 2. Logic: Perform the cloud upload + const driveItem = await uploadLargeFile(session.user.id, file, folderName); - // 2. Create Upload Session (Supports files > 4MB) - const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/root:/${folderName}/${file.name}:/createUploadSession`; - const sessionRes = await fetch(sessionUrl, { - method: "POST", - headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, - body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } }) - }); - - const { uploadUrl } = await sessionRes.json(); - - // 3. Upload File Data - const buffer = Buffer.from(await file.arrayBuffer()); - const uploadRes = await fetch(uploadUrl, { - method: "PUT", - headers: { - "Content-Length": `${file.size}`, - "Content-Range": `bytes 0-${file.size - 1}/${file.size}` - }, - body: buffer - }); - - const driveItem = await uploadRes.json(); - - // 4. Record in PostgreSQL - await prisma.fileNode.create({ - data: { + // 3. Logic: Save the result to our DB + await createFileNode({ oneDriveId: driveItem.id, name: file.name, size: BigInt(file.size), isFolder: false, path: `/${folderName}/${file.name}`, ownerId: session.user.id, - metadata: { type: file.name.split('.').pop()?.toUpperCase() } - } - }); + metadata: { + type: file.name.split('.').pop()?.toUpperCase(), + mimeType: file.type + } + }); - revalidatePath("/dashboard"); - return { success: true }; + revalidatePath("/dashboard"); + return { success: true }; + } catch (error: any) { + console.error("Upload Action Error:", error); + return { success: false, error: error.message || "Upload failed" }; + } } \ No newline at end of file diff --git a/src/app/dashboard/upload.tsx b/src/app/dashboard/upload.tsx index 445ba73..8ac33bd 100644 --- a/src/app/dashboard/upload.tsx +++ b/src/app/dashboard/upload.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { Button, Typography, Box, LinearProgress } from "@mui/material"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import { uploadFileToOneDrive } from "./upload-actions"; +import { uploadFileAction } from "./upload-actions"; export default function UploadForm() { const [uploading, setUploading] = useState(false); @@ -14,7 +14,7 @@ export default function UploadForm() { setUploading(true); try { - await uploadFileToOneDrive(formData); + await uploadFileAction(formData); alert("Uploaded successfully!"); } catch (error) { console.error(error); diff --git a/src/app/settings/actions.ts b/src/app/settings/actions.ts index cfc6115..fc53c3e 100644 --- a/src/app/settings/actions.ts +++ b/src/app/settings/actions.ts @@ -1,15 +1,13 @@ 'use server'; +// src/app/settings/actions.ts + import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { getUserByEmail, getUserById, updateUserRole } from "@/data-access/users"; /** - * Toggles a user's role between 'ADMIN' and 'USER'. - * * Security Logic: - * 1. Checks if the caller is the Bootstrap Admin (via .env). - * 2. Checks if the caller has the 'ADMIN' role in the database. - * 3. Prevents the Bootstrap Admin from being demoted to 'USER'. + * Toggles a user's role between 'ADMIN' and 'USER' using the DAL pattern. */ export async function toggleUserRoleAction(targetUserId: string) { const session = await auth(); @@ -19,46 +17,34 @@ export async function toggleUserRoleAction(targetUserId: string) { throw new Error("Unauthorized: No session found."); } - // 1. Authorization: Who is trying to change the role? + // 1. Authorization: Verify caller's permissions via DAL const isBootstrap = callerEmail === process.env.INITIAL_ADMIN_EMAIL; - - const callerDbRecord = await prisma.user.findUnique({ - where: { email: callerEmail }, - select: { role: true } - }); - + const callerDbRecord = await getUserByEmail(callerEmail); const isAdmin = isBootstrap || callerDbRecord?.role === "ADMIN"; if (!isAdmin) { throw new Error("Forbidden: You do not have permission to manage roles."); } - // 2. Fetch the target user to be modified - const targetUser = await prisma.user.findUnique({ - where: { id: targetUserId }, - select: { id: true, email: true, role: true } - }); + // 2. Fetch target user via DAL + const targetUser = await getUserById(targetUserId); if (!targetUser) { throw new Error("User not found."); } // 3. Protection: Prevent demoting the primary bootstrap admin - // This ensures you don't accidentally lock yourself out of the settings page. if (targetUser.email === process.env.INITIAL_ADMIN_EMAIL && targetUser.role === "ADMIN") { throw new Error("Security Restriction: The primary Bootstrap Admin role cannot be removed."); } - // 4. Determine new role + // 4. Logic: Determine new role const newRole = targetUser.role === "ADMIN" ? "USER" : "ADMIN"; - // 5. Execute Update - await prisma.user.update({ - where: { id: targetUserId }, - data: { role: newRole } - }); + // 5. Execute Update via DAL + await updateUserRole(targetUserId, newRole); - // 6. Refresh the data on the Settings page + // 6. UI Invalidation revalidatePath("/settings"); return { diff --git a/src/app/update/[id]/_actions.ts b/src/app/update/[id]/_actions.ts index 59a0890..59060f2 100644 --- a/src/app/update/[id]/_actions.ts +++ b/src/app/update/[id]/_actions.ts @@ -1,15 +1,23 @@ 'use server'; -//src/app/update/[id]/_actions.ts +// src/app/update/[id]/_actions.ts import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { getFileNodeById, updateFileNode } from "@/data-access/file-nodes"; +/** + * SERVER ACTION: Updates file metadata and organizational data. + * This refactored version uses the Data Access Layer (DAL) to + * ensure separation of concerns. + */ export async function updateFileAction(formData: FormData) { const session = await auth(); + + // 1. Authorization Guard if (!session?.user?.id) throw new Error("Unauthorized"); + // 2. Data Extraction const id = formData.get("id") as string; const name = formData.get("name") as string; const description = formData.get("description") as string; @@ -20,30 +28,39 @@ export async function updateFileAction(formData: FormData) { const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; try { - // 1. Get existing record to preserve system metadata (like mimeType) - const existing = await prisma.fileNode.findUnique({ where: { id } }); - const existingMetadata = (existing?.metadata as Record) || {}; + // 3. DAL: Fetch existing record to safely merge metadata + // This replaces the direct prisma.fileNode.findUnique call + const existing = await getFileNodeById(id); + if (!existing) throw new Error("File record not found"); - // 2. Update the record - await prisma.fileNode.update({ - where: { id }, - data: { - name, - description, - parentId, - metadata: { - ...customMetadata, // User's new keys - type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", - mimeType: existingMetadata.mimeType // Preserve the original mimeType - } - } + const existingMetadata = (existing.metadata as Record) || {}; + + // 4. Logic: Prepare the updated metadata object + const updatedMetadata = { + ...customMetadata, // Apply new user keys + type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", + mimeType: existingMetadata.mimeType // Ensure system metadata isn't overwritten + }; + + // 5. DAL: Perform the update + // This replaces the direct prisma.fileNode.update call + await updateFileNode(id, { + name, + description, + parentId, + metadata: updatedMetadata, }); + // 6. Cache Invalidation revalidatePath("/dashboard"); revalidatePath(`/update/${id}`); + return { success: true }; } catch (error: any) { - console.error("Update error:", error); - return { success: false, message: error.message }; + console.error("Update action error:", error); + return { + success: false, + message: error.message || "An unexpected error occurred during update" + }; } } \ No newline at end of file diff --git a/src/app/update/[id]/update-view.tsx b/src/app/update/[id]/update-view.tsx index a3d9007..0899fc2 100644 --- a/src/app/update/[id]/update-view.tsx +++ b/src/app/update/[id]/update-view.tsx @@ -116,7 +116,7 @@ export default function UpdateView({ fileNode, folders }: any) { {customMetadata.map((row, index) => ( - + - + - + setCustomMetadata(customMetadata.filter((_, i) => i !== index))}> diff --git a/src/app/upload/_actions.ts b/src/app/upload/_actions.ts index ef11360..8de09f0 100644 --- a/src/app/upload/_actions.ts +++ b/src/app/upload/_actions.ts @@ -1,14 +1,16 @@ 'use server'; +// src/app/upload/_actions.ts + import { auth } from "@/auth"; -import { getFreshAccessToken } from "@/lib/auth-utils"; -import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { createFileNode } from "@/data-access/file-nodes"; +//import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; + +import { createOneDriveFolder, uploadToFolderId, ensureOneDriveFolder } from "@/services/onedrive"; /** - * 1. CREATE FOLDER: Virtual Only - * Logic: User-created organizational folders exist ONLY in the database. - * No call to OneDrive is made here. + * 1. CREATE VIRTUAL FOLDER */ export async function createFolderAction(name: string, parentId?: string | null) { const session = await auth(); @@ -16,34 +18,26 @@ export async function createFolderAction(name: string, parentId?: string | null) try { const internalId = crypto.randomUUID(); - - const newNode = await prisma.fileNode.create({ - data: { - id: internalId, - oneDriveId: null, // Virtual folders do not have a cloud ID - name: name, - isFolder: true, - path: `virtual:/${name}`, - ownerId: session.user.id, - parentId: parentId || null, - metadata: { type: "FOLDER" } - } + // Swapped createNode for createFileNode + const newNode = await createFileNode({ + id: internalId, + oneDriveId: null, + name, + isFolder: true, + path: `virtual:/${name}`, + ownerId: session.user.id, + parentId: parentId || null, + metadata: { type: "FOLDER" } }); - revalidatePath("/upload"); revalidatePath("/dashboard"); - return { success: true, node: newNode }; } catch (error: any) { - console.error("Folder creation error:", error); throw new Error(error.message || "Failed to create virtual folder"); } } - /** - * 2. UPLOAD FILE: Physical Container - * Logic: Creates a physical folder (UUID) on OneDrive to hold the file. - * This ensures every file has a unique storage space in the cloud. + * 2. UPLOAD FILE (Physical UUID Folder) */ export async function uploadFileAction(formData: FormData) { const session = await auth(); @@ -59,77 +53,48 @@ export async function uploadFileAction(formData: FormData) { if (!file) throw new Error("No file selected"); - const accessToken = await getFreshAccessToken(session.user.id); const rootFolder = "WebCalibre"; - const internalId = crypto.randomUUID(); // This UUID will be the OneDrive folder name + const internalId = crypto.randomUUID(); // Used for both DB ID and OneDrive Folder Name - // 1. Create the Physical Storage Folder on OneDrive - const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { - method: "POST", - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json" - }, - body: JSON.stringify({ - name: internalId, - folder: {}, - "@microsoft.graph.conflictBehavior": "fail" - }) - }); + try { + // A. Ensure root exists + await ensureOneDriveFolder(session.user.id, rootFolder); - if (!createSubFolderRes.ok) { - const errorData = await createSubFolderRes.json(); - throw new Error(errorData.error?.message || "Storage directory creation failed"); + // B. Create the physical UUID folder on OneDrive + const subFolderRes = await createOneDriveFolder(session.user.id, rootFolder, internalId); + const subFolderData = await subFolderRes.json(); + + // C. Upload the file binary into that specific folder + const uploadedFileData = await uploadToFolderId(session.user.id, file, subFolderData.id); + + // D. Create record in Database + // src/app/upload/_actions.ts + +// ... inside uploadFileAction or createFolderAction ... +// ... inside uploadFileAction after OneDrive work is done ... + +await createFileNode({ + id: internalId, + oneDriveId: uploadedFileData.id, + name: file.name, + description: description, + size: BigInt(file.size), + isFolder: false, + path: `/${rootFolder}/${internalId}/${file.name}`, + ownerId: session.user.id, + parentId: parentId, + metadata: { + ...customMetadata, // User's custom keys from the form + type: file.name.split('.').pop()?.toUpperCase() || "UNKNOWN", + mimeType: file.type } - const subFolderData = await createSubFolderRes.json(); +}); - // 2. Create Upload Session inside the new Physical Folder - const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`; - const sessionRes = await fetch(sessionUrl, { - method: "POST", - headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, - body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) - }); - - const { uploadUrl } = await sessionRes.json(); - const buffer = Buffer.from(await file.arrayBuffer()); - - // 3. PUT the file binary - const uploadRes = await fetch(uploadUrl, { - method: "PUT", - headers: { - "Content-Length": `${file.size}`, - "Content-Range": `bytes 0-${file.size - 1}/${file.size}` - }, - body: buffer - }); - - const uploadedFileData = await uploadRes.json(); - const oneDriveId = uploadedFileData.id; - const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN"; - - // 4. Create record in Database - // Link it to the VIRTUAL folder via parentId - await prisma.fileNode.create({ - data: { - id: internalId, - oneDriveId: oneDriveId, - name: file.name, - description: description, - size: BigInt(file.size), - isFolder: false, - path: `/${rootFolder}/${internalId}/${file.name}`, - ownerId: session.user.id, - parentId: parentId, - metadata: { - ...customMetadata, - type: extension, - mimeType: file.type - } - } - }); - - revalidatePath("/dashboard"); - revalidatePath("/upload"); - return { success: true }; + revalidatePath("/dashboard"); + revalidatePath("/upload"); + return { success: true }; + } catch (error: any) { + console.error("Upload refactor error:", error); + return { success: false, error: error.message }; + } } \ No newline at end of file diff --git a/src/app/upload/upload-view.tsx b/src/app/upload/upload-view.tsx index e92f4fd..9d6fbd9 100644 --- a/src/app/upload/upload-view.tsx +++ b/src/app/upload/upload-view.tsx @@ -208,19 +208,19 @@ export default function UploadView({ user, folders = [] }: any) { {customMetadata.map((row, index) => ( - + updateMetadataRow(index, 'key', e.target.value)} /> - + updateMetadataRow(index, 'value', e.target.value)} /> - + removeMetadataRow(index)}> diff --git a/src/components/LogoutButton.tsx b/src/components/LogoutButton.tsx new file mode 100644 index 0000000..eee8e06 --- /dev/null +++ b/src/components/LogoutButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { signOut } from "next-auth/react"; +import { Button } from "@mui/material"; +import LogoutIcon from '@mui/icons-material/Logout'; + +export default function LogoutButton() { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/providers.tsx b/src/components/providers.tsx index 9837792..bcd8b6b 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -1,12 +1,19 @@ 'use client'; + +import React from "react"; import { SessionProvider } from "next-auth/react"; -// ... other imports like ThemeProvider +import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; +import { ThemeProvider } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import theme from '@/lib/theme'; // Import your custom theme here export function Providers({ children }: { children: React.ReactNode }) { return ( - + + {/* CssBaseline resets browser styles to match MUI and your theme */} + {children} diff --git a/src/data-access/file-nodes.ts b/src/data-access/file-nodes.ts new file mode 100644 index 0000000..b9a722d --- /dev/null +++ b/src/data-access/file-nodes.ts @@ -0,0 +1,104 @@ +// src/data-access/file-nodes.ts +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Retrieve all nodes for the dashboard. + * Centralizing this here allows us to change sort order or filters + * in one place for the entire application. + */ +export async function getAllFileNodes() { + return await prisma.fileNode.findMany({ + orderBy: { + updatedAt: 'desc', + }, + }); +} + +/** + * FETCH: Get a single node by ID. + * Used by the Download route and Update pages to verify a file exists. + */ +export async function getFileNodeById(id: string) { + return await prisma.fileNode.findUnique({ + where: { id }, + }); +} + +/** + * UPDATE: Modify metadata, name, or virtual location. + * This function accepts the data object to keep the DAL flexible. + */ +export async function updateFileNode(id: string, data: any) { + return await prisma.fileNode.update({ + where: { id }, + data: { + ...data, + updatedAt: new Date(), + }, + }); +} + +/** + * DELETE: Remove the record from the database. + * Cloud deletion should be handled by the Service Layer before calling this. + */ +export async function deleteFileNode(id: string) { + return await prisma.fileNode.delete({ + where: { id }, + }); +} + +/** + * MASTER CREATE: Handles both standard uploads and virtual folders. + * If no ID is provided, it generates a fresh UUID. + */ +export async function createFileNode(data: { + id?: string; // Optional: used for virtual folders/UUID storage + oneDriveId: string | null; + name: string; + description?: string; + isFolder: boolean; + path: string; + ownerId: string; + parentId?: string | null; // Optional: for nested structures + size?: bigint; + metadata: any; +}) { + return await prisma.fileNode.create({ + data: { + ...data, + id: data.id ?? crypto.randomUUID(), // Use provided ID or generate new one + } + }); +} + + +// ... other functions (getAllFileNodes, etc) + +/** + * UPSERT: Create or Update a file node based on OneDrive ID + * Moved here because it interacts with the Database. + */ +export async function upsertFileNode(oneDriveId: string, data: any) { + return await prisma.fileNode.upsert({ + where: { oneDriveId }, + update: { + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + updatedAt: new Date(), + }, + create: { + id: crypto.randomUUID(), + oneDriveId: oneDriveId, + name: data.name, + size: data.size, + isFolder: data.isFolder, + path: data.path, + ownerId: data.ownerId, + metadata: data.metadata, + } + }); +} \ No newline at end of file diff --git a/src/data-access/users.ts b/src/data-access/users.ts new file mode 100644 index 0000000..4181b00 --- /dev/null +++ b/src/data-access/users.ts @@ -0,0 +1,42 @@ +import "server-only"; +import { prisma } from "@/lib/prisma"; + +/** + * FETCH: Get user by Email + * Used for authorization checks in actions. + */ +export async function getUserByEmail(email: string) { + return await prisma.user.findUnique({ + where: { email }, + select: { id: true, email: true, role: true } + }); +} + +/** + * FETCH: Get user by ID + */ +export async function getUserById(id: string) { + return await prisma.user.findUnique({ + where: { id }, + select: { id: true, email: true, role: true } + }); +} + +/** + * UPDATE: Update user role + */ +export async function updateUserRole(id: string, role: "ADMIN" | "USER") { + return await prisma.user.update({ + where: { id }, + data: { role } + }); +} + +/** + * FETCH: List all users (for the settings table) + */ +export async function getAllUsers() { + return await prisma.user.findMany({ + orderBy: { email: 'asc' } + }); +} \ No newline at end of file diff --git a/src/lib/theme.ts b/src/lib/theme.ts index 37d7e0d..d46fe01 100644 --- a/src/lib/theme.ts +++ b/src/lib/theme.ts @@ -1,7 +1,6 @@ import { createTheme } from '@mui/material/styles'; import { Roboto } from 'next/font/google'; -// Load the font optimized for Next.js const roboto = Roboto({ weight: ['300', '400', '500', '700'], subsets: ['latin'], @@ -9,39 +8,36 @@ const roboto = Roboto({ }); const theme = createTheme({ - // 1. Color Palette (Clean & Professional for a Library App) palette: { mode: 'light', primary: { - main: '#1976d2', // Professional Blue + main: '#1976d2', }, secondary: { - main: '#9c27b0', // Purple for accents + main: '#9c27b0', }, background: { - default: '#f4f6f8', // Light grey for the app background + default: '#f4f6f8', paper: '#ffffff', }, }, - - // 2. Typography typography: { fontFamily: roboto.style.fontFamily, h6: { fontWeight: 600, }, }, - - // 3. Component Defaults components: { + // Keep your button styles MuiButton: { styleOverrides: { root: { - textTransform: 'none', // Prevents all-caps buttons + textTransform: 'none', borderRadius: 8, }, }, }, + // Keep your paper styles MuiPaper: { defaultProps: { elevation: 2, @@ -52,6 +48,12 @@ const theme = createTheme({ }, }, }, + // ADD THIS: Ensures the theme is compatible with MUI v7's Grid logic + MuiStack: { + defaultProps: { + useFlexGap: true, + }, + }, }, }); diff --git a/src/services/onedrive.ts b/src/services/onedrive.ts new file mode 100644 index 0000000..f2d5560 --- /dev/null +++ b/src/services/onedrive.ts @@ -0,0 +1,172 @@ +// src/services/onedrive.ts +import "server-only"; +import { getFreshAccessToken } from "@/lib/auth-utils"; + +/** + * PRIVATE HELPER: graphRequest + * This internal function handles the heavy lifting of fetching tokens + * and making the actual HTTP call to Microsoft. + */ +async function graphRequest(userId: string, endpoint: string, options: RequestInit = {}) { + // 1. Automatically handle token refresh logic + const token = await getFreshAccessToken(userId); + const baseUrl = "https://graph.microsoft.com/v1.0"; + + const res = await fetch(`${baseUrl}${endpoint}`, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${token}`, + }, + }); + + // 2. Centralized Error Handling for OneDrive + if (!res.ok) { + const errorData = await res.text(); + console.error(`OneDrive API Error [${endpoint}]:`, errorData); + throw new Error(`OneDrive API failed: ${res.statusText}`); + } + + return res; +} + +/** + * SERVICE: Download File Content + * Returns the raw binary stream from OneDrive. + */ +export async function getOneDriveContentStream(userId: string, oneDriveId: string) { + return await graphRequest(userId, `/me/drive/items/${oneDriveId}/content`); +} + +/** + * SERVICE: Get File Metadata + * Used to get the @microsoft.graph.downloadUrl or driveItem properties. + */ +export async function getOneDriveItem(userId: string, oneDriveId: string) { + const res = await graphRequest(userId, `/me/drive/items/${oneDriveId}`); + return res.json(); +} + +/** + * SERVICE: Upload File + * Handles the PUT request to OneDrive for new or updated files. + */ +export async function uploadToOneDrive(userId: string, file: File, oneDriveId?: string) { + // If oneDriveId exists, we update. Otherwise, we'd use a path (needs expansion for new files). + const endpoint = oneDriveId + ? `/me/drive/items/${oneDriveId}/content` + : `/me/drive/root:/${file.name}:/content`; + + return await graphRequest(userId, endpoint, { + method: "PUT", + headers: { "Content-Type": file.type }, + body: Buffer.from(await file.arrayBuffer()), + }); +} + +/** + * SERVICE: Delete from Cloud + */ +export async function deleteFromOneDrive(userId: string, oneDriveId: string) { + return await graphRequest(userId, `/me/drive/items/${oneDriveId}`, { + method: "DELETE", + }); +} + +/** + * SERVICE: List Children of the WebCalibre folder + */ +export async function getWebCalibreChildren(userId: string) { + const res = await graphRequest(userId, "/me/drive/root:/WebCalibre:/children"); + const data = await res.json(); + return data.value; // Returns the array of driveItems +} + +/** + * SERVICE: Ensure a specific folder exists in OneDrive + * Returns the folder ID + */ +export async function ensureOneDriveFolder(userId: string, folderName: string) { + try { + const res = await graphRequest(userId, `/me/drive/root:/${folderName}`); + const data = await res.json(); + return data.id; + } catch (error) { + // If 404, create it + const createRes = await graphRequest(userId, `/me/drive/root/children`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: folderName, folder: {} }) + }); + const data = await createRes.json(); + return data.id; + } +} + +/** + * SERVICE: Upload Large File via Session + * This replaces the basic PUT for better reliability + */ +export async function uploadLargeFile(userId: string, file: File, folderName: string) { + // 1. Create Upload Session + const sessionRes = await graphRequest(userId, `/me/drive/root:/${folderName}/${file.name}:/createUploadSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "rename" } }) + }); + const { uploadUrl } = await sessionRes.json(); + + // 2. Upload the data to the provided URL (No Authorization header needed for the uploadUrl itself) + const buffer = Buffer.from(await file.arrayBuffer()); + const uploadRes = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": `${file.size}`, + "Content-Range": `bytes 0-${file.size - 1}/${file.size}` + }, + body: buffer + }); + + if (!uploadRes.ok) throw new Error("Upload session failed"); + return await uploadRes.json(); // Returns the DriveItem +} +/** + * SERVICE: Create a folder by name inside a parent path + */ +export async function createOneDriveFolder(userId: string, parentPath: string, folderName: string) { + return await graphRequest(userId, `/me/drive/root:/${parentPath}:/children`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: folderName, + folder: {}, + "@microsoft.graph.conflictBehavior": "fail" + }) + }); +} + +/** + * SERVICE: Upload to a specific folder ID (using session) + */ +export async function uploadToFolderId(userId: string, file: File, folderId: string) { + const sessionRes = await graphRequest(userId, `/me/drive/items/${folderId}:/${encodeURIComponent(file.name)}:/createUploadSession`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) + }); + + const { uploadUrl } = await sessionRes.json(); + const buffer = Buffer.from(await file.arrayBuffer()); + + const uploadRes = await fetch(uploadUrl, { + method: "PUT", + headers: { + "Content-Length": `${file.size}`, + "Content-Range": `bytes 0-${file.size - 1}/${file.size}` + }, + body: buffer + }); + + if (!uploadRes.ok) throw new Error("Upload failed"); + return await uploadRes.json(); +} \ No newline at end of file