feat(cloud-page) - setup1
This commit is contained in:
7
apps/Backend/src/routes/cloud-storage.ts
Normal file
7
apps/Backend/src/routes/cloud-storage.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import paymentsRoutes from "./payments";
|
|||||||
import databaseManagementRoutes from "./database-management";
|
import databaseManagementRoutes from "./database-management";
|
||||||
import notificationsRoutes from "./notifications";
|
import notificationsRoutes from "./notifications";
|
||||||
import paymentOcrRoutes from "./paymentOcrExtraction";
|
import paymentOcrRoutes from "./paymentOcrExtraction";
|
||||||
|
import cloudStorageRoutes from "./cloud-storage";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -28,5 +29,6 @@ router.use("/payments", paymentsRoutes);
|
|||||||
router.use("/database-management", databaseManagementRoutes);
|
router.use("/database-management", databaseManagementRoutes);
|
||||||
router.use("/notifications", notificationsRoutes);
|
router.use("/notifications", notificationsRoutes);
|
||||||
router.use("/payment-ocr", paymentOcrRoutes);
|
router.use("/payment-ocr", paymentOcrRoutes);
|
||||||
|
router.use("/cloud-storage", cloudStorageRoutes);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
12
apps/Backend/src/utils/prismaFileUtils.ts
Normal file
12
apps/Backend/src/utils/prismaFileUtils.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Helper: convert Prisma CloudFile result to JSON-friendly object.
|
||||||
|
*/
|
||||||
|
export function serializeFile(f: any) {
|
||||||
|
if (!f) return null;
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
fileSize: typeof f.fileSize === "bigint" ? f.fileSize.toString() : f.fileSize,
|
||||||
|
createdAt: f.createdAt?.toISOString?.(),
|
||||||
|
updatedAt: f.updatedAt?.toISOString?.(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ const DatabaseManagementPage = lazy(
|
|||||||
() => import("./pages/database-management-page")
|
() => import("./pages/database-management-page")
|
||||||
);
|
);
|
||||||
const ReportsPage = lazy(() => import("./pages/reports-page"));
|
const ReportsPage = lazy(() => import("./pages/reports-page"));
|
||||||
|
const CloudStoragePage = lazy(() => import("./pages/cloud-storage-page"));
|
||||||
const NotFound = lazy(() => import("./pages/not-found"));
|
const NotFound = lazy(() => import("./pages/not-found"));
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
@@ -50,7 +51,8 @@ function Router() {
|
|||||||
path="/database-management"
|
path="/database-management"
|
||||||
component={() => <DatabaseManagementPage />}
|
component={() => <DatabaseManagementPage />}
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute path="/reports/" component={() => <ReportsPage />} />
|
<ProtectedRoute path="/reports" component={() => <ReportsPage />} />
|
||||||
|
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
|
||||||
<Route path="/auth" component={() => <AuthPage />} />
|
<Route path="/auth" component={() => <AuthPage />} />
|
||||||
<Route component={() => <NotFound />} />
|
<Route component={() => <NotFound />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FolderOpen,
|
FolderOpen,
|
||||||
Database,
|
Database,
|
||||||
FileText,
|
FileText,
|
||||||
|
Cloud,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@@ -61,6 +62,11 @@ export function Sidebar() {
|
|||||||
path: "/reports",
|
path: "/reports",
|
||||||
icon: <FileText className="h-5 w-5" />,
|
icon: <FileText className="h-5 w-5" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Cloud storage",
|
||||||
|
path: "/cloud-storage",
|
||||||
|
icon: <Cloud className="h-5 w-5" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Backup Database",
|
name: "Backup Database",
|
||||||
path: "/database-management",
|
path: "/database-management",
|
||||||
|
|||||||
831
apps/Frontend/src/pages/cloud-storage-page.tsx
Normal file
831
apps/Frontend/src/pages/cloud-storage-page.tsx
Normal file
@@ -0,0 +1,831 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
||||||
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import {
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
Folder,
|
||||||
|
Trash2,
|
||||||
|
FolderPlus,
|
||||||
|
Edit2,
|
||||||
|
MoreVertical,
|
||||||
|
FileText,
|
||||||
|
Image,
|
||||||
|
FileCode,
|
||||||
|
FileArchive,
|
||||||
|
FileAudio,
|
||||||
|
FileVideo,
|
||||||
|
Eye,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { CloudFolder, CloudFile } from "@shared/schema";
|
||||||
|
|
||||||
|
interface CloudStorageItem {
|
||||||
|
folders: CloudFolder[];
|
||||||
|
files: CloudFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CloudStoragePage() {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
const [currentFolderId, setCurrentFolderId] = useState<number | null>(null);
|
||||||
|
const [folderPath, setFolderPath] = useState<
|
||||||
|
{ id: number | null; name: string }[]
|
||||||
|
>([{ id: null, name: "My Cloud Storage" }]);
|
||||||
|
|
||||||
|
// Dialog states
|
||||||
|
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||||
|
const [isRenameOpen, setIsRenameOpen] = useState(false);
|
||||||
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||||
|
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
||||||
|
|
||||||
|
// Form states
|
||||||
|
const [newFolderName, setNewFolderName] = useState("");
|
||||||
|
const [newItemName, setNewItemName] = useState("");
|
||||||
|
const [selectedItem, setSelectedItem] = useState<{
|
||||||
|
type: "folder" | "file";
|
||||||
|
item: CloudFolder | CloudFile;
|
||||||
|
} | null>(null);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [viewingFile, setViewingFile] = useState<CloudFile | null>(null);
|
||||||
|
const [fileViewUrl, setFileViewUrl] = useState<string>("");
|
||||||
|
const [isViewerOpen, setIsViewerOpen] = useState(false);
|
||||||
|
|
||||||
|
// Fetch folders and files
|
||||||
|
const {
|
||||||
|
data: storageItems,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useQuery<CloudStorageItem>({
|
||||||
|
queryKey: ["/api/cloud-storage/items", currentFolderId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = currentFolderId
|
||||||
|
? `?parentId=${currentFolderId}`
|
||||||
|
: "?parentId=";
|
||||||
|
const res = await fetch(`/api/cloud-storage/items${params}`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch items");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create folder mutation
|
||||||
|
const createFolderMutation = useMutation({
|
||||||
|
mutationFn: async (name: string) => {
|
||||||
|
const res = await apiRequest("POST", "/api/cloud-storage/folders", {
|
||||||
|
name,
|
||||||
|
parentId: currentFolderId,
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsCreateFolderOpen(false);
|
||||||
|
setNewFolderName("");
|
||||||
|
toast({
|
||||||
|
title: "Folder created",
|
||||||
|
description: "Your folder has been created successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to create folder. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rename folder mutation
|
||||||
|
const renameFolderMutation = useMutation({
|
||||||
|
mutationFn: async ({ id, name }: { id: number; name: string }) => {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"PATCH",
|
||||||
|
`/api/cloud-storage/folders/${id}`,
|
||||||
|
{ name }
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsRenameOpen(false);
|
||||||
|
setNewItemName("");
|
||||||
|
setSelectedItem(null);
|
||||||
|
toast({
|
||||||
|
title: "Folder renamed",
|
||||||
|
description: "Your folder has been renamed successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to rename folder. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rename file mutation
|
||||||
|
const renameFileMutation = useMutation({
|
||||||
|
mutationFn: async ({ id, name }: { id: number; name: string }) => {
|
||||||
|
const res = await apiRequest("PATCH", `/api/cloud-storage/files/${id}`, {
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsRenameOpen(false);
|
||||||
|
setNewItemName("");
|
||||||
|
setSelectedItem(null);
|
||||||
|
toast({
|
||||||
|
title: "File renamed",
|
||||||
|
description: "Your file has been renamed successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to rename file. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete folder mutation
|
||||||
|
const deleteFolderMutation = useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"DELETE",
|
||||||
|
`/api/cloud-storage/folders/${id}`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error("Failed to delete folder");
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
setSelectedItem(null);
|
||||||
|
toast({
|
||||||
|
title: "Folder deleted",
|
||||||
|
description: "Your folder has been deleted successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to delete folder. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete file mutation
|
||||||
|
const deleteFileMutation = useMutation({
|
||||||
|
mutationFn: async (id: number) => {
|
||||||
|
const res = await apiRequest("DELETE", `/api/cloud-storage/files/${id}`);
|
||||||
|
if (!res.ok) throw new Error("Failed to delete file");
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
setSelectedItem(null);
|
||||||
|
toast({
|
||||||
|
title: "File deleted",
|
||||||
|
description: "Your file has been deleted successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to delete file. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload file mutation
|
||||||
|
const uploadFileMutation = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
// Get upload URL
|
||||||
|
const uploadUrlRes = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/cloud-storage/upload-url",
|
||||||
|
{
|
||||||
|
fileName: file.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { uploadURL, storagePath } = await uploadUrlRes.json();
|
||||||
|
|
||||||
|
// Upload file to object storage
|
||||||
|
const uploadRes = await fetch(uploadURL, {
|
||||||
|
method: "PUT",
|
||||||
|
body: file,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": file.type || "application/octet-stream",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uploadRes.ok) {
|
||||||
|
throw new Error("Failed to upload file to storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save file metadata with the storage path
|
||||||
|
const res = await apiRequest("POST", "/api/cloud-storage/files", {
|
||||||
|
name: file.name,
|
||||||
|
folderId: currentFolderId,
|
||||||
|
fileUrl: storagePath,
|
||||||
|
fileSize: file.size,
|
||||||
|
mimeType: file.type,
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/cloud-storage/items"] });
|
||||||
|
setIsUploadOpen(false);
|
||||||
|
setSelectedFile(null);
|
||||||
|
toast({
|
||||||
|
title: "File uploaded",
|
||||||
|
description: "Your file has been uploaded successfully.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to upload file. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFolderClick = (folder: CloudFolder) => {
|
||||||
|
setCurrentFolderId(folder.id);
|
||||||
|
setFolderPath([...folderPath, { id: folder.id, name: folder.name }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = (index: number) => {
|
||||||
|
const newPath = folderPath.slice(0, index + 1);
|
||||||
|
setFolderPath(newPath);
|
||||||
|
setCurrentFolderId(newPath[newPath.length - 1]!.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateFolder = () => {
|
||||||
|
if (newFolderName.trim()) {
|
||||||
|
createFolderMutation.mutate(newFolderName.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
if (selectedItem && newItemName.trim()) {
|
||||||
|
if (selectedItem.type === "folder") {
|
||||||
|
renameFolderMutation.mutate({
|
||||||
|
id: (selectedItem.item as CloudFolder).id,
|
||||||
|
name: newItemName.trim(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
renameFileMutation.mutate({
|
||||||
|
id: (selectedItem.item as CloudFile).id,
|
||||||
|
name: newItemName.trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedItem) {
|
||||||
|
if (selectedItem.type === "folder") {
|
||||||
|
deleteFolderMutation.mutate((selectedItem.item as CloudFolder).id);
|
||||||
|
} else {
|
||||||
|
deleteFileMutation.mutate((selectedItem.item as CloudFile).id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = () => {
|
||||||
|
if (selectedFile) {
|
||||||
|
uploadFileMutation.mutate(selectedFile);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewFile = async (file: CloudFile) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/cloud-storage/files/${file.id}/url`, {
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to get file URL");
|
||||||
|
const { url, mimeType } = await res.json();
|
||||||
|
|
||||||
|
setViewingFile(file);
|
||||||
|
setFileViewUrl(url);
|
||||||
|
setIsViewerOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to open file. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (mimeType?: string) => {
|
||||||
|
if (!mimeType) return <FileText className="h-12 w-12 text-gray-400" />;
|
||||||
|
|
||||||
|
if (mimeType.startsWith("image/"))
|
||||||
|
return <Image className="h-12 w-12 text-blue-400" />;
|
||||||
|
if (mimeType.startsWith("video/"))
|
||||||
|
return <FileVideo className="h-12 w-12 text-purple-400" />;
|
||||||
|
if (mimeType.startsWith("audio/"))
|
||||||
|
return <FileAudio className="h-12 w-12 text-green-400" />;
|
||||||
|
if (mimeType.includes("zip") || mimeType.includes("tar"))
|
||||||
|
return <FileArchive className="h-12 w-12 text-yellow-400" />;
|
||||||
|
if (
|
||||||
|
mimeType.includes("javascript") ||
|
||||||
|
mimeType.includes("typescript") ||
|
||||||
|
mimeType.includes("json")
|
||||||
|
)
|
||||||
|
return <FileCode className="h-12 w-12 text-orange-400" />;
|
||||||
|
|
||||||
|
return <FileText className="h-12 w-12 text-gray-400" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: 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 Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="container mx-auto space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Cloud Storage</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View and manage files and folder at cloud storage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={() => setIsCreateFolderOpen(true)}>
|
||||||
|
<FolderPlus className="h-4 w-4 mr-2" />
|
||||||
|
New Folder
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsUploadOpen(true)}>
|
||||||
|
<Upload className="h-4 w-4 mr-2" />
|
||||||
|
Upload File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-3">
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
{folderPath.map((item, index) => (
|
||||||
|
<div key={index} className="flex items-center">
|
||||||
|
{index > 0 && <BreadcrumbSeparator />}
|
||||||
|
<BreadcrumbItem>
|
||||||
|
{index === folderPath.length - 1 ? (
|
||||||
|
<BreadcrumbPage>{item.name}</BreadcrumbPage>
|
||||||
|
) : (
|
||||||
|
<BreadcrumbLink
|
||||||
|
className="cursor-pointer hover:text-primary"
|
||||||
|
onClick={() => handleBreadcrumbClick(index)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
)}
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Storage Content */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Files and Folders</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage your files and folders in the cloud
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-8">Loading...</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||||
|
{/* Folders */}
|
||||||
|
{storageItems?.folders.map((folder) => (
|
||||||
|
<div
|
||||||
|
key={folder.id}
|
||||||
|
className="relative group cursor-pointer"
|
||||||
|
onDoubleClick={() => handleFolderClick(folder)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center p-4 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<Folder className="h-12 w-12 text-yellow-500 mb-2" />
|
||||||
|
<span className="text-sm text-center truncate w-full">
|
||||||
|
{folder.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem({ type: "folder", item: folder });
|
||||||
|
setNewItemName(folder.name);
|
||||||
|
setIsRenameOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit2 className="h-4 w-4 mr-2" />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem({ type: "folder", item: folder });
|
||||||
|
setIsDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
className="text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Files */}
|
||||||
|
{storageItems?.files.map((file) => (
|
||||||
|
<div key={file.id} className="relative group cursor-pointer">
|
||||||
|
<div className="flex flex-col items-center p-4 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
{getFileIcon(file.mimeType || undefined)}
|
||||||
|
<span className="text-sm text-center truncate w-full mt-2">
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{formatFileSize(file.fileSize)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={() => handleViewFile(file)}>
|
||||||
|
<Eye className="h-4 w-4 mr-2" />
|
||||||
|
View
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/cloud-storage/files/${file.id}/url`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error("Failed to get file URL");
|
||||||
|
const { url } = await res.json();
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to download file.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem({ type: "file", item: file });
|
||||||
|
setNewItemName(file.name);
|
||||||
|
setIsRenameOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit2 className="h-4 w-4 mr-2" />
|
||||||
|
Rename
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem({ type: "file", item: file });
|
||||||
|
setIsDeleteOpen(true);
|
||||||
|
}}
|
||||||
|
className="text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{!storageItems?.folders.length && !storageItems?.files.length && (
|
||||||
|
<div className="col-span-full text-center py-12">
|
||||||
|
<Folder className="h-16 w-16 mx-auto mb-4 text-gray-300" />
|
||||||
|
<p className="text-gray-500">This folder is empty</p>
|
||||||
|
<p className="text-sm text-gray-400 mt-2">
|
||||||
|
Create a new folder or upload files to get started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Create Folder Dialog */}
|
||||||
|
<Dialog open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Folder</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter a name for your new folder
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="folder-name">Folder Name</Label>
|
||||||
|
<Input
|
||||||
|
id="folder-name"
|
||||||
|
value={newFolderName}
|
||||||
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
|
placeholder="My Folder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsCreateFolderOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateFolder}
|
||||||
|
disabled={!newFolderName.trim()}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Rename Dialog */}
|
||||||
|
<Dialog open={isRenameOpen} onOpenChange={setIsRenameOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Rename {selectedItem?.type === "folder" ? "Folder" : "File"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter a new name for this {selectedItem?.type}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="new-name">New Name</Label>
|
||||||
|
<Input
|
||||||
|
id="new-name"
|
||||||
|
value={newItemName}
|
||||||
|
onChange={(e) => setNewItemName(e.target.value)}
|
||||||
|
placeholder="New name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsRenameOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRename} disabled={!newItemName.trim()}>
|
||||||
|
Rename
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<Dialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Delete {selectedItem?.type === "folder" ? "Folder" : "File"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete "{selectedItem?.item.name}"? This
|
||||||
|
action cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsDeleteOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDelete}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Upload File Dialog */}
|
||||||
|
<Dialog open={isUploadOpen} onOpenChange={setIsUploadOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Upload File</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select a file to upload to your cloud storage
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="file-upload">Select File</Label>
|
||||||
|
<Input
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedFile && (
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<p>File: {selectedFile.name}</p>
|
||||||
|
<p>Size: {formatFileSize(selectedFile.size)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsUploadOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleFileUpload} disabled={!selectedFile}>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* File Viewer Dialog */}
|
||||||
|
<Dialog open={isViewerOpen} onOpenChange={setIsViewerOpen}>
|
||||||
|
<DialogContent className="max-w-6xl w-full h-[90vh] p-0">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">{viewingFile?.name}</h2>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setIsViewerOpen(false)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto p-4 bg-gray-50">
|
||||||
|
{viewingFile && fileViewUrl && (
|
||||||
|
<>
|
||||||
|
{/* PDF Viewer */}
|
||||||
|
{viewingFile.mimeType === "application/pdf" && (
|
||||||
|
<div className="w-full h-full min-h-[70vh]">
|
||||||
|
<iframe
|
||||||
|
src={`${fileViewUrl}#view=fit`}
|
||||||
|
className="w-full h-full border rounded"
|
||||||
|
title={viewingFile.name}
|
||||||
|
style={{ minHeight: "70vh" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image Viewer */}
|
||||||
|
{viewingFile.mimeType?.startsWith("image/") && (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<img
|
||||||
|
src={fileViewUrl}
|
||||||
|
alt={viewingFile.name}
|
||||||
|
className="max-w-full max-h-full object-contain rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Text/Code Viewer */}
|
||||||
|
{(viewingFile.mimeType?.startsWith("text/") ||
|
||||||
|
viewingFile.mimeType === "application/json" ||
|
||||||
|
viewingFile.mimeType === "application/javascript") && (
|
||||||
|
<iframe
|
||||||
|
src={fileViewUrl}
|
||||||
|
className="w-full h-full min-h-[70vh] border rounded bg-white"
|
||||||
|
title={viewingFile.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Unsupported file type */}
|
||||||
|
{!viewingFile.mimeType?.startsWith("image/") &&
|
||||||
|
viewingFile.mimeType !== "application/pdf" &&
|
||||||
|
!viewingFile.mimeType?.startsWith("text/") &&
|
||||||
|
viewingFile.mimeType !== "application/json" &&
|
||||||
|
viewingFile.mimeType !== "application/javascript" && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-gray-500">
|
||||||
|
<FileText className="h-16 w-16 mb-4" />
|
||||||
|
<p className="text-lg font-medium">
|
||||||
|
Preview not available
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
This file type cannot be previewed
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/cloud-storage/files/${viewingFile.id}/url`,
|
||||||
|
{
|
||||||
|
credentials: "include",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!res.ok)
|
||||||
|
throw new Error("Failed to get file URL");
|
||||||
|
const { url } = await res.json();
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to download file.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
Download File
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
package-lock.json
generated
260
package-lock.json
generated
@@ -13,7 +13,7 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.13.0",
|
"@prisma/client": "^6.16.2",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"decimal.js": "^10.6.0",
|
"decimal.js": "^10.6.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"prisma": "^6.13.0",
|
"prisma": "^6.16.2",
|
||||||
"turbo": "^2.5.3"
|
"turbo": "^2.5.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1534,9 +1534,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.2.tgz",
|
||||||
"integrity": "sha512-8m2+I3dQovkV8CkDMluiwEV1TxV9EXdT6xaCz39O6jYw7mkf5gwfmi+cL4LJsEPwz5tG7sreBwkRpEMJedGYUQ==",
|
"integrity": "sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1556,56 +1556,56 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz",
|
||||||
"integrity": "sha512-OYMM+pcrvj/NqNWCGESSxVG3O7kX6oWuGyvufTUNnDw740KIQvNyA4v0eILgkpuwsKIDU36beZCkUtIt0naTog==",
|
"integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "3.1.0",
|
"c12": "3.1.0",
|
||||||
"deepmerge-ts": "7.1.5",
|
"deepmerge-ts": "7.1.5",
|
||||||
"effect": "3.16.12",
|
"effect": "3.16.12",
|
||||||
"read-package-up": "11.0.0"
|
"empathic": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.2.tgz",
|
||||||
"integrity": "sha512-um+9pfKJW0ihmM83id9FXGi5qEbVJ0Vxi1Gm0xpYsjwUBnw6s2LdPBbrsG9QXRX46K4CLWCTNvskXBup4i9hlw==",
|
"integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.2.tgz",
|
||||||
"integrity": "sha512-D+1B79LFvtWA0KTt8ALekQ6A/glB9w10ETknH5Y9g1k2NYYQOQy93ffiuqLn3Pl6IPJG3EsK/YMROKEaq8KBrA==",
|
"integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.13.0",
|
"@prisma/debug": "6.16.2",
|
||||||
"@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
|
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"@prisma/fetch-engine": "6.13.0",
|
"@prisma/fetch-engine": "6.16.2",
|
||||||
"@prisma/get-platform": "6.13.0"
|
"@prisma/get-platform": "6.16.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
|
"version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz",
|
||||||
"integrity": "sha512-MpPyKSzBX7P/ZY9odp9TSegnS/yH3CSbchQE9f0yBg3l2QyN59I6vGXcoYcqKC9VTniS1s18AMmhyr1OWavjHg==",
|
"integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz",
|
||||||
"integrity": "sha512-grmmq+4FeFKmaaytA8Ozc2+Tf3BC8xn/DVJos6LL022mfRlMZYjT3hZM0/xG7+5fO95zFG9CkDUs0m1S2rXs5Q==",
|
"integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.13.0",
|
"@prisma/debug": "6.16.2",
|
||||||
"@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd",
|
"@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43",
|
||||||
"@prisma/get-platform": "6.13.0"
|
"@prisma/get-platform": "6.16.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/generator-helper": {
|
"node_modules/@prisma/generator-helper": {
|
||||||
@@ -1669,13 +1669,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz",
|
||||||
"integrity": "sha512-Nii2pX50fY4QKKxQwm7/vvqT6Ku8yYJLZAFX4e2vzHwRdMqjugcOG5hOSLjxqoXb0cvOspV70TOhMzrw8kqAnw==",
|
"integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.13.0"
|
"@prisma/debug": "6.16.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/internals": {
|
"node_modules/@prisma/internals": {
|
||||||
@@ -6745,6 +6745,16 @@
|
|||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/empathic": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@@ -7479,19 +7489,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/find-up-simple": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/flat-cache": {
|
"node_modules/flat-cache": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||||
@@ -8274,19 +8271,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/index-to-position": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@@ -9545,9 +9529,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.6.6",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
"integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==",
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -9717,16 +9701,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
||||||
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
|
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.1.6",
|
"citty": "^0.1.6",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"pkg-types": "^2.2.0",
|
"pkg-types": "^2.3.0",
|
||||||
"tinyexec": "^1.0.1"
|
"tinyexec": "^1.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -10361,9 +10345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10678,15 +10662,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.13.0",
|
"version": "6.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz",
|
||||||
"integrity": "sha512-dfzORf0AbcEyyzxuv2lEwG8g+WRGF/qDQTpHf/6JoHsyF5MyzCEZwClVaEmw3WXcobgadosOboKUgQU0kFs9kw==",
|
"integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.13.0",
|
"@prisma/config": "6.16.2",
|
||||||
"@prisma/engines": "6.13.0"
|
"@prisma/engines": "6.16.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
@@ -11237,123 +11221,6 @@
|
|||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/read-package-up": {
|
|
||||||
"version": "11.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
|
|
||||||
"integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"find-up-simple": "^1.0.0",
|
|
||||||
"read-pkg": "^9.0.0",
|
|
||||||
"type-fest": "^4.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/hosted-git-info": {
|
|
||||||
"version": "7.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
|
|
||||||
"integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^10.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^16.14.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/lru-cache": {
|
|
||||||
"version": "10.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/normalize-package-data": {
|
|
||||||
"version": "6.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
|
|
||||||
"integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"hosted-git-info": "^7.0.0",
|
|
||||||
"semver": "^7.3.5",
|
|
||||||
"validate-npm-package-license": "^3.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^16.14.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/parse-json": {
|
|
||||||
"version": "8.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
|
||||||
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/code-frame": "^7.26.2",
|
|
||||||
"index-to-position": "^1.1.0",
|
|
||||||
"type-fest": "^4.39.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/read-pkg": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/normalize-package-data": "^2.4.3",
|
|
||||||
"normalize-package-data": "^6.0.0",
|
|
||||||
"parse-json": "^8.0.0",
|
|
||||||
"type-fest": "^4.6.0",
|
|
||||||
"unicorn-magic": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/node_modules/semver": {
|
|
||||||
"version": "7.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-package-up/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/read-pkg": {
|
"node_modules/read-pkg": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||||
@@ -13175,19 +13042,6 @@
|
|||||||
"tiny-inflate": "^1.0.0"
|
"tiny-inflate": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unicorn-magic": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
|
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unique-string": {
|
"node_modules/unique-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"prisma": "^6.13.0",
|
"prisma": "^6.16.2",
|
||||||
"turbo": "^2.5.3"
|
"turbo": "^2.5.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.13.0",
|
"@prisma/client": "^6.16.2",
|
||||||
"@reduxjs/toolkit": "^2.8.2",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"decimal.js": "^10.6.0",
|
"decimal.js": "^10.6.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ model User {
|
|||||||
updatedPayments Payment[] @relation("PaymentUpdatedBy")
|
updatedPayments Payment[] @relation("PaymentUpdatedBy")
|
||||||
backups DatabaseBackup[]
|
backups DatabaseBackup[]
|
||||||
notifications Notification[]
|
notifications Notification[]
|
||||||
|
cloudFolders CloudFolder[]
|
||||||
|
cloudFiles CloudFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Patient {
|
model Patient {
|
||||||
@@ -310,3 +312,50 @@ enum NotificationTypes {
|
|||||||
PAYMENT
|
PAYMENT
|
||||||
ETC
|
ETC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CloudFolder {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
name String
|
||||||
|
parentId Int?
|
||||||
|
parent CloudFolder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
|
||||||
|
children CloudFolder[] @relation("FolderChildren")
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
files CloudFile[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, parentId, name]) // prevents sibling folder name duplicates
|
||||||
|
@@index([parentId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CloudFile {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
userId Int
|
||||||
|
name String
|
||||||
|
mimeType String?
|
||||||
|
fileSize BigInt @db.BigInt
|
||||||
|
folderId Int? // optional: null => root
|
||||||
|
isComplete Boolean @default(false) // upload completed?
|
||||||
|
totalChunks Int? // optional: expected number of chunks
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
folder CloudFolder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
chunks CloudFileChunk[]
|
||||||
|
|
||||||
|
@@index([folderId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CloudFileChunk {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
fileId Int
|
||||||
|
seq Int
|
||||||
|
data Bytes
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
file CloudFile @relation(fields: [fileId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([fileId, seq])
|
||||||
|
@@index([fileId, seq])
|
||||||
|
}
|
||||||
|
|||||||
7
packages/db/types/cloudStorage-types.ts
Normal file
7
packages/db/types/cloudStorage-types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { CloudFolderUncheckedCreateInputObjectSchema, CloudFileUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export type CloudFolder = z.infer<typeof CloudFolderUncheckedCreateInputObjectSchema>;
|
||||||
|
export type CloudFile = z.infer<typeof CloudFileUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
|
|
||||||
@@ -8,3 +8,4 @@ export * from "./staff-types";
|
|||||||
export * from "./user-types";
|
export * from "./user-types";
|
||||||
export * from "./databaseBackup-types";
|
export * from "./databaseBackup-types";
|
||||||
export * from "./notifications-types";
|
export * from "./notifications-types";
|
||||||
|
export * from "./cloudStorage-types";
|
||||||
@@ -15,3 +15,5 @@ export * from '../shared/schemas/enums/PaymentStatus.schema'
|
|||||||
export * from '../shared/schemas/enums/NotificationTypes.schema'
|
export * from '../shared/schemas/enums/NotificationTypes.schema'
|
||||||
export * from '../shared/schemas/objects/NotificationUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/NotificationUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/DatabaseBackupUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/DatabaseBackupUncheckedCreateInput.schema'
|
||||||
|
export * from '../shared/schemas/objects/CloudFolderUncheckedCreateInput.schema'
|
||||||
|
export * from '../shared/schemas/objects/CloudFileUncheckedCreateInput.schema'
|
||||||
Reference in New Issue
Block a user