Finish working on backups side deletion feature, to be review, test and deply in RC for tests.

This commit is contained in:
charlesgauthereau
2025-09-22 20:31:29 +02:00
parent 34ff11f377
commit 654ff6b9a5
10 changed files with 426 additions and 185 deletions
@@ -148,7 +148,8 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id} <TableRow key={row.id}
className={highlightRow && highlightRow(row.original) ? "bg-gray-100 pointer-events-none" : ""} className={highlightRow && highlightRow(row.original) ? "bg-gray-100 pointer-events-none" : ""}
data-state={row.getIsSelected() && "selected"}> data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell> key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
@@ -0,0 +1,66 @@
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {Toggle} from "@/components/ui/toggle";
import {CheckIcon, Filter, RefreshCcw} from "lucide-react";
import {Badge} from "@/components/ui/badge";
export type FilterItem = {
label: string,
value: string
}
type FiltersDropdownProps = {
items: FilterItem[];
selectedItems: FilterItem[];
onSelect: (item: FilterItem) => void;
clearFilters: () => void;
}
export const FiltersDropdown = ({items, selectedItems, onSelect, clearFilters}: FiltersDropdownProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="relative">
<Toggle variant="outline" size="sm" className="cursor-pointer w-fit">
<Filter className="h-4 w-4"/>
</Toggle>
{selectedItems.length > 0 && (
<Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 flex items-center justify-center text-[10px]">
{selectedItems.length}
</Badge>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
{items.map((item, index) => {
const isSelected = selectedItems.some(f => f.value === item.value);
return (
<DropdownMenuItem
key={index}
className="flex items-center justify-between cursor-pointer"
onClick={(e) => {
e.preventDefault();
onSelect(item);
}}
>
<span>{item.label}</span>
{isSelected && <CheckIcon className="h-4 w-4 text-blue-500"/>}
</DropdownMenuItem>
)
})}
<DropdownMenuItem
disabled={selectedItems.length === 0}
className="flex gap-2 cursor-pointer"
onClick={(e) => {
e.preventDefault();
clearFilters();
}}
>
<RefreshCcw className='h-4 w-4'/>
Clear filters
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,146 @@
"use client"
import {backupColumns} from "@/features/dashboard/backup/columns";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {MoreHorizontal, Trash2} from "lucide-react";
import {FilterItem, FiltersDropdown} from "@/components/wrappers/common/table/filters";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {useMemo, useState} from "react";
import {Backup, DatabaseWith} from "@/db/schema/07_database";
import {Setting} from "@/db/schema/01_setting";
import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
type DatabaseBackupListProps = {
isAlreadyRestore: boolean;
settings: Setting;
database: DatabaseWith;
backups: Backup[];
}
export const DatabaseBackupList = (props: DatabaseBackupListProps) => {
const items = [
{label: "Deleted", value: "deleted"},
{label: "Available", value: "available"},
]
const [selectedFilters, setSelectedFilters] = useState<FilterItem[]>([items[1]]);
const router = useRouter();
const filteredBackups = useMemo(() => {
if (!props.backups) return [];
return props.backups.filter(backup => {
// --- Status Filter ---
if (selectedFilters.length > 0) {
const selectedValues = selectedFilters.map(f => f.value);
const status = backup.deletedAt != null ? "deleted" : "available";
if (!selectedValues.includes(status)) return false;
}
return true;
});
}, [props.backups, selectedFilters]);
const handleSelectFilter = (item: FilterItem) => {
setSelectedFilters(prev =>
prev.some(f => f.value === item.value)
? prev.filter(f => f.value !== item.value)
: [...prev, item]
);
};
const clearFilters = () => setSelectedFilters([]);
const mutationDeleteBackups = useMutation({
mutationFn: async (backups: Backup[]) => {
const results = await Promise.all(
backups.map(async (backup) => {
if (backup.deletedAt == null) {
const backupDeleted = await deleteBackupAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: props.database?.project?.slug!
});
return {
success: backupDeleted?.data?.success,
message: backupDeleted?.data?.success
? backupDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restoration?.data?.actionError.message,
};
}
return {
success: true,
message: `Already deleted this backup (ref: ${backup.id}).`,
}
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return (
<DataTable
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)}
data={filteredBackups}
enablePagination
selectedActions={(rows) => (
<div className="flex justify-start md:justify-between gap-3 md:gap-0 items-center w-full ml-0">
<div className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteBackups.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<FiltersDropdown
items={items}
selectedItems={selectedFilters}
onSelect={handleSelectFilter}
clearFilters={clearFilters}
/>
</div>
</div>
)}
/>
)
}
@@ -0,0 +1,87 @@
"use client"
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {MoreHorizontal, Trash2} from "lucide-react";
import {Restoration} from "@/db/schema/07_database";
import {useMutation} from "@tanstack/react-query";
import {deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
type DatabaseRestoreListProps = {
isAlreadyRestore: boolean;
restorations: Restoration[];
}
export const DatabaseRestoreList = (props: DatabaseRestoreListProps) => {
const router = useRouter();
const mutationDeleteRestorations = useMutation({
mutationFn: async (restorations: Restoration[]) => {
const results = await Promise.all(
restorations.map(async (restoration) => {
const restorationDeleted = await deleteRestoreAction({
restorationId: restoration.id,
});
return {
success: restorationDeleted?.data?.success,
message: restorationDeleted?.data?.success
? restorationDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restorationDeleted?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return (
<DataTable
columns={restoreColumns(props.isAlreadyRestore)}
data={props.restorations}
enablePagination
selectedActions={(rows) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteRestorations.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
await mutationDeleteRestorations.mutateAsync(rows)
}}
disabled={props.isAlreadyRestore}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
/>
)
}
@@ -4,17 +4,10 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useRouter, useSearchParams} from "next/navigation"; import {useRouter, useSearchParams} from "next/navigation";
import {eventUpdate} from "@/types/events"; import {eventUpdate} from "@/types/events";
import {backupColumns} from "@/features/dashboard/backup/columns";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database"; import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database";
import {Setting} from "@/db/schema/01_setting"; import {Setting} from "@/db/schema/01_setting";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"; import {DatabaseBackupList} from "@/components/wrappers/dashboard/projects/database/database-backup-list";
import {MoreHorizontal, Trash2} from "lucide-react"; import {DatabaseRestoreList} from "@/components/wrappers/dashboard/projects/database/database-restore-list";
import {useMutation} from "@tanstack/react-query";
import {deleteBackupAction, deleteRestoreAction} from "@/features/dashboard/restore/restore.action";
import {toast} from "sonner";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
export type DatabaseTabsProps = { export type DatabaseTabsProps = {
settings: Setting settings: Setting
@@ -44,7 +37,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
const newTab = searchParams.get("tab") ?? "backup"; const newTab = searchParams.get("tab") ?? "backup";
setTab(newTab); setTab(newTab);
@@ -54,65 +46,6 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
router.push(`?tab=${value}`); router.push(`?tab=${value}`);
}; };
const mutationDeleteBackups = useMutation({
mutationFn: async (backups: Backup[]) => {
const results = await Promise.all(
backups.map(async (backup) => {
const backupDeleted = await deleteBackupAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: props.database?.project?.slug!
});
return {
success: backupDeleted?.data?.success,
message: backupDeleted?.data?.success
? backupDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restoration?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
const mutationDeleteRestorations = useMutation({
mutationFn: async (restorations: Restoration[]) => {
const results = await Promise.all(
restorations.map(async (restoration) => {
const restorationDeleted = await deleteRestoreAction({
restorationId: restoration.id,
});
return {
success: restorationDeleted?.data?.success,
message: restorationDeleted?.data?.success
? restorationDeleted?.data?.actionSuccess?.message
// @ts-ignore
: restorationDeleted?.data?.actionError.message,
};
})
);
results.forEach((result) => {
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
});
router.refresh();
},
});
return ( return (
<Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}> <Tabs className="flex flex-col flex-1" value={tab} onValueChange={handleChangeTab}>
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
@@ -120,83 +53,17 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
<TabsTrigger value="restore">Restoration</TabsTrigger> <TabsTrigger value="restore">Restoration</TabsTrigger>
</TabsList> </TabsList>
<TabsContent className="h-full justify-between" value="backup"> <TabsContent className="h-full justify-between" value="backup">
<DataTable <DatabaseBackupList
columns={backupColumns(props.isAlreadyRestore, props.settings, props.database)} isAlreadyRestore={props.isAlreadyRestore}
data={props.backups} settings={props.settings}
enablePagination database={props.database}
selectedActions={(rows) => ( backups={props.backups}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteBackups.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteBackups.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{/*<DropdownMenuItem*/}
{/* onClick={() => {*/}
{/* console.log("Deleting rows:", rows)*/}
{/* }}*/}
{/*>*/}
{/* <Download className="w-4 h-4 mr-2"/>*/}
{/* Download Selected*/}
{/*</DropdownMenuItem>*/}
{/*<Separator/>*/}
<DropdownMenuItem
onClick={async () => {
console.log("Deleting rows:", rows)
await mutationDeleteBackups.mutateAsync(rows)
}}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
/> />
</TabsContent> </TabsContent>
<TabsContent className="h-full justify-between" value="restore"> <TabsContent className="h-full justify-between" value="restore">
<DataTable <DatabaseRestoreList
columns={restoreColumns(props.isAlreadyRestore)} isAlreadyRestore={props.isAlreadyRestore}
data={props.restorations} restorations={props.restorations}
enablePagination
selectedActions={(rows) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithLoading
variant="outline"
text="Actions"
onClick={() => {
}}
disabled={rows.length === 0 || mutationDeleteRestorations.isPending}
icon={<MoreHorizontal/>}
isPending={mutationDeleteRestorations.isPending}
size="sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
onClick={async () => {
await mutationDeleteRestorations.mutateAsync(rows)
}}
disabled={props.isAlreadyRestore}
className="text-red-600 focus:text-red-700"
>
<Trash2 className="w-4 h-4 mr-2"/>
Delete Selected
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
+29 -9
View File
@@ -28,23 +28,43 @@ import {Setting} from "@/db/schema/01_setting";
import {SafeActionResult} from "next-safe-action"; import {SafeActionResult} from "next-safe-action";
import {ZodString} from "zod"; import {ZodString} from "zod";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {cn} from "@/lib/utils";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef<Backup>[] { export function backupColumns(isAlreadyRestore: boolean, settings: Setting, database: DatabaseWith): ColumnDef<Backup>[] {
return [ return [
{
id: "availability",
cell: ({row}) => {
const colorStatus = row.original.deletedAt != null ? "bg-red-400 border-red-600" : "bg-green-400 border-green-600";
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={cn("w-5 h-5 rounded-full border-4", colorStatus)}/>
</TooltipTrigger>
{row.original.deletedAt != null && (
<TooltipContent>
<p>{formatFrenchDate(row.getValue("deletedAt"))}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
},
},
{ {
accessorKey: "id", accessorKey: "id",
header: "Reference", header: "Reference",
}, },
{ // {
accessorKey: "deletedAt", // accessorKey: "deletedAt",
header: "Deleted At", // header: "Deleted At",
cell: ({row}) => { // cell: ({row}) => {
// return row.original.deletedAt ? formatFrenchDate(row.getValue("deletedAt")) : "-"
return row.original.deletedAt ? formatFrenchDate(row.getValue("deletedAt")) : "" // },
}, // },
},
{ {
accessorKey: "createdAt", accessorKey: "createdAt",
header: "Created At", header: "Created At",
@@ -15,6 +15,7 @@ import {
uploadS3Private uploadS3Private
} from "@/features/upload/private/upload.action"; } from "@/features/upload/private/upload.action";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {withUpdatedAt} from "@/db/utils";
export const deleteRestoreAction = userAction export const deleteRestoreAction = userAction
.schema( .schema(
@@ -63,6 +64,7 @@ export const deleteBackupAction = userAction
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => { .action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try { try {
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1); const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) { if (!settings) {
return { return {
@@ -76,6 +78,12 @@ export const deleteBackupAction = userAction
}; };
} }
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({
deletedAt: new Date(),
}))
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
let success: boolean, message: string; let success: boolean, message: string;
@@ -98,36 +106,79 @@ export const deleteBackupAction = userAction
}; };
} }
await db return {
.delete(drizzleDb.schemas.backup) success: true,
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId))) actionSuccess: {
.execute(); message: `Backup deleted successfully (ref: ${parsedInput.backupId}).`,
},
const backupExists = await db };
.select()
.from(drizzleDb.schemas.backup)
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
.execute();
if (backupExists.length === 0) { // const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
return { // if (!settings) {
success: true, // return {
actionSuccess: { // success: false,
message: "Backup deleted successfully.", // actionError: {
}, // message: "No settings found.",
}; // status: 404,
} else { // cause: "No settings found.",
return { // messageParams: {message: "Error deleting the backup"},
success: false, // },
actionError: { // };
message: "Backup not found or already deleted.", // }
status: 404, //
cause: "Backup could not be deleted (from database or remote storage).", //
messageParams: {message: "Error deleting the backup"}, // let success: boolean, message: string;
}, //
}; // const result =
} // settings.storage === "local"
// ? await deleteLocalPrivate(parsedInput.file)
// : await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
//
// ({success, message} = result);
//
// if (!success) {
// return {
// success: false,
// actionError: {
// message: message,
// status: 404,
// cause: "Unable to delete backup from storage",
// messageParams: {message: "Error deleting the backup"},
// },
// };
// }
//
// await db
// .delete(drizzleDb.schemas.backup)
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
// .execute();
//
// const backupExists = await db
// .select()
// .from(drizzleDb.schemas.backup)
// .where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
// .execute();
//
//
// if (backupExists.length === 0) {
// return {
// success: true,
// actionSuccess: {
// message: "Backup deleted successfully.",
// },
// };
// } else {
// return {
// success: false,
// actionError: {
// message: "Backup not found or already deleted.",
// status: 404,
// cause: "Backup could not be deleted (from database or remote storage).",
// messageParams: {message: "Error deleting the backup"},
// },
// };
// }
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
+2
View File
@@ -5,6 +5,7 @@ import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf";
import {retentionPolicy} from "@/db/schema/07_database"; import {retentionPolicy} from "@/db/schema/07_database";
import {eq, isNull} from "drizzle-orm"; import {eq, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {eventEmitter} from "../../../../app/api/events/route";
export const retentionCleanTask = async () => { export const retentionCleanTask = async () => {
@@ -54,4 +55,5 @@ export async function enforceRetention(
}); });
break; break;
} }
eventEmitter.emit('modification', {update: true});
} }
+3 -2
View File
@@ -1,5 +1,5 @@
import {db} from "@/db"; import {db} from "@/db";
import {eq, lt, and, desc} from "drizzle-orm"; import {eq, lt, and, desc, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete"; import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
@@ -9,7 +9,8 @@ export async function enforceRetentionDays(databaseId: string, days: number) {
const expiredBackups = await db.query.backup.findMany({ const expiredBackups = await db.query.backup.findMany({
where: and( where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseId), eq(drizzleDb.schemas.backup.databaseId, databaseId),
lt(drizzleDb.schemas.backup.createdAt, cutoff) lt(drizzleDb.schemas.backup.createdAt, cutoff),
isNull(drizzleDb.schemas.backup.deletedAt)
), ),
with: { with: {
database: { database: {
+2 -2
View File
@@ -1,6 +1,6 @@
import {db} from "@/db"; import {db} from "@/db";
import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns"; import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns";
import {eq, desc} from "drizzle-orm"; import {eq, desc, isNull, and} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete"; import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
@@ -11,7 +11,7 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
yearly: number; yearly: number;
}) { }) {
const backups = await db.query.backup.findMany({ const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, databaseId), where: and(eq(drizzleDb.schemas.backup.databaseId, databaseId), isNull(drizzleDb.schemas.backup.deletedAt)),
orderBy: desc(drizzleDb.schemas.backup.createdAt), orderBy: desc(drizzleDb.schemas.backup.createdAt),
with: { with: {
database: { database: {