feat: purge import history by retention
This commit is contained in:
21
server/db.ts
21
server/db.ts
@@ -1,4 +1,4 @@
|
||||
import { eq, and, desc, sql, inArray, getTableColumns, count } from "drizzle-orm";
|
||||
import { eq, and, desc, sql, inArray, getTableColumns, count, lt } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/mysql2";
|
||||
import {
|
||||
InsertUser,
|
||||
@@ -628,6 +628,25 @@ export async function deleteAllImportLogs(userId: number | null): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
/** Compte les seuls journaux d'import qui seront concernés par une purge de rétention. */
|
||||
export async function countImportLogsBefore(cutoff: Date): Promise<number> {
|
||||
const db = await getDb();
|
||||
if (!db) return 0;
|
||||
const [result] = await db.select({ total: count() }).from(importLogs).where(lt(importLogs.importedAt, cutoff));
|
||||
return Number(result?.total ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime uniquement les journaux d'import antérieurs à la borne donnée.
|
||||
* Les factures, fichiers source et PDF ne sont jamais modifiés.
|
||||
*/
|
||||
export async function deleteImportLogsBefore(cutoff: Date): Promise<number> {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
const result = await db.delete(importLogs).where(lt(importLogs.importedAt, cutoff));
|
||||
return Number(result[0]?.affectedRows ?? 0);
|
||||
}
|
||||
|
||||
// ============= LLM LOG OPERATIONS =============
|
||||
|
||||
export async function createLlmLog(data: InsertLlmLog): Promise<LlmLog> {
|
||||
|
||||
24
server/importLogRetention.test.ts
Normal file
24
server/importLogRetention.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
IMPORT_LOG_RETENTION_OPTIONS,
|
||||
getImportLogRetentionCutoff,
|
||||
isImportLogRetentionMonths,
|
||||
} from "@shared/importLogRetention";
|
||||
|
||||
describe("rétention des journaux d'import", () => {
|
||||
it("propose uniquement les périodes de purge autorisées", () => {
|
||||
expect(IMPORT_LOG_RETENTION_OPTIONS).toEqual([3, 6, 12]);
|
||||
expect(isImportLogRetentionMonths(3)).toBe(true);
|
||||
expect(isImportLogRetentionMonths(6)).toBe(true);
|
||||
expect(isImportLogRetentionMonths(12)).toBe(true);
|
||||
expect(isImportLogRetentionMonths(9)).toBe(false);
|
||||
});
|
||||
|
||||
it("calcule une date limite calendaire sans modifier la date de référence", () => {
|
||||
const now = new Date("2026-10-09T12:00:00.000Z");
|
||||
const cutoff = getImportLogRetentionCutoff(6, now);
|
||||
|
||||
expect(cutoff.toISOString()).toBe("2026-04-09T12:00:00.000Z");
|
||||
expect(now.toISOString()).toBe("2026-10-09T12:00:00.000Z");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapElig
|
||||
import { requiresBapBeforeOriginalPdfExport } from "@shared/bapExportGuard";
|
||||
import { calculateInvoiceQualityScore, withCalculatedInvoiceQuality } from "@shared/invoiceQuality";
|
||||
import { normalizeImportLogPagination } from "@shared/importLogPagination";
|
||||
import { getImportLogRetentionCutoff, isImportLogRetentionMonths } from "@shared/importLogRetention";
|
||||
import { webPortalUrlSchema } from "./webPortalUrl";
|
||||
|
||||
interface Condition {
|
||||
@@ -50,6 +51,8 @@ import {
|
||||
getImportLogsByUser,
|
||||
getImportLogsWithDetailsByUser,
|
||||
deleteAllImportLogs,
|
||||
countImportLogsBefore,
|
||||
deleteImportLogsBefore,
|
||||
getLlmLogsBySourceFile,
|
||||
getLlmLogsByInvoice,
|
||||
getImportSettingsByUser,
|
||||
@@ -1748,6 +1751,34 @@ export const appRouter = router({
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
retentionPreview: protectedProcedure
|
||||
.input(z.object({ months: z.number().int() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "admin") {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Réservé aux administrateurs." });
|
||||
}
|
||||
if (!isImportLogRetentionMonths(input.months)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Durée de rétention invalide." });
|
||||
}
|
||||
const cutoff = getImportLogRetentionCutoff(input.months);
|
||||
const count = await countImportLogsBefore(cutoff);
|
||||
return { months: input.months, cutoff, count };
|
||||
}),
|
||||
|
||||
purgeBeforeRetention: protectedProcedure
|
||||
.input(z.object({ months: z.number().int() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "admin") {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Réservé aux administrateurs." });
|
||||
}
|
||||
if (!isImportLogRetentionMonths(input.months)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Durée de rétention invalide." });
|
||||
}
|
||||
const cutoff = getImportLogRetentionCutoff(input.months);
|
||||
const deletedCount = await deleteImportLogsBefore(cutoff);
|
||||
return { months: input.months, cutoff, deletedCount };
|
||||
}),
|
||||
}),
|
||||
|
||||
// ============= LLM LOGS ROUTES =============
|
||||
|
||||
Reference in New Issue
Block a user