Working on retention policy and cron system.

This commit is contained in:
charlesgauthereau
2025-09-14 16:46:23 +02:00
parent 2d559e456d
commit c3e2978919
13 changed files with 393 additions and 72 deletions
+9 -2
View File
@@ -3,6 +3,8 @@ 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/07_database";
import {eq, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
export const retentionCleanTask = async () => {
@@ -10,13 +12,16 @@ export const retentionCleanTask = async () => {
const databases = await db.query.database.findMany({
with: {
retentionPolicy: true,
backups: true,
backups: {
where: isNull(drizzleDb.schemas.backup.deletedAt),
},
},
});
console.log(databases);
for (const db of databases) {
if (!db.retentionPolicy) continue; // no policy = skip
await enforceRetention(db.id, db.retentionPolicy);
}
} catch (e: any) {
@@ -29,6 +34,8 @@ export async function enforceRetention(
databaseId: string,
policy: typeof retentionPolicy.$inferSelect
) {
console.log(`EnforceRetention: ${policy}`);
switch (policy.type) {
case "count":
await enforceRetentionCount(databaseId, policy.count ?? 7);
+28 -2
View File
@@ -1,17 +1,43 @@
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {desc, eq} from "drizzle-orm";
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
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),
with:{
database: {
with: {
project: true
}
}
}
});
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
// await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
const deletion = await deleteBackupCronAction({
backupId: b.id,
databaseId: b.databaseId,
file: b.file!,
projectSlug: b.database.project?.slug!
});
// @ts-ignore
if (deletion.data.success) {
// @ts-ignore
console.log(deletion.data.actionSuccess.message);
} else {
// @ts-ignore
console.log(deletion.data.actionError.message);
}
}
}
+39 -8
View File
@@ -1,14 +1,45 @@
import {db} from "@/db";
import {eq, lt} from "drizzle-orm";
import {eq, lt, and, desc} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
export async function enforceRetentionDays(databaseId: string, days: number) {
const cutoff = new Date(Date.now() - days * 86400000); // days → ms
const cutoff = new Date(Date.now() - days * 86400000);
await db.delete(drizzleDb.schemas.backup).where(
eq(drizzleDb.schemas.backup.databaseId, databaseId) &&
lt(drizzleDb.schemas.backup.createdAt, cutoff)
);
const expiredBackups = await db.query.backup.findMany({
where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseId),
lt(drizzleDb.schemas.backup.createdAt, cutoff)
),
with: {
database: {
with: {
project: true
}
}
}
});
for (const backup of expiredBackups) {
const deletion = await deleteBackupCronAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: backup.database.project?.slug!
});
// @ts-ignore
if (deletion.data.success) {
// @ts-ignore
console.log(deletion.data.actionSuccess.message);
} else {
// @ts-ignore
console.log(deletion.data.actionError.message);
}
}
}
// Note: files should also be deleted from storage
}
+28 -2
View File
@@ -2,6 +2,7 @@ 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";
import {deleteBackupCronAction} from "@/lib/tasks/database/utils/delete";
export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
daily: number;
@@ -12,6 +13,13 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, databaseId),
orderBy: desc(drizzleDb.schemas.backup.createdAt),
with: {
database: {
with: {
project: true
}
}
}
});
const now = new Date();
@@ -52,8 +60,26 @@ export async function enforceRetentionGFS(databaseId: string, gfsSettings: {
// 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
// await db.delete(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.id, b.id));
const deletion = await deleteBackupCronAction({
backupId: b.id,
databaseId: b.databaseId,
file: b.file!,
projectSlug: b.database.project?.slug!
});
// @ts-ignore
if (deletion.data.success) {
// @ts-ignore
console.log(deletion.data.actionSuccess.message);
} else {
// @ts-ignore
console.log(deletion.data.actionError.message);
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
"use server"
import {action, userAction} from "@/safe-actions";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {Backup} from "@/db/schema/07_database";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {and, eq} from "drizzle-orm";
import {deleteFileS3Private, deleteLocalPrivate} from "@/features/upload/private/upload.action";
import {env} from "@/env.mjs";
export const deleteBackupCronAction = action
.schema(
z.object({
backupId: z.string(),
databaseId: z.string(),
projectSlug: z.string(),
file: z.string(),
})
)
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try {
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
return {
success: false,
actionError: {
message: "No settings found.",
status: 404,
cause: "No settings found.",
messageParams: {message: "Error deleting the backup"},
},
};
}
await db
.update(drizzleDb.schemas.backup)
.set({
deletedAt: new Date(),
})
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
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();
return {
success: true,
actionSuccess: {
message: "Backup deleted successfully.",
},
};
} catch (error) {
console.error(error);
return {
success: false,
actionError: {
message: "Failed to delete backup.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {message: "Error deleting the backup"},
},
};
}
});