feat(core-shared): grace-purge soft-deleted rows + boot registration
The retention purge job gated its whole body on activeRetention while every collection declares only postDeletion, and no app ever called registerRetentionPurgeJobs — retention was dead end to end (audit findings A2 + A3). The DSR soft delete now stamps a deletedAt tombstone on postDeletion collections (kept distinct from processingRestrictedAt so an Art. 18 restriction never feeds the purge), the job grace-purges tombstoned rows past postDeletion.duration with the declared action, core-cms injects the tombstone field + Payload task definitions, and bindAllProduction enqueues the first purge cycle at boot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import type { Payload } from "payload";
|
||||
import { buildRetentionPurgeTask } from "@/payload/retention-purge/task";
|
||||
|
||||
function makeFakePayload() {
|
||||
const find = vi.fn().mockResolvedValue({ docs: [] });
|
||||
const jobsQueue = vi.fn().mockResolvedValue({ id: "job-1" });
|
||||
const payload = {
|
||||
config: {
|
||||
collections: [
|
||||
{
|
||||
slug: "users",
|
||||
custom: {
|
||||
retention: {
|
||||
purgeSchedule: "daily",
|
||||
postDeletion: {
|
||||
duration: "P30D",
|
||||
trigger: "after-deletion",
|
||||
action: "hard-delete",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
find,
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
jobs: { queue: jobsQueue },
|
||||
} as unknown as Payload;
|
||||
return { payload, find, jobsQueue };
|
||||
}
|
||||
|
||||
describe("buildRetentionPurgeTask (A3)", () => {
|
||||
it("uses the retention-purge--<slug> task slug", () => {
|
||||
expect(buildRetentionPurgeTask("users").slug).toBe(
|
||||
"retention-purge--users",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the purge against req.payload and re-enqueues the next cycle", async () => {
|
||||
const { payload, find, jobsQueue } = makeFakePayload();
|
||||
const task = buildRetentionPurgeTask("users");
|
||||
|
||||
const result = await task.handler({ req: { payload } });
|
||||
|
||||
expect(result).toEqual({ output: {} });
|
||||
// postDeletion branch queried the tombstone field
|
||||
expect(find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
collection: "users",
|
||||
where: { deletedAt: expect.anything() },
|
||||
}),
|
||||
);
|
||||
// self-re-enqueue went through the payload job queue
|
||||
expect(jobsQueue).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ task: "retention-purge--users" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user