mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
Sixty-two handler functions across twenty-four modules now resolve to a
JobOutcome instead of undefined. The counts they already wrote to their
wide event ride out in `did`, so a run that deleted nothing and a run
that could not run are no longer the same shape.
Three rules decided each conversion:
- a catch that swallowed becomes jobFailed, keeping its warning line;
- a catch that already rethrew keeps its throw, because the throw
already fails the job and rewrapping it buries the stack;
- a fan-out pass is judged on the pass. One account's revoked grant
belongs on that account's integration ledger, not in a queue-wide
failure that retries the whole cohort, so per-leg failures ride out
as a users_failed count — visible for the first time.
The retention cleanups are the behaviour change worth naming: every one
of them used to warn and return, so a purge that had not run for weeks
looked like a purge with nothing to do. They fail now. Their registrar
sets retryLimit accordingly in the commit that converts the bindings.
JobFailure carries the thrown error's stack; without it every one of the
101 queues would report the same runJob frame.
66 lines
2.6 KiB
TypeScript
66 lines
2.6 KiB
TypeScript
/**
|
|
* Daily physical purge for soft-deleted document-vault rows.
|
|
*
|
|
* Delete in the vault is undo-able: the DELETE route tombstones
|
|
* (`deletedAt`), the UI's undo toast and the restore endpoint clear the
|
|
* tombstone, and THIS job hard-deletes tombstones older than the 30-day
|
|
* grace window — returning the TOAST space the encrypted blob holds (a
|
|
* tombstoned document keeps counting against the quota until purged, so
|
|
* "deleted" bytes are never invisible backup weight). Staged facts cascade
|
|
* via the FK. Restore-after-purge answers 409 at the API.
|
|
*/
|
|
import { type Job } from "pg-boss";
|
|
import { withBackgroundEvent } from "@/lib/logging/background";
|
|
import { jobDone, jobFailed, type JobOutcome } from "@/lib/jobs/job-outcome";
|
|
import { getWorkerPrisma } from "@/lib/jobs/reminder/shared";
|
|
import type { PrismaClient } from "@/generated/prisma/client";
|
|
|
|
export const DOCUMENT_PURGE_QUEUE = "document-tombstone-purge";
|
|
/** Daily at 04:10 Europe/Berlin — after the med-notes backfill tick (04:05). */
|
|
export const DOCUMENT_PURGE_CRON = "10 4 * * *";
|
|
|
|
/** Days a tombstoned document stays restorable before the purge claims it. */
|
|
export const DOCUMENT_PURGE_GRACE_DAYS = 30;
|
|
|
|
const DAY_MS = 86_400_000;
|
|
|
|
export interface DocumentPurgePayload {
|
|
triggeredAt?: string;
|
|
}
|
|
|
|
/**
|
|
* Hard-delete tombstoned `inbound_documents` rows whose `deletedAt` is older
|
|
* than the grace horizon. Returns the number of rows purged. Rows inside the
|
|
* grace window and live rows are never touched.
|
|
*/
|
|
export async function purgeExpiredDocumentTombstones(
|
|
prisma: PrismaClient,
|
|
now: Date = new Date(),
|
|
): Promise<number> {
|
|
const cutoff = new Date(now.getTime() - DOCUMENT_PURGE_GRACE_DAYS * DAY_MS);
|
|
const { count } = await prisma.inboundDocument.deleteMany({
|
|
where: { deletedAt: { not: null, lt: cutoff } },
|
|
});
|
|
return count;
|
|
}
|
|
|
|
export async function handleDocumentPurge(
|
|
jobs: Job<DocumentPurgePayload>[],
|
|
): Promise<JobOutcome> {
|
|
void jobs;
|
|
return withBackgroundEvent("job.document_tombstone_purge", async (evt) => {
|
|
const prisma = getWorkerPrisma();
|
|
try {
|
|
const purged = await purgeExpiredDocumentTombstones(prisma);
|
|
evt.setAction({ name: "documents.vault.purge" });
|
|
evt.addMeta("document_purge_deleted", purged);
|
|
// A night with no expired tombstone purges nothing and is still a run
|
|
// that did its work — the zero is the fact, not an absence of one.
|
|
return jobDone({ document_purge_deleted: purged });
|
|
} catch (err) {
|
|
evt.addWarning(`document-tombstone-purge failed: ${err}`);
|
|
return jobFailed("document tombstone purge failed", err);
|
|
}
|
|
});
|
|
}
|