From 2f9e33f4dba02f3962e3400c662fde61f467524c Mon Sep 17 00:00:00 2001 From: charlesgauthereau Date: Wed, 27 Aug 2025 21:16:13 +0200 Subject: [PATCH] Patching a bug when delete an organization, add refetch. --- .../database/[databaseId]/page.tsx | 3 + .../admin-email-tab/email-form/email-form.tsx | 59 +- .../admin-email-tab/settings-email-tab.tsx | 3 +- .../storage-s3/storage-s3-form.tsx | 3 +- .../backup-retention-settings.tsx | 321 +++++ .../retention-policy-sheet.tsx | 25 + src/db/migrations/0003_absent_maestro.sql | 16 + src/db/migrations/meta/0003_snapshot.json | 1264 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/06_database.ts | 25 + src/lib/tasks/database/index.ts | 50 + src/lib/tasks/database/retention-count.ts | 17 + src/lib/tasks/database/retention-days.ts | 14 + src/lib/tasks/database/retention-gsf.ts | 59 + src/lib/tasks/index.ts | 2 +- src/lib/tasks/project.ts | 12 - 16 files changed, 1842 insertions(+), 38 deletions(-) create mode 100644 src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx create mode 100644 src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx create mode 100644 src/db/migrations/0003_absent_maestro.sql create mode 100644 src/db/migrations/meta/0003_snapshot.json create mode 100644 src/lib/tasks/database/index.ts create mode 100644 src/lib/tasks/database/retention-count.ts create mode 100644 src/lib/tasks/database/retention-days.ts create mode 100644 src/lib/tasks/database/retention-gsf.ts delete mode 100644 src/lib/tasks/project.ts diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index 788407d5..fe9b77c1 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -12,6 +12,7 @@ import {eq, and, inArray} from "drizzle-orm"; import * as drizzleDb from "@/db"; import {getOrganizationProjectDatabases} from "@/lib/services"; import {getOrganization} from "@/lib/auth/auth"; +import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet"; export default async function RoutePage(props: PageParams<{ projectId: string; @@ -89,9 +90,11 @@ export default async function RoutePage(props: PageParams<{ {dbItem.name} + + {dbItem.description} diff --git a/src/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.tsx b/src/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.tsx index 5d505570..01ad0b45 100644 --- a/src/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.tsx +++ b/src/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.tsx @@ -1,18 +1,31 @@ "use client"; -import { Card, CardContent } from "@/components/ui/card"; -import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Form } from "@/components/ui/form"; -import { Button } from "@/components/ui/button"; -import { useMutation } from "@tanstack/react-query"; -import { TooltipProvider } from "@/components/ui/tooltip"; +import {Card, CardContent} from "@/components/ui/card"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + useZodForm +} from "@/components/ui/form"; +import {Input} from "@/components/ui/input"; +import {Form} from "@/components/ui/form"; +import {Button} from "@/components/ui/button"; +import {useMutation} from "@tanstack/react-query"; +import {TooltipProvider} from "@/components/ui/tooltip"; -import { EmailFormSchema, EmailFormType } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema"; -import { PasswordInput } from "@/components/wrappers/auth/password-input/password-input"; -import { updateEmailSettingsAction } from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action"; -import { toast } from "sonner"; -import { useRouter } from "next/navigation"; +import { + EmailFormSchema, + EmailFormType +} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema"; +import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input"; +import { + updateEmailSettingsAction +} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.action"; +import {toast} from "sonner"; +import {useRouter} from "next/navigation"; export type EmailFormProps = { defaultValues?: EmailFormType; @@ -27,7 +40,7 @@ export const EmailForm = (props: EmailFormProps) => { const mutation = useMutation({ mutationFn: async (values: EmailFormType) => { - const updateEmailSettings = await updateEmailSettingsAction({ name: "system", data: values }); + const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values}); const data = updateEmailSettings?.data?.data; if (updateEmailSettings?.serverError || !data) { toast.error(updateEmailSettings?.serverError); @@ -53,14 +66,14 @@ export const EmailForm = (props: EmailFormProps) => { control={form.control} name="smtpFrom" defaultValue="" - render={({ field }) => ( + render={({field}) => ( From Email * {"The email from where the email will be send"} - + )} /> @@ -68,14 +81,14 @@ export const EmailForm = (props: EmailFormProps) => { control={form.control} name="smtpHost" defaultValue="" - render={({ field }) => ( + render={({field}) => ( Server Host * {"Your email server host"} - + )} /> @@ -83,14 +96,14 @@ export const EmailForm = (props: EmailFormProps) => { control={form.control} name="smtpPort" defaultValue="" - render={({ field }) => ( + render={({field}) => ( Server Port * {"Your email server port (send)"} - + )} /> @@ -99,7 +112,7 @@ export const EmailForm = (props: EmailFormProps) => { control={form.control} name="smtpPassword" defaultValue="" - render={({ field }) => ( + render={({field}) => ( Password @@ -107,7 +120,7 @@ export const EmailForm = (props: EmailFormProps) => { {"Your email server password"} - + )} /> @@ -115,14 +128,14 @@ export const EmailForm = (props: EmailFormProps) => { control={form.control} name="smtpUser" defaultValue="" - render={({ field }) => ( + render={({field}) => ( User Email * {"The email server user"} - + )} /> diff --git a/src/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab.tsx b/src/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab.tsx index 02b725a5..420d9872 100644 --- a/src/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab.tsx +++ b/src/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab.tsx @@ -7,6 +7,7 @@ import TestEmailSettings from "../../../../../../emails/TestEmailSettings"; import { render } from "@react-email/render"; import { toast } from "sonner"; import {Setting} from "@/db/schema/00_setting"; +import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema"; export type SettingsEmailTabProps = { settings: Setting; @@ -53,7 +54,7 @@ export const SettingsEmailTab = (props: SettingsEmailTabProps) => { )}
- +
); diff --git a/src/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form.tsx b/src/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form.tsx index 734a3217..2cdefa6f 100644 --- a/src/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form.tsx +++ b/src/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/storage-s3-form.tsx @@ -12,6 +12,7 @@ import { S3FormSchema, S3FormType } from "@/components/wrappers/dashboard/admin/ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { updateS3SettingsAction } from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action"; +import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input"; export type S3FormProps = { defaultValues?: S3FormType; @@ -86,7 +87,7 @@ export const StorageS3Form = (props: S3FormProps) => { Secret Key * - + {"Add your secret key"} diff --git a/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx new file mode 100644 index 00000000..744a519d --- /dev/null +++ b/src/components/wrappers/dashboard/database/retention-policy/backup-retention-settings.tsx @@ -0,0 +1,321 @@ +"use client" + +import { useState } from "react" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { Clock, Database, Save, Settings, Calendar, RotateCcw } from "lucide-react" +import {toast} from "sonner"; + +type RetentionPolicyType = "count" | "days" | "gfs" + +interface GFSSettings { + daily: number + weekly: number + monthly: number + yearly: number +} + +interface RetentionSettings { + type: RetentionPolicyType + count: number + days: number + gfs: GFSSettings +} + +export function BackupRetentionSettings() { + const [settings, setSettings] = useState({ + type: "gfs", + count: 7, + days: 30, + gfs: { + daily: 7, + weekly: 4, + monthly: 12, + yearly: 3, + }, + }) + const [isLoading, setIsLoading] = useState(false) + + const handleSave = async () => { + setIsLoading(true) + try { + await new Promise((resolve) => setTimeout(resolve, 1000)) + toast.success("Your backup retention settings have been saved successfully.") + } catch (error) { + toast.error("Failed to save retention settings") + } finally { + setIsLoading(false) + } + } + + const calculateTotalFiles = () => { + if (settings.type === "gfs") { + return settings.gfs.daily + settings.gfs.weekly + settings.gfs.monthly + settings.gfs.yearly + } + return settings.type === "count" ? settings.count : Math.ceil(settings.days / 1) // Assuming daily backups + } + + const getStorageEstimate = () => { + const totalFiles = calculateTotalFiles() + if (totalFiles <= 10) return "Low" + if (totalFiles <= 30) return "Medium" + return "High" + } + + return ( +
+ + + + + Backup Retention Policy + + + Configure how long to keep your .dump backup files. Choose from simple count-based, time-based, or + enterprise GFS rotation strategies. + + + +
+ + setSettings((prev) => ({ ...prev, type: value }))} + className="grid grid-cols-1 gap-4" + > +
+ +
+ +

+ Simple count-based retention (e.g., keep last 10 backups) +

+
+
+ +
+ +
+ +

Time-based retention (e.g., keep backups for 30 days)

+
+
+ +
+ +
+ +

+ Grandfather-Father-Son rotation for enterprise/critical systems +

+
+
+
+
+ + + + {settings.type === "count" && ( +
+
+ + +
+
+ + setSettings((prev) => ({ ...prev, count: Number.parseInt(e.target.value) || 1 }))} + className="w-32" + /> +

+ Older backups beyond this count will be automatically deleted +

+
+
+ )} + + {settings.type === "days" && ( +
+
+ + +
+
+ + setSettings((prev) => ({ ...prev, days: Number.parseInt(e.target.value) || 1 }))} + className="w-32" + /> +

+ Backups older than {settings.days} days will be automatically deleted +

+
+
+ )} + + {settings.type === "gfs" && ( +
+
+ + +
+
+
+ + + setSettings((prev) => ({ + ...prev, + gfs: { ...prev.gfs, daily: Number.parseInt(e.target.value) || 1 }, + })) + } + /> +

Keep last N daily backups

+
+ +
+ + + setSettings((prev) => ({ + ...prev, + gfs: { ...prev.gfs, weekly: Number.parseInt(e.target.value) || 0 }, + })) + } + /> +

Keep N weekly backups

+
+ +
+ + + setSettings((prev) => ({ + ...prev, + gfs: { ...prev.gfs, monthly: Number.parseInt(e.target.value) || 0 }, + })) + } + /> +

Keep N monthly backups

+
+ +
+ + + setSettings((prev) => ({ + ...prev, + gfs: { ...prev.gfs, yearly: Number.parseInt(e.target.value) || 0 }, + })) + } + /> +

Keep N yearly backups

+
+
+ +
+

GFS Strategy Benefits:

+
    +
  • • Balanced storage usage with long-term retention
  • +
  • • Automatic promotion of backups (daily → weekly → monthly → yearly)
  • +
  • • Industry standard for enterprise backup systems
  • +
+
+
+ )} + + + +
+
+
+ + Storage Impact Summary +
+ + {getStorageEstimate()} Usage + +
+ +
+
+ Estimated files per database: +

{calculateTotalFiles()} backup files

+
+
+ Policy type: +

+ {settings.type === "gfs" ? "GFS Rotation" : settings.type === "count" ? "Count-based" : "Time-based"} +

+
+
+ + {settings.type === "gfs" && ( +
+ Note: With GFS rotation, you'll have approximately {calculateTotalFiles()} files per + database per year, providing excellent long-term retention with optimized storage usage. +
+ )} +
+ + +
+
+
+ ) +} diff --git a/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx b/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx new file mode 100644 index 00000000..be0544a5 --- /dev/null +++ b/src/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet.tsx @@ -0,0 +1,25 @@ +import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger} from "@/components/ui/sheet"; +import {Button} from "@/components/ui/button"; +import {DatabaseZap} from "lucide-react"; + + +export const RetentionPolicySheet = () => { + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account + and remove your data from our servers. + + + + + ) +} \ No newline at end of file diff --git a/src/db/migrations/0003_absent_maestro.sql b/src/db/migrations/0003_absent_maestro.sql new file mode 100644 index 00000000..814d09c9 --- /dev/null +++ b/src/db/migrations/0003_absent_maestro.sql @@ -0,0 +1,16 @@ +CREATE TYPE "public"."retention_policy_type" AS ENUM('count', 'days', 'gfs');--> statement-breakpoint +CREATE TABLE "retention_policies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "database_id" uuid NOT NULL, + "type" "retention_policy_type" NOT NULL, + "count" integer DEFAULT 7, + "days" integer DEFAULT 30, + "gfs_daily" integer DEFAULT 7, + "gfs_weekly" integer DEFAULT 4, + "gfs_monthly" integer DEFAULT 12, + "gfs_yearly" integer DEFAULT 3, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "retention_policies" ADD CONSTRAINT "retention_policies_database_id_databases_id_fk" FOREIGN KEY ("database_id") REFERENCES "public"."databases"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0003_snapshot.json b/src/db/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..f26e2a74 --- /dev/null +++ b/src/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,1264 @@ +{ + "id": "8237f895-ee7c-4d2c-8890-c5b088e46d63", + "prevId": "ca73d698-19da-458a-b07d-887f32f581d4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "storage": { + "name": "storage", + "type": "type_storage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "s3_endpoint_url": { + "name": "s3_endpoint_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_access_key_id": { + "name": "s3_access_key_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_secret_access_key": { + "name": "s3_secret_access_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "s3_bucket_name": { + "name": "s3_bucket_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_password": { + "name": "smtp_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_from": { + "name": "smtp_from", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_host": { + "name": "smtp_host", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "smtp_user": { + "name": "smtp_user", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_name_unique": { + "name": "settings_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_slug_unique": { + "name": "projects_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backups": { + "name": "backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "file": { + "name": "file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "backups_database_id_databases_id_fk": { + "name": "backups_database_id_databases_id_fk", + "tableFrom": "backups", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.databases": { + "name": "databases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_database_id": { + "name": "agent_database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dbms": { + "name": "dbms", + "type": "dbms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backup_policy": { + "name": "backup_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_waiting_for_backup": { + "name": "is_waiting_for_backup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_to_restore": { + "name": "backup_to_restore", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "databases_agent_id_agents_id_fk": { + "name": "databases_agent_id_agents_id_fk", + "tableFrom": "databases", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "databases_project_id_projects_id_fk": { + "name": "databases_project_id_projects_id_fk", + "tableFrom": "databases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restorations": { + "name": "restorations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'waiting'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restorations_backup_id_backups_id_fk": { + "name": "restorations_backup_id_backups_id_fk", + "tableFrom": "restorations", + "tableTo": "backups", + "columnsFrom": [ + "backup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "restorations_database_id_databases_id_fk": { + "name": "restorations_database_id_databases_id_fk", + "tableFrom": "restorations", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.retention_policies": { + "name": "retention_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "database_id": { + "name": "database_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "retention_policy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "days": { + "name": "days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30 + }, + "gfs_daily": { + "name": "gfs_daily", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 7 + }, + "gfs_weekly": { + "name": "gfs_weekly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 4 + }, + "gfs_monthly": { + "name": "gfs_monthly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 12 + }, + "gfs_yearly": { + "name": "gfs_yearly", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "retention_policies_database_id_databases_id_fk": { + "name": "retention_policies_database_id_databases_id_fk", + "tableFrom": "retention_policies", + "tableTo": "databases", + "columnsFrom": [ + "database_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_contact": { + "name": "last_contact", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "agents_slug_unique": { + "name": "agents_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.retention_policy_type": { + "name": "retention_policy_type", + "schema": "public", + "values": [ + "count", + "days", + "gfs" + ] + }, + "public.dbms_status": { + "name": "dbms_status", + "schema": "public", + "values": [ + "postgresql", + "mongodb" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "waiting", + "ongoing", + "failed", + "success" + ] + }, + "public.type_storage": { + "name": "type_storage", + "schema": "public", + "values": [ + "local", + "s3" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 34a191e7..9e13801f 100755 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1753720983182, "tag": "0002_pink_groot", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1756321312852, + "tag": "0003_absent_maestro", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/06_database.ts b/src/db/schema/06_database.ts index 066ad824..4fa0ad4c 100644 --- a/src/db/schema/06_database.ts +++ b/src/db/schema/06_database.ts @@ -24,6 +24,7 @@ export const database = pgTable("databases", { projectId: uuid("project_id") .references(() => project.id), + }); export const backup = pgTable( @@ -41,6 +42,23 @@ export const backup = pgTable( // (table) => [uniqueIndex("database_id_status_unique").on(table.databaseId, table.status)] ); +export const retentionPolicyType = pgEnum("retention_policy_type", ["count", "days", "gfs"]); + +export const retentionPolicy = pgTable("retention_policies", { + id: uuid("id").primaryKey().defaultRandom(), + databaseId: uuid("database_id").notNull().references(() => database.id, {onDelete: "cascade"}), + type: retentionPolicyType("type").notNull(), + count: integer("count").default(7), // for "count" + days: integer("days").default(30), // for "days" + gfsDaily: integer("gfs_daily").default(7), + gfsWeekly: integer("gfs_weekly").default(4), + gfsMonthly: integer("gfs_monthly").default(12), + gfsYearly: integer("gfs_yearly").default(3), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at"), +}); + + export const restoration = pgTable("restorations", { id: uuid("id").primaryKey().defaultRandom(), status: statusEnum("status").default("waiting").notNull(), @@ -53,6 +71,7 @@ export const restoration = pgTable("restorations", { }); export const databaseRelations = relations(database, ({one, many}) => ({ + retentionPolicy: one(retentionPolicy), agent: one(agent, {fields: [database.agentId], references: [agent.id]}), project: one(project, {fields: [database.projectId], references: [project.id]}), backups: many(backup), @@ -78,10 +97,16 @@ export type Backup = z.infer; export const restorationSchema = createSelectSchema(restoration); export type Restoration = z.infer; +export const retentionPolicySchema = createSelectSchema(retentionPolicy); +export type RetentionPolicy = z.infer; + + export type DatabaseWith = Database & { agent?: Agent | null; project?: Project | null; backups?: Backup[] | null; restorations?: Restoration[] | null; + retentionPolicy?: RetentionPolicy | null; + }; diff --git a/src/lib/tasks/database/index.ts b/src/lib/tasks/database/index.ts new file mode 100644 index 00000000..7a56637e --- /dev/null +++ b/src/lib/tasks/database/index.ts @@ -0,0 +1,50 @@ +import {db} from "@/db"; +import {enforceRetentionCount} from "@/lib/tasks/database/retention-count"; +import {enforceRetentionDays} from "@/lib/tasks/database/retention-days"; +import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf"; +import {retentionPolicy} from "@/db/schema/06_database"; + + +export const retentionCleanTask = async () => { + try { + const databases = await db.query.database.findMany({ + with: { + retentionPolicy: true, + backups: true, + }, + }); + + for (const db of databases) { + if (!db.retentionPolicy) continue; // no policy = skip + + await enforceRetention(db.id, db.retentionPolicy); + } + } catch (e: any) { + console.error("Retention cleanup failed:", e); + throw e; + } +}; + +export async function enforceRetention( + databaseId: string, + policy: typeof retentionPolicy.$inferSelect +) { + switch (policy.type) { + case "count": + await enforceRetentionCount(databaseId, policy.count ?? 7); + break; + + case "days": + await enforceRetentionDays(databaseId, policy.days ?? 30); + break; + + case "gfs": + await enforceRetentionGFS(databaseId, { + daily: policy.gfsDaily ?? 7, + weekly: policy.gfsWeekly ?? 4, + monthly: policy.gfsMonthly ?? 12, + yearly: policy.gfsYearly ?? 3, + }); + break; + } +} diff --git a/src/lib/tasks/database/retention-count.ts b/src/lib/tasks/database/retention-count.ts new file mode 100644 index 00000000..d3c47d7b --- /dev/null +++ b/src/lib/tasks/database/retention-count.ts @@ -0,0 +1,17 @@ +import * as drizzleDb from "@/db"; +import {db} from "@/db"; +import {desc, eq} from "drizzle-orm"; + +export async function enforceRetentionCount(databaseId: string, count: number) { + const backups = await db.query.backup.findMany({ + where: eq(drizzleDb.schemas.backup.databaseId, databaseId), + orderBy: desc(drizzleDb.schemas.backup.createdAt), + }); + + const toDelete = backups.slice(count); // keep first `count`, delete rest + + for (const b of toDelete) { + await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id)); + // TODO: delete backup file from storage + } +} \ No newline at end of file diff --git a/src/lib/tasks/database/retention-days.ts b/src/lib/tasks/database/retention-days.ts new file mode 100644 index 00000000..c233e8bc --- /dev/null +++ b/src/lib/tasks/database/retention-days.ts @@ -0,0 +1,14 @@ +import {db} from "@/db"; +import {eq, lt} from "drizzle-orm"; +import * as drizzleDb from "@/db"; + +export async function enforceRetentionDays(databaseId: string, days: number) { + const cutoff = new Date(Date.now() - days * 86400000); // days → ms + + await db.delete(drizzleDb.schemas.backup).where( + eq(drizzleDb.schemas.backup.databaseId, databaseId) && + lt(drizzleDb.schemas.backup.createdAt, cutoff) + ); + + // Note: files should also be deleted from storage +} \ No newline at end of file diff --git a/src/lib/tasks/database/retention-gsf.ts b/src/lib/tasks/database/retention-gsf.ts new file mode 100644 index 00000000..a668e651 --- /dev/null +++ b/src/lib/tasks/database/retention-gsf.ts @@ -0,0 +1,59 @@ +import {db} from "@/db"; +import {subDays, subWeeks, subMonths, subYears, startOfWeek, startOfMonth, startOfYear} from "date-fns"; +import {eq, desc} from "drizzle-orm"; +import * as drizzleDb from "@/db"; + +export async function enforceRetentionGFS(databaseId: string, gfsSettings: { + daily: number; + weekly: number; + monthly: number; + yearly: number; +}) { + const backups = await db.query.backup.findMany({ + where: eq(drizzleDb.schemas.backup.databaseId, databaseId), + orderBy: desc(drizzleDb.schemas.backup.createdAt), + }); + + const now = new Date(); + const toKeep: Set = new Set(); + + // DAILY + backups.forEach((b) => { + if (b.createdAt >= subDays(now, gfsSettings.daily)) toKeep.add(b.id); + }); + + // WEEKLY + const weekStartDates = Array.from({length: gfsSettings.weekly}, (_, i) => startOfWeek(subWeeks(now, i))); + weekStartDates.forEach((weekStart) => { + const backupOfWeek = backups.find( + (b) => b.createdAt >= weekStart && b.createdAt < subWeeks(weekStart, -1) + ); + if (backupOfWeek) toKeep.add(backupOfWeek.id); + }); + + // MONTHLY + const monthStartDates = Array.from({length: gfsSettings.monthly}, (_, i) => startOfMonth(subMonths(now, i))); + monthStartDates.forEach((monthStart) => { + const backupOfMonth = backups.find( + (b) => b.createdAt >= monthStart && b.createdAt < subMonths(monthStart, -1) + ); + if (backupOfMonth) toKeep.add(backupOfMonth.id); + }); + + // YEARLY + const yearStartDates = Array.from({length: gfsSettings.yearly}, (_, i) => startOfYear(subYears(now, i))); + yearStartDates.forEach((yearStart) => { + const backupOfYear = backups.find( + (b) => b.createdAt >= yearStart && b.createdAt < subYears(yearStart, -1) + ); + if (backupOfYear) toKeep.add(backupOfYear.id); + }); + + // Delete backups not in `toKeep` + for (const b of backups) { + if (!toKeep.has(b.id)) { + await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id)); + // TODO: delete backup file from storage + } + } +} diff --git a/src/lib/tasks/index.ts b/src/lib/tasks/index.ts index d8ee0395..e1a38329 100644 --- a/src/lib/tasks/index.ts +++ b/src/lib/tasks/index.ts @@ -1,5 +1,5 @@ import cron from "node-cron"; -import {retentionCleanTask} from "@/lib/tasks/project"; +import {retentionCleanTask} from "@/lib/tasks/database"; export const retentionJob = cron.schedule("* * * * *", async () => { diff --git a/src/lib/tasks/project.ts b/src/lib/tasks/project.ts deleted file mode 100644 index 22556f44..00000000 --- a/src/lib/tasks/project.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {db} from "@/db"; -import * as drizzleDb from "@/db"; - -export const retentionCleanTask = async () => { - try { - const projects = await db.query.project.findMany(); - console.log(projects); - - } catch (e: any) { - throw e; - } -} \ No newline at end of file