feat(vaccinations): write the backup, wipe and rotation verdicts

- both models are backed up and claimed two-ended: an immunization
  history has no other copy once the paper Pass has been transcribed
- the document link is wiped ahead of the dose, and the dose ahead of
  the practitioner it points at with SetNull, so the audit row's
  per-model counts stay truthful
- the dose note joins ENCRYPTED_COLUMNS and the rotation walk; the batch
  code and the other structured columns stay queryable plaintext
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 13:39:06 +02:00
1 parent f28658c611
commit 1bd97de943
4 files changed
+44

No files matched your search

+11
View File
@@ -562,6 +562,17 @@ async function main() {
),
);
// ───── v1.38.0 dose free text (Bytes column) ─────
// The note on an administered dose — a reaction, a sore arm. Always
// encrypted on write, so every non-null row rotates.
results.push(
await rotateBytesColumn(
"VaccinationRecord",
"noteEncrypted",
prisma.vaccinationRecord,
),
);
// ───── Inbound clinical document (Bytes column, codec-dispatched) ─────
// The raw uploaded document. Two layouts recorded per row in
// `contentCodec` ("base64v1" string codec | "binary2" binary codec), so
+7
View File
@@ -242,6 +242,13 @@ export const ENCRYPTED_COLUMNS: readonly EncryptedColumn[] = [
{ model: "Encounter", field: "outcomeEncrypted", kind: "bytes" },
{ model: "Practitioner", field: "noteEncrypted", kind: "bytes" },
// ───── v1.38.0 dose free text (Bytes column) ─────
// What the person wrote about the dose — a reaction, a sore arm, why the
// schedule went the way it did. The Pass's own structured fields stay
// queryable plaintext (the batch code is a batch code, not a secret), so
// this is the one column on the record that rotates.
{ model: "VaccinationRecord", field: "noteEncrypted", kind: "bytes" },
// ───── Inbound clinical document (Bytes column, codec-dispatched) ─────
// The raw uploaded document (the most sensitive blob in the corpus; never
// logged, never in wide-event meta). Two layouts, recorded per row in
+9
View File
@@ -91,6 +91,15 @@ export const WIPE_MODELS = [
"EncounterConditionLink",
"Encounter",
// ── Vaccinations ────────────────────────────────────────────────────────
// Same ordering argument as the visit links: the document link cascades
// from the dose AND from the stored document, so it is deleted before
// either or its count reads zero on the audit row. The dose points at a
// practitioner and an encounter with SetNull, which is why it goes here,
// ahead of both.
"VaccinationDocumentLink",
"VaccinationRecord",
// ── Clinical record ─────────────────────────────────────────────────────
"MentalHealthAssessment",
"LabResult",
+17
View File
@@ -142,6 +142,14 @@ export const BACKED_UP_MODELS = [
"EncounterLabLink",
"EncounterConditionLink",
// ── Vaccinations ──────────────────────────────────────────────────────────
// An immunization history is the one part of a health record that has no
// other copy: the paper Pass is the original, and once it has been
// transcribed the transcription is what the person relies on. Nothing
// re-syncs it from a provider.
"VaccinationRecord",
"VaccinationDocumentLink",
// ── Coach ─────────────────────────────────────────────────────────────────
"CoachConversation",
"CoachMessage",
@@ -262,6 +270,15 @@ export const TWO_ENDED_MODELS = [
"EncounterDocumentLink",
"EncounterLabLink",
"EncounterConditionLink",
// Doses travel both ways from the release that introduces them, and the
// link with them. `DocumentConditionLink` one list down says what the
// alternative costs — documents and conditions both restore, the filing
// between them does not. Repeating that here would return a restored
// Impfpass scan and a restored dose with nothing between them, which is the
// same regret against a record a person cannot reconstruct from anywhere
// else.
"VaccinationRecord",
"VaccinationDocumentLink",
] as const;
/** One model claimed to travel both ways. */