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:
2026-07-10 18:02:43 +02:00
parent d09b3e2cdd
commit 413ac0273c
15 changed files with 755 additions and 54 deletions

View File

@@ -0,0 +1,42 @@
import type { Payload } from "payload";
import { PayloadJobQueue } from "../../jobs/payload-job-queue";
import { buildPurgeHandler, type PayloadPurgeApi } from "./retention-purge.job";
/**
* Minimal shape of a Payload job-task definition — enough for
* `payload.config.ts` `jobs.tasks` composition without dragging the full
* generated TaskConfig generics through core-shared.
*/
export type RetentionPurgeTask = {
slug: string;
handler: (args: { req: { payload: Payload } }) => Promise<{
output: Record<string, never>;
}>;
};
/**
* Build the Payload job-task definition for one collection's retention purge
* (audit finding A3): `registerRetentionPurgeJobs` enqueues
* `retention-purge--<slug>` tasks at boot, and this definition is what makes
* Payload able to RUN them. Everything the handler needs comes from the
* running instance on `req.payload` (config, local API, job queue for the
* self-re-enqueue), so the task can be declared at config-composition time
* with no bootstrapping order problems.
*/
export function buildRetentionPurgeTask(
collectionSlug: string,
): RetentionPurgeTask {
return {
slug: `retention-purge--${collectionSlug}`,
handler: async ({ req }) => {
const payload = req.payload;
const run = buildPurgeHandler(collectionSlug, {
queue: new PayloadJobQueue(payload),
config: payload.config,
getPayload: async () => payload as unknown as PayloadPurgeApi,
});
await run();
return { output: {} };
},
};
}