feat: purger les contenus expirés ou anciens
This commit is contained in:
@@ -41,6 +41,7 @@ import { fr } from "date-fns/locale";
|
|||||||
|
|
||||||
type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
|
type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre";
|
||||||
type ReadFilter = "unread" | "all" | "read";
|
type ReadFilter = "unread" | "all" | "read";
|
||||||
|
type AapPurgeScope = "all" | "expired" | "older_than";
|
||||||
|
|
||||||
interface AAPItem {
|
interface AAPItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -78,6 +79,7 @@ const CAT_ACCENT: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 24;
|
const PAGE_SIZE = 24;
|
||||||
|
const PURGE_MONTH_OPTIONS = [1, 3, 6, 12, 18, 24] as const;
|
||||||
|
|
||||||
function formatDate(d: Date | null | undefined): string | null {
|
function formatDate(d: Date | null | undefined): string | null {
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
@@ -137,12 +139,16 @@ export default function AAPDashboard() {
|
|||||||
});
|
});
|
||||||
const unreadCountQuery = trpc.aap.unreadCount.useQuery();
|
const unreadCountQuery = trpc.aap.unreadCount.useQuery();
|
||||||
const unreadCount = unreadCountQuery.data?.count ?? 0;
|
const unreadCount = unreadCountQuery.data?.count ?? 0;
|
||||||
|
const [purgeScope, setPurgeScope] = useState<AapPurgeScope>("expired");
|
||||||
|
const [purgeMonths, setPurgeMonths] = useState<(typeof PURGE_MONTH_OPTIONS)[number]>(6);
|
||||||
|
|
||||||
const purgeMutation = trpc.aap.purge.useMutation({
|
const purgeMutation = trpc.aap.purge.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
||||||
utils.aap.list.invalidate();
|
utils.aap.list.invalidate();
|
||||||
utils.aap.filters.invalidate();
|
utils.aap.filters.invalidate();
|
||||||
|
utils.aap.unreadCount.invalidate();
|
||||||
|
utils.aap.getReadIds.invalidate();
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(`Erreur lors de la purge : ${err.message}`);
|
toast.error(`Erreur lors de la purge : ${err.message}`);
|
||||||
@@ -258,11 +264,58 @@ export default function AAPDashboard() {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle size={20} className="text-destructive" />
|
<AlertTriangle size={20} className="text-destructive" />
|
||||||
Purger tous les appels à projets
|
Purger les appels à projets
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription asChild>
|
<AlertDialogDescription asChild>
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<p>Cette action va <strong>supprimer définitivement</strong> tous les appels à projets.</p>
|
<p>Choisissez les AAP à <strong>supprimer définitivement</strong>.</p>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{([
|
||||||
|
["expired", "AAP clôturés"],
|
||||||
|
["older_than", "AAP plus anciens"],
|
||||||
|
["all", "Tous les AAP"],
|
||||||
|
] as const).map(([scope, label]) => (
|
||||||
|
<button
|
||||||
|
key={scope}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPurgeScope(scope)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-2 px-3 py-2 text-left text-sm font-medium transition-colors",
|
||||||
|
purgeScope === scope
|
||||||
|
? "border-primary bg-primary/5 text-primary"
|
||||||
|
: "border-border hover:border-primary/40 text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{purgeScope === "older_than" && (
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{PURGE_MONTH_OPTIONS.map((months) => (
|
||||||
|
<button
|
||||||
|
key={months}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPurgeMonths(months)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-2 px-2 py-2 text-sm font-medium transition-colors",
|
||||||
|
purgeMonths === months
|
||||||
|
? "border-primary bg-primary/5 text-primary"
|
||||||
|
: "border-border hover:border-primary/40 text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{months} mois
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
{purgeScope === "expired"
|
||||||
|
? "Seuls les AAP dont la date de clôture est dépassée seront supprimés."
|
||||||
|
: purgeScope === "all"
|
||||||
|
? "Tous les appels à projets seront supprimés."
|
||||||
|
: `Les AAP publiés, ou importés sans date de publication, il y a plus de ${purgeMonths} mois seront supprimés.`}
|
||||||
|
</p>
|
||||||
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
|
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
@@ -271,10 +324,14 @@ export default function AAPDashboard() {
|
|||||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
onClick={() => purgeMutation.mutate()}
|
onClick={() => purgeMutation.mutate(
|
||||||
|
purgeScope === "older_than"
|
||||||
|
? { scope: "older_than", months: purgeMonths }
|
||||||
|
: { scope: purgeScope },
|
||||||
|
)}
|
||||||
disabled={purgeMutation.isPending}
|
disabled={purgeMutation.isPending}
|
||||||
>
|
>
|
||||||
{purgeMutation.isPending ? "Purge en cours..." : "Oui, purger tous les appels à projets"}
|
{purgeMutation.isPending ? "Purge en cours..." : "Oui, supprimer définitivement"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { fr } from "date-fns/locale";
|
|||||||
|
|
||||||
type TypeVeille = "reglementaire" | "concurrentielle" | "technologique" | "informationnelle";
|
type TypeVeille = "reglementaire" | "concurrentielle" | "technologique" | "informationnelle";
|
||||||
type ReadFilter = "unread" | "all" | "read";
|
type ReadFilter = "unread" | "all" | "read";
|
||||||
|
type VeillePurgeScope = "all" | "older_than";
|
||||||
|
|
||||||
interface VeilleItem {
|
interface VeilleItem {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -97,6 +98,7 @@ const TYPE_HEADER_BG: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZE = 24;
|
const PAGE_SIZE = 24;
|
||||||
|
const PURGE_MONTH_OPTIONS = [1, 3, 6, 12, 18, 24] as const;
|
||||||
|
|
||||||
function formatDate(d: Date | null | undefined): string | null {
|
function formatDate(d: Date | null | undefined): string | null {
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
@@ -254,12 +256,16 @@ export default function VeilleDashboard() {
|
|||||||
});
|
});
|
||||||
const unreadCountQuery = trpc.veille.unreadCount.useQuery();
|
const unreadCountQuery = trpc.veille.unreadCount.useQuery();
|
||||||
const unreadCount = unreadCountQuery.data?.count ?? 0;
|
const unreadCount = unreadCountQuery.data?.count ?? 0;
|
||||||
|
const [purgeScope, setPurgeScope] = useState<VeillePurgeScope>("older_than");
|
||||||
|
const [purgeMonths, setPurgeMonths] = useState<(typeof PURGE_MONTH_OPTIONS)[number]>(6);
|
||||||
|
|
||||||
const purgeMutation = trpc.veille.purge.useMutation({
|
const purgeMutation = trpc.veille.purge.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`);
|
||||||
utils.veille.list.invalidate();
|
utils.veille.list.invalidate();
|
||||||
utils.veille.filters.invalidate();
|
utils.veille.filters.invalidate();
|
||||||
|
utils.veille.unreadCount.invalidate();
|
||||||
|
utils.veille.getReadIds.invalidate();
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error(`Erreur lors de la purge : ${err.message}`);
|
toast.error(`Erreur lors de la purge : ${err.message}`);
|
||||||
@@ -381,11 +387,55 @@ export default function VeilleDashboard() {
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle size={20} className="text-destructive" />
|
<AlertTriangle size={20} className="text-destructive" />
|
||||||
Purger toutes les données de veille
|
Purger la veille stratégique
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription asChild>
|
<AlertDialogDescription asChild>
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<p>Cette action va <strong>supprimer définitivement</strong> toutes les entrées de la veille stratégique.</p>
|
<p>Choisissez les articles à <strong>supprimer définitivement</strong>.</p>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{([
|
||||||
|
["older_than", "Articles plus anciens"],
|
||||||
|
["all", "Tous les articles"],
|
||||||
|
] as const).map(([scope, label]) => (
|
||||||
|
<button
|
||||||
|
key={scope}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPurgeScope(scope)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-2 px-3 py-2 text-left text-sm font-medium transition-colors",
|
||||||
|
purgeScope === scope
|
||||||
|
? "border-primary bg-primary/5 text-primary"
|
||||||
|
: "border-border hover:border-primary/40 text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{purgeScope === "older_than" && (
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{PURGE_MONTH_OPTIONS.map((months) => (
|
||||||
|
<button
|
||||||
|
key={months}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPurgeMonths(months)}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border-2 px-2 py-2 text-sm font-medium transition-colors",
|
||||||
|
purgeMonths === months
|
||||||
|
? "border-primary bg-primary/5 text-primary"
|
||||||
|
: "border-border hover:border-primary/40 text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{months} mois
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
{purgeScope === "all"
|
||||||
|
? "Tous les articles de veille seront supprimés."
|
||||||
|
: `Les articles publiés, ou importés sans date de publication, il y a plus de ${purgeMonths} mois seront supprimés.`}
|
||||||
|
</p>
|
||||||
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
|
<p className="text-destructive font-medium">Cette opération est irréversible.</p>
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
@@ -394,10 +444,14 @@ export default function VeilleDashboard() {
|
|||||||
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
onClick={() => purgeMutation.mutate()}
|
onClick={() => purgeMutation.mutate(
|
||||||
|
purgeScope === "all"
|
||||||
|
? { scope: "all" }
|
||||||
|
: { scope: "older_than", months: purgeMonths },
|
||||||
|
)}
|
||||||
disabled={purgeMutation.isPending}
|
disabled={purgeMutation.isPending}
|
||||||
>
|
>
|
||||||
{purgeMutation.isPending ? "Purge en cours..." : "Oui, purger toutes les données"}
|
{purgeMutation.isPending ? "Purge en cours..." : "Oui, supprimer définitivement"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
49
server/db.ts
49
server/db.ts
@@ -684,3 +684,52 @@ export async function purgeAapItems(): Promise<number> {
|
|||||||
const result = await db.delete(aapItems);
|
const result = await db.delete(aapItems);
|
||||||
return (result as any).affectedRows ?? 0;
|
return (result as any).affectedRows ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calcule une date limite calendaire sans approximer un mois par un nombre de jours. */
|
||||||
|
function cutoffForRetentionMonths(months: number): Date {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setMonth(cutoff.getMonth() - months);
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime les articles de veille dont la date de publication est trop ancienne.
|
||||||
|
* Pour les imports dont la source ne fournit pas cette date, importedAt garantit
|
||||||
|
* une purge prévisible plutôt que de conserver indéfiniment l'article.
|
||||||
|
*/
|
||||||
|
export async function purgeVeilleItemsOlderThan(months: number): Promise<number> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
const cutoff = cutoffForRetentionMonths(months);
|
||||||
|
const where = sql`COALESCE(${veilleItems.datePublication}, ${veilleItems.importedAt}) <= ${cutoff}`;
|
||||||
|
const items = await db.select({ id: veilleItems.id }).from(veilleItems).where(where);
|
||||||
|
await removeArticleReadRecords("veille", items.map((item: { id: number }) => item.id));
|
||||||
|
const result = await db.delete(veilleItems).where(where);
|
||||||
|
return (result as any).affectedRows ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Supprime les AAP clôturés dont la date de clôture est connue. */
|
||||||
|
export async function purgeExpiredAapItems(): Promise<number> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
const where = lte(aapItems.dateCloture, new Date());
|
||||||
|
const items = await db.select({ id: aapItems.id }).from(aapItems).where(where);
|
||||||
|
await removeArticleReadRecords("aap", items.map((item: { id: number }) => item.id));
|
||||||
|
const result = await db.delete(aapItems).where(where);
|
||||||
|
return (result as any).affectedRows ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applique aux AAP la même règle d'ancienneté que pour la veille stratégique. */
|
||||||
|
export async function purgeAapItemsOlderThan(months: number): Promise<number> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
const cutoff = cutoffForRetentionMonths(months);
|
||||||
|
const where = sql`COALESCE(${aapItems.datePublication}, ${aapItems.importedAt}) <= ${cutoff}`;
|
||||||
|
const items = await db.select({ id: aapItems.id }).from(aapItems).where(where);
|
||||||
|
await removeArticleReadRecords("aap", items.map((item: { id: number }) => item.id));
|
||||||
|
const result = await db.delete(aapItems).where(where);
|
||||||
|
return (result as any).affectedRows ?? 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import {
|
|||||||
getAapDistinctValues,
|
getAapDistinctValues,
|
||||||
purgeVeilleItems,
|
purgeVeilleItems,
|
||||||
purgeAapItems,
|
purgeAapItems,
|
||||||
|
purgeVeilleItemsOlderThan,
|
||||||
|
purgeExpiredAapItems,
|
||||||
|
purgeAapItemsOlderThan,
|
||||||
getAllSettings,
|
getAllSettings,
|
||||||
setSettings,
|
setSettings,
|
||||||
getImportLogs,
|
getImportLogs,
|
||||||
@@ -56,6 +59,28 @@ const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
|||||||
return next({ ctx });
|
return next({ ctx });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const retentionMonths = z.union([
|
||||||
|
z.literal(1), z.literal(3), z.literal(6), z.literal(12), z.literal(18), z.literal(24),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const veillePurgeInput = z.object({
|
||||||
|
scope: z.enum(["all", "older_than"]),
|
||||||
|
months: retentionMonths.optional(),
|
||||||
|
}).superRefine((value, ctx) => {
|
||||||
|
if (value.scope === "older_than" && value.months === undefined) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "La durée de conservation est requise." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const aapPurgeInput = z.object({
|
||||||
|
scope: z.enum(["all", "expired", "older_than"]),
|
||||||
|
months: retentionMonths.optional(),
|
||||||
|
}).superRefine((value, ctx) => {
|
||||||
|
if (value.scope === "older_than" && value.months === undefined) {
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "La durée de conservation est requise." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Router principal ─────────────────────────────────────────────────────────
|
// ─── Router principal ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
@@ -127,10 +152,14 @@ export const appRouter = router({
|
|||||||
filters: publicProcedure.query(async () => {
|
filters: publicProcedure.query(async () => {
|
||||||
return getVeilleDistinctValues();
|
return getVeilleDistinctValues();
|
||||||
}),
|
}),
|
||||||
purge: adminProcedure.mutation(async () => {
|
purge: adminProcedure
|
||||||
const count = await purgeVeilleItems();
|
.input(veillePurgeInput)
|
||||||
return { success: true, deleted: count };
|
.mutation(async ({ input }) => {
|
||||||
}),
|
const count = input.scope === "all"
|
||||||
|
? await purgeVeilleItems()
|
||||||
|
: await purgeVeilleItemsOlderThan(input.months!);
|
||||||
|
return { success: true, deleted: count, scope: input.scope, months: input.months ?? null };
|
||||||
|
}),
|
||||||
|
|
||||||
reclassifyWithAI: adminProcedure
|
reclassifyWithAI: adminProcedure
|
||||||
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
||||||
@@ -238,10 +267,16 @@ export const appRouter = router({
|
|||||||
filters: publicProcedure.query(async () => {
|
filters: publicProcedure.query(async () => {
|
||||||
return getAapDistinctValues();
|
return getAapDistinctValues();
|
||||||
}),
|
}),
|
||||||
purge: adminProcedure.mutation(async () => {
|
purge: adminProcedure
|
||||||
const count = await purgeAapItems();
|
.input(aapPurgeInput)
|
||||||
return { success: true, deleted: count };
|
.mutation(async ({ input }) => {
|
||||||
}),
|
const count = input.scope === "all"
|
||||||
|
? await purgeAapItems()
|
||||||
|
: input.scope === "expired"
|
||||||
|
? await purgeExpiredAapItems()
|
||||||
|
: await purgeAapItemsOlderThan(input.months!);
|
||||||
|
return { success: true, deleted: count, scope: input.scope, months: input.months ?? null };
|
||||||
|
}),
|
||||||
|
|
||||||
reclassifyWithAI: adminProcedure
|
reclassifyWithAI: adminProcedure
|
||||||
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
.input(z.object({ limit: z.number().int().positive().max(200).default(50) }))
|
||||||
|
|||||||
@@ -10,11 +10,23 @@ vi.mock("./db", async () => {
|
|||||||
getVeilleDistinctValues: vi.fn(async () => ({ categories: [], niveaux: [], territoires: [] })),
|
getVeilleDistinctValues: vi.fn(async () => ({ categories: [], niveaux: [], territoires: [] })),
|
||||||
getAapItems: vi.fn(async () => ({ items: [], total: 0 })),
|
getAapItems: vi.fn(async () => ({ items: [], total: 0 })),
|
||||||
getAapDistinctValues: vi.fn(async () => ({ regions: [], departements: [] })),
|
getAapDistinctValues: vi.fn(async () => ({ regions: [], departements: [] })),
|
||||||
|
purgeVeilleItems: vi.fn(async () => 12),
|
||||||
|
purgeVeilleItemsOlderThan: vi.fn(async () => 4),
|
||||||
|
purgeAapItems: vi.fn(async () => 9),
|
||||||
|
purgeExpiredAapItems: vi.fn(async () => 3),
|
||||||
|
purgeAapItemsOlderThan: vi.fn(async () => 2),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
import { appRouter } from "./routers";
|
import { appRouter } from "./routers";
|
||||||
import type { TrpcContext } from "./_core/context";
|
import type { TrpcContext } from "./_core/context";
|
||||||
|
import {
|
||||||
|
purgeAapItems,
|
||||||
|
purgeAapItemsOlderThan,
|
||||||
|
purgeExpiredAapItems,
|
||||||
|
purgeVeilleItems,
|
||||||
|
purgeVeilleItemsOlderThan,
|
||||||
|
} from "./db";
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -128,6 +140,36 @@ describe("protection admin", () => {
|
|||||||
const caller = appRouter.createCaller(ctx);
|
const caller = appRouter.createCaller(ctx);
|
||||||
await expect(caller.rss.classificationErrors({ page: 1, pageSize: 25 })).rejects.toThrow();
|
await expect(caller.rss.classificationErrors({ page: 1, pageSize: 25 })).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refuse les purges ciblées pour un non admin", async () => {
|
||||||
|
const caller = appRouter.createCaller(makeUserCtx());
|
||||||
|
await expect(caller.veille.purge({ scope: "older_than", months: 6 })).rejects.toThrow();
|
||||||
|
await expect(caller.aap.purge({ scope: "expired" })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("purges administrateur", () => {
|
||||||
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
|
it("applique la durée choisie à la veille", async () => {
|
||||||
|
const result = await appRouter.createCaller(makeAdminCtx()).veille.purge({ scope: "older_than", months: 6 });
|
||||||
|
expect(purgeVeilleItemsOlderThan).toHaveBeenCalledWith(6);
|
||||||
|
expect(purgeVeilleItems).not.toHaveBeenCalled();
|
||||||
|
expect(result).toMatchObject({ success: true, deleted: 4, scope: "older_than", months: 6 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supprime uniquement les AAP clôturés lorsque demandé", async () => {
|
||||||
|
const result = await appRouter.createCaller(makeAdminCtx()).aap.purge({ scope: "expired" });
|
||||||
|
expect(purgeExpiredAapItems).toHaveBeenCalledOnce();
|
||||||
|
expect(purgeAapItems).not.toHaveBeenCalled();
|
||||||
|
expect(purgeAapItemsOlderThan).not.toHaveBeenCalled();
|
||||||
|
expect(result).toMatchObject({ success: true, deleted: 3, scope: "expired", months: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuse une purge par ancienneté sans durée", async () => {
|
||||||
|
const caller = appRouter.createCaller(makeAdminCtx());
|
||||||
|
await expect(caller.aap.purge({ scope: "older_than" } as never)).rejects.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Tests accès public ───────────────────────────────────────────────────────
|
// ─── Tests accès public ───────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user