Prechádzať zdrojové kódy

refactor(api): migrate from REST API to server actions for file operations

- Replace REST API endpoints with Next.js server actions
- Remove deprecated API routes (/api/upload, /api/files/*)
- Update file upload to use uploadFile server action
- Update file listing to use getFiles server action
- Update file download to use downloadFile server action
- Update file deletion to use deleteFile server action
- Remove CORS handling from API endpoints
- Update components to use server actions instead of fetch
- Simplify file upload response structure
- Add proper error handling with thrown errors

BREAKING CHANGE: REST API endpoints removed, use server actions instead
vidane 6 mesiacov pred
rodič
commit
d6cc24772b

+ 9 - 2
app/actions/file-upload.ts

@@ -16,10 +16,17 @@ export async function uploadFile(file: File) {
       },
     });
 
-    return { success: true, data: uploadedFile };
+    return {
+      id: uploadedFile.id,
+      filename: uploadedFile.filename,
+      mimetype: uploadedFile.mimetype,
+      size: uploadedFile.size,
+      createdAt: uploadedFile.createdAt.toISOString(),
+      updatedAt: uploadedFile.updatedAt.toISOString(),
+    };
   } catch (error) {
     console.error('Error uploading file:', error);
-    return { success: false, error: 'Failed to upload file' };
+    throw new Error('Failed to upload file');
   }
 }
 

+ 95 - 0
app/actions/files.ts

@@ -0,0 +1,95 @@
+"use server";
+
+import { PrismaClient } from "@prisma/client";
+import { revalidatePath } from "next/cache";
+
+const prisma = new PrismaClient();
+
+export interface FileData {
+    id: string;
+    filename: string;
+    mimetype: string;
+    size: number;
+    createdAt: Date;
+    updatedAt: Date;
+}
+
+export async function getFiles(): Promise<FileData[]> {
+    try {
+        const files = await prisma.file.findMany({
+            select: {
+                id: true,
+                filename: true,
+                mimetype: true,
+                size: true,
+                createdAt: true,
+                updatedAt: true,
+            },
+            orderBy: {
+                createdAt: 'desc',
+            },
+        });
+
+        return files;
+    } catch (error) {
+        console.error("Error listing files:", error);
+        throw new Error("Failed to list files");
+    } finally {
+        await prisma.$disconnect();
+    }
+}
+
+export async function downloadFile(id: string): Promise<{
+    data: Uint8Array;
+    filename: string;
+    mimetype: string;
+    size: number;
+} | null> {
+    try {
+        const file = await prisma.file.findUnique({
+            where: { id },
+        });
+
+        if (!file) {
+            return null;
+        }
+
+        return {
+            data: file.data,
+            filename: file.filename,
+            mimetype: file.mimetype,
+            size: file.size,
+        };
+    } catch (error) {
+        console.error("Error retrieving file:", error);
+        throw new Error("Failed to retrieve file");
+    } finally {
+        await prisma.$disconnect();
+    }
+}
+
+export async function deleteFile(id: string): Promise<boolean> {
+    try {
+        const file = await prisma.file.findUnique({
+            where: { id },
+        });
+
+        if (!file) {
+            return false;
+        }
+
+        await prisma.file.delete({
+            where: { id },
+        });
+
+        revalidatePath('/files');
+        revalidatePath('/dashboard');
+
+        return true;
+    } catch (error) {
+        console.error("Error deleting file:", error);
+        throw new Error("Failed to delete file");
+    } finally {
+        await prisma.$disconnect();
+    }
+}

+ 1 - 132
app/api-docs/page.tsx

@@ -39,137 +39,6 @@ const openApiSpec = {
     },
   },
   paths: {
-    '/api/upload': {
-      post: {
-        summary: 'Upload a file',
-        description: 'Upload a file to the server and store it in the database',
-        tags: ['Files'],
-        requestBody: {
-          required: true,
-          content: {
-            'multipart/form-data': {
-              schema: {
-                type: 'object',
-                properties: {
-                  file: {
-                    type: 'string',
-                    format: 'binary',
-                    description: 'The file to upload',
-                  },
-                },
-                required: ['file'],
-              },
-            },
-          },
-        },
-        responses: {
-          '200': {
-            description: 'File uploaded successfully',
-            content: {
-              'application/json': {
-                schema: {
-                  type: 'object',
-                  properties: {
-                    success: { type: 'boolean', example: true },
-                    file: { $ref: '#/components/schemas/File' },
-                  },
-                },
-              },
-            },
-          },
-          '400': {
-            description: 'Bad request',
-            content: {
-              'application/json': {
-                schema: { $ref: '#/components/schemas/Error' },
-              },
-            },
-          },
-          '500': {
-            description: 'Internal server error',
-            content: {
-              'application/json': {
-                schema: { $ref: '#/components/schemas/Error' },
-              },
-            },
-          },
-        },
-      },
-    },
-    '/api/files': {
-      get: {
-        summary: 'List all files',
-        description: 'Get a list of all uploaded files with metadata',
-        tags: ['Files'],
-        responses: {
-          '200': {
-            description: 'List of files retrieved successfully',
-            content: {
-              'application/json': {
-                schema: {
-                  type: 'object',
-                  properties: {
-                    success: { type: 'boolean', example: true },
-                    files: {
-                      type: 'array',
-                      items: { $ref: '#/components/schemas/File' },
-                    },
-                  },
-                },
-              },
-            },
-          },
-        },
-      },
-    },
-    '/api/files/{id}': {
-      get: {
-        summary: 'Download a file',
-        description: 'Download a specific file by its ID',
-        tags: ['Files'],
-        parameters: [
-          {
-            name: 'id',
-            in: 'path',
-            required: true,
-            description: 'The ID of the file to download',
-            schema: {
-              type: 'string',
-              example: 'clh123abc456',
-            },
-          },
-        ],
-        responses: {
-          '200': {
-            description: 'File downloaded successfully',
-            content: {
-              'application/octet-stream': {
-                schema: {
-                  type: 'string',
-                  format: 'binary',
-                },
-              },
-            },
-          },
-          '404': {
-            description: 'File not found',
-            content: {
-              'application/json': {
-                schema: { $ref: '#/components/schemas/Error' },
-              },
-            },
-          },
-          '500': {
-            description: 'Internal server error',
-            content: {
-              'application/json': {
-                schema: { $ref: '#/components/schemas/Error' },
-              },
-            },
-          },
-        },
-      },
-    },
   },
 }
 
@@ -187,7 +56,7 @@ export default function ApiDocs() {
                 Interactive API documentation for file upload and management endpoints.
               </p>
             </div>
-            <ApiReferenceReact 
+            <ApiReferenceReact
               configuration={{
                 content: openApiSpec,
                 theme: 'default',

+ 0 - 97
app/api/files/[id]/route.ts

@@ -1,97 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
-
-export const GET = async (
-  req: NextRequest,
-  { params }: { params: Promise<{ id: string }> }
-) => {
-  const { id } = await params;
-  try {
-    const file = await prisma.file.findUnique({
-      where: { id },
-    });
-
-    if (!file) {
-      const response = NextResponse.json(
-        { error: "File not found" },
-        { status: 404 }
-      );
-      response.headers.set('Access-Control-Allow-Origin', '*');
-      return response;
-    }
-
-    // Return file as downloadable response
-    const response = new NextResponse(file.data, {
-      headers: {
-        "Content-Type": file.mimetype,
-        "Content-Disposition": `attachment; filename="${file.filename}"`,
-        "Content-Length": file.size.toString(),
-        "Access-Control-Allow-Origin": "*",
-      },
-    });
-    
-    return response;
-  } catch (error) {
-    console.error("Error retrieving file:", error);
-    const response = NextResponse.json(
-      { error: "Failed to retrieve file" },
-      { status: 500 }
-    );
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } finally {
-    await prisma.$disconnect();
-  }
-};
-
-export const DELETE = async (
-  req: NextRequest,
-  { params }: { params: Promise<{ id: string }> }
-) => {
-  const { id } = await params;
-  try {
-    const file = await prisma.file.findUnique({
-      where: { id },
-    });
-
-    if (!file) {
-      const response = NextResponse.json(
-        { error: "File not found" },
-        { status: 404 }
-      );
-      response.headers.set('Access-Control-Allow-Origin', '*');
-      return response;
-    }
-
-    await prisma.file.delete({
-      where: { id },
-    });
-
-    const response = NextResponse.json(
-      { message: "File deleted successfully" },
-      { status: 200 }
-    );
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } catch (error) {
-    console.error("Error deleting file:", error);
-    const response = NextResponse.json(
-      { error: "Failed to delete file" },
-      { status: 500 }
-    );
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } finally {
-    await prisma.$disconnect();
-  }
-};
-
-export const OPTIONS = async () => {
-  const response = new NextResponse(null, { status: 200 });
-  response.headers.set('Access-Control-Allow-Origin', '*');
-  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
-  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-  return response;
-};

+ 0 - 53
app/api/files/route.ts

@@ -1,53 +0,0 @@
-import { NextResponse } from "next/server";
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
-
-export const GET = async () => {
-  try {
-    const files = await prisma.file.findMany({
-      select: {
-        id: true,
-        filename: true,
-        mimetype: true,
-        size: true,
-        createdAt: true,
-        updatedAt: true,
-      },
-      orderBy: {
-        createdAt: 'desc',
-      },
-    });
-
-    const response = NextResponse.json({
-      success: true,
-      files,
-    });
-    
-    // Add CORS headers
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
-    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-    
-    return response;
-  } catch (error) {
-    console.error("Error listing files:", error);
-    const response = NextResponse.json(
-      { success: false, error: "Failed to list files" },
-      { status: 500 }
-    );
-    
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } finally {
-    await prisma.$disconnect();
-  }
-};
-
-export const OPTIONS = async () => {
-  const response = new NextResponse(null, { status: 200 });
-  response.headers.set('Access-Control-Allow-Origin', '*');
-  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
-  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-  return response;
-};

+ 0 - 68
app/api/upload/route.ts

@@ -1,68 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { PrismaClient } from "@prisma/client";
-
-const prisma = new PrismaClient();
-
-export const POST = async (req: NextRequest) => {
-  try {
-    const formData = await req.formData();
-    const body = Object.fromEntries(formData);
-    const file = (body.file as File) || null;
-
-    if (!file) {
-      const response = NextResponse.json({
-        success: false,
-        error: "No file provided",
-      }, { status: 400 });
-      
-      response.headers.set('Access-Control-Allow-Origin', '*');
-      return response;
-    }
-
-    // Convert file to buffer
-    const buffer = Buffer.from(await file.arrayBuffer());
-    
-    // Store file in database
-    const savedFile = await prisma.file.create({
-      data: {
-        filename: file.name,
-        mimetype: file.type,
-        size: file.size,
-        data: buffer,
-      },
-    });
-
-    const response = NextResponse.json({
-      success: true,
-      file: {
-        id: savedFile.id,
-        filename: savedFile.filename,
-        mimetype: savedFile.mimetype,
-        size: savedFile.size,
-        createdAt: savedFile.createdAt,
-      },
-    });
-    
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } catch (error) {
-    console.error("Upload error:", error);
-    const response = NextResponse.json({
-      success: false,
-      error: "Failed to upload file",
-    }, { status: 500 });
-    
-    response.headers.set('Access-Control-Allow-Origin', '*');
-    return response;
-  } finally {
-    await prisma.$disconnect();
-  }
-};
-
-export const OPTIONS = async () => {
-  const response = new NextResponse(null, { status: 200 });
-  response.headers.set('Access-Control-Allow-Origin', '*');
-  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
-  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
-  return response;
-};

+ 66 - 66
app/components/filesTable.tsx

@@ -11,7 +11,7 @@ import {
   SortingState,
   RowSelectionState,
 } from "@tanstack/react-table";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Checkbox } from "@/components/ui/checkbox";
@@ -26,14 +26,15 @@ import {
 import { Skeleton } from "@/components/ui/skeleton";
 import { AlertCircle, Download, Trash2, RefreshCw } from "lucide-react";
 import { Input } from "@/components/ui/input";
+import { getFiles, deleteFile } from "@/app/actions/files";
 
 interface FileData {
   id: string;
   filename: string;
   mimetype: string;
   size: number;
-  createdAt: string;
-  updatedAt: string;
+  createdAt: Date;
+  updatedAt: Date;
 }
 
 export function FilesTable() {
@@ -41,17 +42,12 @@ export function FilesTable() {
   const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
   const [files, setFiles] = useState<FileData[]>([]);
   const [isUploading, setIsUploading] = useState(false);
-  const queryClient = useQueryClient();
 
   const { data, isLoading, isError, error, refetch } = useQuery({
     queryKey: ["files"],
     queryFn: async () => {
-      const response = await fetch("/api/files");
-      if (!response.ok) {
-        throw new Error("Failed to fetch files");
-      }
-      const data = await response.json();
-      return data.files as FileData[];
+      const files = await getFiles();
+      return files;
     },
   });
 
@@ -105,11 +101,11 @@ export function FilesTable() {
       cell: ({ row }) => {
         const bytes = row.getValue("size") as number;
         if (bytes === 0) return "0 Bytes";
-        
+
         const k = 1024;
         const sizes = ["Bytes", "KB", "MB", "GB"];
         const i = Math.floor(Math.log(bytes) / Math.log(k));
-        
+
         return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
       },
     },
@@ -189,29 +185,27 @@ export function FilesTable() {
 
     setIsUploading(true);
 
-    const formData = new FormData();
-    formData.append("file", file);
-
     try {
-      const response = await fetch("/api/upload", {
-        method: "POST",
-        body: formData,
-      });
+      // Import the server action
+      const { uploadFile } = await import('../actions/file-upload');
 
-      const result = await response.json();
-      
-      if (result.success) {
-        // Add new file to the beginning of the list
-        setFiles(prevFiles => [result.file, ...prevFiles]);
-        
-        // Invalidate and refetch to ensure consistency
-        await queryClient.invalidateQueries({ queryKey: ["files"] });
-        
-        // Reset file input
-        event.target.value = '';
-      } else {
-        alert(`Upload failed: ${result.error || 'Unknown error'}`);
-      }
+      const uploadedFile = await uploadFile(file);
+
+      // Use the uploadedFile response to update the table immediately
+      const newFile: FileData = {
+        id: uploadedFile.id,
+        filename: uploadedFile.filename,
+        mimetype: uploadedFile.mimetype,
+        size: uploadedFile.size,
+        createdAt: new Date(uploadedFile.createdAt),
+        updatedAt: new Date(uploadedFile.updatedAt),
+      };
+
+      // Add the new file to the beginning of the files array
+      setFiles(prevFiles => [newFile, ...prevFiles]);
+
+      // Reset file input
+      event.target.value = '';
     } catch (error) {
       console.error("Upload error:", error);
       alert("Failed to upload file");
@@ -224,20 +218,36 @@ export function FilesTable() {
     refetch();
   };
 
-  const handleDownload = (fileId: string, filename: string) => {
-    const downloadUrl = `/api/files/${fileId}`;
-    const link = document.createElement('a');
-    link.href = downloadUrl;
-    link.download = filename;
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
+  const handleDownload = async (fileId: string, filename: string) => {
+    try {
+      const { downloadFile } = await import('../actions/files');
+      const fileData = await downloadFile(fileId);
+
+      if (!fileData) {
+        alert('File not found');
+        return;
+      }
+
+      // Create blob and download
+      const blob = new Blob([fileData.data], { type: fileData.mimetype });
+      const url = window.URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = filename;
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      window.URL.revokeObjectURL(url);
+    } catch (error) {
+      console.error('Error downloading file:', error);
+      alert('Failed to download file');
+    }
   };
 
   const handleDownloadSelected = () => {
     const selectedRows = table.getSelectedRowModel().rows;
     if (selectedRows.length === 0) return;
-    
+
     if (selectedRows.length === 1) {
       const selected = selectedRows[0];
       handleDownload(selected.original.id, selected.original.filename);
@@ -257,23 +267,15 @@ export function FilesTable() {
     const confirmDelete = window.confirm(
       `Are you sure you want to delete ${selectedRows.length} file${selectedRows.length > 1 ? 's' : ''}? This action cannot be undone.`
     );
-    
+
     if (!confirmDelete) return;
 
     try {
       const results = [];
       for (const row of selectedRows) {
         try {
-          const response = await fetch(`/api/files/${row.original.id}`, {
-            method: 'DELETE',
-          });
-          
-          if (!response.ok) {
-            const errorText = await response.text();
-            results.push({ id: row.original.id, success: false, error: errorText });
-          } else {
-            results.push({ id: row.original.id, success: true });
-          }
+          const success = await deleteFile(row.original.id);
+          results.push({ id: row.original.id, success });
         } catch (error) {
           results.push({ id: row.original.id, success: false, error: String(error) });
         }
@@ -281,16 +283,16 @@ export function FilesTable() {
 
       const successful = results.filter(r => r.success).length;
       const failed = results.filter(r => !r.success).length;
-      
+
       if (failed > 0) {
         alert(`${successful} file(s) deleted successfully, ${failed} failed.`);
       } else {
         alert(`${successful} file(s) deleted successfully.`);
       }
-      
+
       setRowSelection({});
       refetch();
-      
+
     } catch (error) {
       console.error('Error in delete process:', error);
       alert('Failed to delete files. Please try again.');
@@ -301,20 +303,18 @@ export function FilesTable() {
     const confirmDelete = window.confirm(
       `Are you sure you want to delete "${filename}"? This action cannot be undone.`
     );
-    
+
     if (!confirmDelete) return;
 
     try {
-      const response = await fetch(`/api/files/${fileId}`, {
-        method: 'DELETE',
-      });
-      
-      if (!response.ok) {
-        throw new Error('Failed to delete file');
+      const success = await deleteFile(fileId);
+
+      if (success) {
+        refetch();
+      } else {
+        alert('File not found or could not be deleted.');
       }
-      
-      refetch();
-      
+
     } catch (error) {
       console.error('Error deleting file:', error);
       alert('Failed to delete file. Please try again.');

+ 13 - 21
app/components/uploadForm.tsx

@@ -20,31 +20,23 @@ export const UploadForm = ({ onFileUploaded }: { onFileUploaded?: (file: FileDat
     if (e.target.files && e.target.files[0]) {
       setIsUploading(true);
       const file = e.target.files[0];
-      const formData = new FormData();
-      formData.append("file", file);
 
       try {
-        const response = await fetch("/api/upload", {
-          method: "POST",
-          body: formData,
-        });
+        // Import the server action
+        const { uploadFile } = await import('../actions/file-upload');
 
-        const result = await response.json();
-        
-        if (result.success) {
-          // Invalidate the files query to trigger a refresh
-          await queryClient.invalidateQueries({ queryKey: ["files"] });
-          
-          // Call the callback if provided
-          if (onFileUploaded) {
-            onFileUploaded(result.file);
-          }
-          
-          // Reset the file input
-          e.target.value = '';
-        } else {
-          alert(`Upload failed: ${result.error || 'Unknown error'}`);
+        const uploadedFile = await uploadFile(file);
+
+        // Invalidate the files query to trigger a refresh
+        await queryClient.invalidateQueries({ queryKey: ["files"] });
+
+        // Call the callback if provided
+        if (onFileUploaded) {
+          onFileUploaded(uploadedFile);
         }
+
+        // Reset the file input
+        e.target.value = '';
       } catch (error) {
         console.error("Upload error:", error);
         alert("Failed to upload file");