Files
HealthLog/tests/integration/backup-fixture.ts
T
Marc-André Bombeck 64fa65b460 test(backup): pin the file and the restored account against golden copies
Seed one account as far as the schema allows (a row of every two-ended
model, every column of those rows and of the account row, the instance
settings, a sharing grant), export it both ways and restore each file
into a fresh account, over the live account, and from a version-1 file
in the old shapes. The files, counts, parsed payload, preview, restore
outcome, reported phases, audit rows, wide-event fields and every row the
account holds afterwards are compared with golden copies after ids,
ciphertext and run-time timestamps are normalised. Refusals, skip reports
and the schema's issue list for malformed files are pinned the same way.

The seed moves out of the round-trip test into a shared fixture module
so both files fill the same account.
2026-10-22 06:22:30 +02:00

1574 lines
52 KiB
TypeScript

/**
* The account the backup tests fill: one row of every two-ended model, the
* shapes a CHECK constraint ties to a particular row, and the values the
* column fill uses where a type alone would produce one the restore refuses.
*
* Shared by `backup-round-trip.test.ts`, which proves each model and column
* comes back, and `backup-parity.test.ts`, which pins the whole file and the
* restored account against a golden copy.
*/
import { createHash } from "node:crypto";
import type { PrismaClient } from "@/generated/prisma/client";
import { encrypt, encryptBytes } from "@/lib/crypto";
import { encryptToBytes } from "@/lib/ai/coach/bytes-codec";
import { sealDayCountry } from "@/lib/dawarich/connection";
import { sealLocation } from "@/lib/environment/location-cipher";
import { encryptFactData, encryptFactProvenance } from "@/lib/documents/store";
import { encryptWaveformToBytes } from "@/lib/withings/ecg-waveform-codec";
import { encryptContextToBytes } from "@/lib/labs/biomarker-store";
import type { AuditColumn, AuditModel } from "./backup-column-audit";
import { cookieJar } from "./mock-next-headers";
export const OWNER_ID = "round-trip-owner";
export const AT = (iso: string) => new Date(iso);
// The restore drops a measurement tombstone older than
// `TOMBSTONE_RETENTION_DAYS` on purpose, so the fixture's deletion has to stay
// recent on whatever day the suite runs. A fixed date aged out on 2026-10-03.
export const TOMBSTONE_DELETED_AT = new Date(
Math.floor((Date.now() - 10 * 86_400_000) / 1000) * 1000,
).toISOString();
export const PHQ9_RESPONSES = JSON.stringify({
items: [2, 1, 2, 2, 1, 1, 2, 1, 2],
schema: 1,
});
export const CONSENT_ARTEFACT = "JVBERi0xLjQKJWZpeHR1cmU=";
export const INVENTORY_NOTE = "second pack, kept in the kitchen drawer";
export const COACH_FACT = "does not tolerate ACE inhibitors";
// v1.41 — the model text of a turn's trail, stored encrypted beside the turn.
export const COACH_TRAIL_JSON = JSON.stringify({
entries: [{ id: "a1", title: "Checking the trend", text: "Read 30 days." }],
});
export const COACH_PLAN_CUE = "if the evening reading is over 140";
export const COACH_PLAN_ACTION =
"then walk twenty minutes before dinner tomorrow";
export const COACH_REMINDER_NOTE = "ask how the evening walks are going";
export const COACH_SUMMARY = "earlier turns: weight trend and evening walks";
export const METRIC_LEGACY_NOTE = "left hand, after the long ride";
export const METRIC_SEALED_NOTE = "right hand, rested";
export const COACH_USER_TURN =
"my readings look higher this week, is that real?";
export const COACH_ASSISTANT_TURN =
"the last seven mornings average 4 mmHg above the fortnight before";
/** The table of values the assistant turn read: ciphertext at rest. */
export const COACH_RESULTS_JSON = JSON.stringify([
{
ref: "r1",
source: {
tool: "get_metric_series",
domain: "bp",
window: "last7days",
period: "current",
granularity: "day",
},
shape: "timeSeries",
titleKey: "coach.result.title.metricByPeriod",
title: "Blood pressure by day",
rowCount: 2,
chartKind: "line",
displayed: true,
columns: [
{
key: "period",
kind: "period",
labelKey: "coach.result.column.day",
label: "Day",
},
{
key: "sys",
kind: "number",
labelKey: "coach.result.column.systolic",
label: "Systolic",
unit: "mmHg",
},
],
rows: [
["2026-07-19", 131],
["2026-07-20", null],
],
truncated: false,
chart: { kind: "line", x: "period", series: ["sys"] },
},
]);
export const ROUND_TRIP_CATEGORY_KEY =
"custom:5f0c6a52-6a43-4f63-9b39-0c1f4d7e2a10";
export const DOSE_CHANGE_NOTE = "titration note, encrypted at rest";
export const EXTRACTED_FACT_SPAN = "Ferritin 91 ng/mL (30 - 400)";
export const SIDE_EFFECT_NOTE = "nausea for two hours after the evening dose";
/** A short micro-volt strip. Long enough that a defaulted empty array shows. */
export const ECG_SAMPLES = [-12, 4, 118, -33, 7, 2, -5, 61];
/**
* One row of every two-ended model, for one account.
*
* Minimal on purpose: this file asks whether a row survives the round trip, and
* a fat fixture only makes the answer slower to get. The mood factor and the
* cycle symptom are the account's OWN custom rows rather than seeded catalogue
* entries, because a catalogue row survives the account being deleted and would
* make the link tables look restored when nothing restored them.
*/
export async function seedEveryTwoEndedModel(
prisma: PrismaClient,
): Promise<void> {
// Held rather than discarded: the personal best below points at this row
// through a column that is a real foreign key in the database, so the
// fixture needs the id to build the reference the restore has to resolve.
const weightMeasurement = await prisma.measurement.create({
data: {
userId: OWNER_ID,
type: "WEIGHT",
value: 74.2,
unit: "kg",
measuredAt: AT("2026-07-01T07:00:00.000Z"),
source: "MANUAL",
},
});
// A DELETED reading, kept as a tombstone. It is here because the obvious way
// to make the weekly backup cheaper — stop exporting soft-deleted rows, which
// on a real record is four rows in five — is wrong, and nothing in the tree
// said so out loud. A tombstone inside the retention window is what tells a
// synced client the row is gone (`/api/sync/changes` reads exactly these) and
// what makes a re-sent Apple Health sample a no-op instead of a resurrection.
// Drop them from a disaster-recovery file and a restore quietly brings back
// every measurement the account ever deleted on the next device sync.
await prisma.measurement.create({
data: {
userId: OWNER_ID,
type: "PULSE",
value: 61,
unit: "bpm",
measuredAt: AT("2026-07-02T06:30:00.000Z"),
source: "APPLE_HEALTH",
externalId: "round-trip-deleted-sample",
syncVersion: 4,
deletedAt: AT(TOMBSTONE_DELETED_AT),
},
});
// The EVENT row an ECG strip is filed against, and the strip itself. The
// pairing is the point: `EcgRecording.measurementId` is a real foreign key,
// so this is the reference whose remap has to survive the round trip rather
// than fail the transaction.
const rhythmEvent = await prisma.measurement.create({
data: {
userId: OWNER_ID,
type: "IRREGULAR_RHYTHM_NOTIFICATION",
value: 1,
unit: "event",
measuredAt: AT("2026-06-15T14:22:00.000Z"),
source: "WITHINGS",
rhythmClassification: "IRREGULAR",
},
});
await prisma.ecgRecording.create({
data: {
userId: OWNER_ID,
source: "WITHINGS",
externalRecordingId: "round-trip-signal-1",
recordedAt: AT("2026-06-15T14:22:00.000Z"),
waveformEncrypted: encryptWaveformToBytes(ECG_SAMPLES),
samplingFrequency: 300,
sampleCount: ECG_SAMPLES.length,
durationSeconds: ECG_SAMPLES.length / 300,
lead: "I",
averageHeartRate: 88,
rhythmClassification: "IRREGULAR",
measurementId: rhythmEvent.id,
},
});
await prisma.intradayCumulativeProfile.create({
data: {
userId: OWNER_ID,
type: "ACTIVITY_STEPS",
dateKey: "2026-07-01",
// One slot per hour — the restore refuses a profile of any other length,
// because a short row lands in the table and is dropped on read.
hourlyCumulative: Array.from({ length: 24 }, (_, hour) => hour * 350),
dayTotal: 8050,
sampleCount: 24,
timezone: "Europe/Berlin",
},
});
// Two courses with a gap, and the window the row projects from them (the
// latest). The earlier course exists nowhere else once it has ended.
const medication = await prisma.medication.create({
data: {
userId: OWNER_ID,
name: "Round-trip tablet",
dose: "5 mg",
startsOn: AT("2026-06-10T00:00:00.000Z"),
endsOn: AT("2026-06-16T00:00:00.000Z"),
schedules: {
create: { windowStart: "08:00", windowEnd: "09:00", label: "Morning" },
},
courses: {
create: [
{
userId: OWNER_ID,
startsOn: AT("2026-03-01T00:00:00.000Z"),
endsOn: AT("2026-03-07T00:00:00.000Z"),
noteEncrypted: encryptToBytes("the March flu"),
},
{
userId: OWNER_ID,
startsOn: AT("2026-06-10T00:00:00.000Z"),
endsOn: AT("2026-06-16T00:00:00.000Z"),
},
],
},
},
});
await prisma.medicationIntakeEvent.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
scheduledFor: AT("2026-07-01T08:00:00.000Z"),
takenAt: AT("2026-07-01T08:04:00.000Z"),
},
});
// Not OTHER: a missing row already reads as OTHER, so only a chosen value
// proves the category came back. A category the person named, so the
// assignment can only resolve if the label came back before it.
await prisma.medicationCategoryLabel.create({
data: {
userId: OWNER_ID,
key: ROUND_TRIP_CATEGORY_KEY,
labelEncrypted: encryptToBytes("Heart and circulation"),
sortOrder: 2,
},
});
await prisma.medicationCategoryAssignment.create({
data: { medicationId: medication.id, category: ROUND_TRIP_CATEGORY_KEY },
});
// The one row in this fixture that is also checked field by field after the
// restore — see the assertion at the end of the test for why.
await prisma.medicationSideEffect.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
occurredAt: AT("2026-07-01T21:00:00.000Z"),
category: "GI",
entry: "NAUSEA",
severity: 3,
notesEncrypted: encryptToBytes(SIDE_EFFECT_NOTE),
},
});
// Two eras, and the open one carries the weight. `resumedAt: null` means the
// drug is paused right now; a restore that closed it at restore time would
// still count one row back here and read as recovered. The field-level
// assertion at the end of the test is what holds the null.
// A two-step ramp. The ORDER is the content here: same drug, same unit,
// 5 mg then 10 mg. A restore that returned them reversed would still count
// two rows back and would describe the opposite clinical story.
// One open pack and one used up, plus the two ledger rows behind the open
// one. The count on the pack is deliberately NOT the sum of its events: 30
// in, 8 consumed, and the remaining count says 21 because the person threw
// one away without recording it. A restore that recomputed the count from
// the ledger would "fix" that to 22 and disagree with every low-stock
// reminder the account has already received.
// A PHQ-9 administration whose item 9 was answered above zero. The flag and
// the encrypted answers are seeded to DISAGREE with a naive recomputation:
// the flag says true, and a restore that decided to recompute it would have
// to decrypt the blob to do so, which is the handling this data is kept away
// from. The assertion below reads the flag and the ciphertext separately.
await prisma.mentalHealthAssessment.create({
data: {
userId: OWNER_ID,
instrument: "PHQ9",
locale: "de",
version: "standard",
responsesEncrypted: encryptToBytes(PHQ9_RESPONSES),
totalScore: 14,
severityBand: "moderate",
item9Flagged: true,
crisisShownAt: AT("2026-07-05T18:02:00.000Z"),
takenAt: AT("2026-07-05T18:00:00.000Z"),
tz: "Europe/Berlin",
source: "WEB",
},
});
// One active consent and one revoked. The revocation is the part a restore
// can quietly lose: dropping `revokedAt` turns a withdrawn agreement back
// into a standing one, and the latest-per-user lookup shortcuts on exactly
// that column.
await prisma.consentReceipt.createMany({
data: [
{
userId: OWNER_ID,
kind: "ai-processing",
artefact: CONSENT_ARTEFACT,
signedAt: AT("2026-06-01T09:00:00.000Z"),
},
{
userId: OWNER_ID,
kind: "research-sharing",
artefact: "eyJhbGciOiJub25lIn0.e30.",
signedAt: AT("2026-06-02T09:00:00.000Z"),
revokedAt: AT("2026-07-10T11:00:00.000Z"),
},
],
});
// Tuned away from every default, and one threshold in PERCENT rather than
// MINUTES: a restore that fell back to the schema defaults would return four
// plausible numbers and silently change when this drug turns orange.
await prisma.reminderPhaseConfig.create({
data: {
medicationId: medication.id,
greenValue: 45,
greenMode: "MINUTES",
yellowValue: 20,
yellowMode: "PERCENT",
orangeValue: 5,
orangeMode: "MINUTES",
redValue: 180,
redMode: "MINUTES",
},
});
const openPack = await prisma.medicationInventoryItem.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
state: "ACTIVE",
containerType: "BLISTER",
unitsTotal: "30",
unitsRemaining: "21",
firstUseAt: AT("2026-07-01T08:00:00.000Z"),
expiresAt: AT("2027-01-31T00:00:00.000Z"),
manufacturer: "Fixture Pharma",
doseStrength: "10 mg",
notesEncrypted: encryptToBytes(INVENTORY_NOTE),
},
});
await prisma.medicationInventoryItem.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
state: "USED_UP",
containerType: "BOTTLE",
unitsTotal: "60",
unitsRemaining: "0",
},
});
await prisma.medicationInventoryEvent.createMany({
data: [
{
medicationId: medication.id,
delta: 30,
reason: "purchased",
occurredAt: AT("2026-07-01T07:00:00.000Z"),
},
{
medicationId: medication.id,
delta: -8,
reason: "consumed",
occurredAt: AT("2026-07-09T07:00:00.000Z"),
},
],
});
await prisma.medicationDoseChange.createMany({
data: [
{
medicationId: medication.id,
effectiveFrom: AT("2026-04-01T08:00:00.000Z"),
doseValue: 5,
doseUnit: "mg",
noteEncrypted: encryptToBytes(DOSE_CHANGE_NOTE),
},
{
medicationId: medication.id,
effectiveFrom: AT("2026-06-01T08:00:00.000Z"),
doseValue: 10,
doseUnit: "mg",
noteEncrypted: null,
},
],
});
// Three archived eras, and the CHAIN through them is the content. The
// middle one was corrected, so it points at the third; the first and the
// third stand on their own. A restore that returned three rows with every
// link null satisfies any count and has destroyed the only thing the
// pointer says — every era consumer skips a superseded row, so the
// corrected middle era would come back live beside the correction that
// replaced it and the timeline would show the same window twice.
await prisma.medicationScheduleRevision.create({
data: {
medicationId: medication.id,
validFrom: AT("2026-01-01T00:00:00.000Z"),
validUntil: AT("2026-03-01T00:00:00.000Z"),
payload: [{ windowStart: "07:00", windowEnd: "09:00", dose: "5 mg" }],
source: "ARCHIVED",
},
});
const correctionEra = await prisma.medicationScheduleRevision.create({
data: {
medicationId: medication.id,
validFrom: AT("2026-03-01T00:00:00.000Z"),
validUntil: AT("2026-05-01T00:00:00.000Z"),
payload: [{ windowStart: "08:00", windowEnd: "10:00", dose: "10 mg" }],
source: "MANUAL",
},
});
await prisma.medicationScheduleRevision.create({
data: {
medicationId: medication.id,
validFrom: AT("2026-02-01T00:00:00.000Z"),
validUntil: AT("2026-03-01T00:00:00.000Z"),
payload: [{ windowStart: "12:00", windowEnd: "13:00", dose: "10 mg" }],
source: "ARCHIVED",
supersededByRevisionId: correctionEra.id,
},
});
// v1.43 — a composition entered in a non-canonical unit, so the entered
// amount and unit are set too.
await prisma.medicationIngredient.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
nutrient: "vitamin_d",
amountPerUnit: 25,
unit: "ug",
enteredAmount: 1000,
enteredUnit: "IU",
source: "LABEL_AI",
},
});
await prisma.medicationPauseEra.createMany({
data: [
{
userId: OWNER_ID,
medicationId: medication.id,
pausedAt: AT("2026-05-02T08:00:00.000Z"),
resumedAt: AT("2026-05-20T08:00:00.000Z"),
},
{
userId: OWNER_ID,
medicationId: medication.id,
pausedAt: AT("2026-07-15T08:00:00.000Z"),
resumedAt: null,
},
],
});
// A RATED tag the account defined itself — the export carries only rated
// links, so a BINARY tag would leave `MoodEntryTagLink` empty.
const moodCategory = await prisma.moodTagCategory.findFirstOrThrow({
where: { userId: null },
});
// A category the account created itself, not a seeded one. It cascades away
// with the user, and `MoodTag.categoryId` is a real foreign key — so before
// the category travelled, this one row made the restore violate
// `mood_tags_category_id_fkey` and answer 500 with the account left empty.
const ownCategory = await prisma.moodTagCategory.create({
data: {
userId: OWNER_ID,
key: "round_trip_own_category",
labelKey: "mood.category.roundTripOwn",
sortOrder: 90,
},
});
const moodTag = await prisma.moodTag.create({
data: {
userId: OWNER_ID,
categoryId: ownCategory.id,
key: "round_trip_factor",
labelKey: "mood.tag.roundTripFactor",
kind: "RATED",
scaleMin: 1,
scaleMax: 5,
},
});
// A SEEDED tag the account chose to hide. The id differs per instance, so
// this is the row that proves the hidden set travels by key.
const seededTagToHide = await prisma.moodTag.findFirstOrThrow({
where: { userId: null },
});
await prisma.moodTagHidden.create({
data: { userId: OWNER_ID, moodTagId: seededTagToHide.id },
});
await prisma.moodEntry.create({
data: {
userId: OWNER_ID,
date: "2026-07-01",
mood: "GUT",
score: 4,
source: "MOODLOG",
moodLoggedAt: AT("2026-07-01T20:00:00.000Z"),
tagLinks: { create: { moodTagId: moodTag.id, rating: 4 } },
context: {
create: {
userId: OWNER_ID,
workStatus: "regular",
contactCircles: JSON.stringify(["family"]),
leisureJoy: 6,
},
},
},
});
const biomarker = await prisma.biomarker.create({
data: { userId: OWNER_ID, name: "Ferritin", unit: "ng/mL" },
});
// Another spelling of the marker, and the ledger row of a marker that was
// merged into it: both ride the marker (D1223).
await prisma.biomarkerAlias.create({
data: {
userId: OWNER_ID,
biomarkerId: biomarker.id,
name: "Ferr.",
nameFolded: "ferr.",
source: "IMPORT_CHOICE",
},
});
await prisma.biomarkerMerge.create({
data: {
userId: OWNER_ID,
targetBiomarkerId: biomarker.id,
sourceBiomarkerId: "merged-away-marker",
sourceName: "Ferritin (Serum)",
readingCount: 3,
},
});
const labResult = await prisma.labResult.create({
data: {
userId: OWNER_ID,
biomarkerId: biomarker.id,
analyte: "Ferritin",
value: 91,
unit: "ng/mL",
takenAt: AT("2026-06-30T09:00:00.000Z"),
},
});
// What the drug above is FOR, pinned to the analyte just created. The two
// rows are seeded at opposite ends of this fixture on purpose: the drug is
// restored hundreds of lines before the analyte, so this pairing is what
// proves the second pass runs late enough to resolve the name.
await prisma.medicationEfficacyTarget.create({
data: {
medicationId: medication.id,
biomarkerId: biomarker.id,
primary: true,
},
});
await prisma.allergy.create({
data: { userId: OWNER_ID, substance: "Penicillin" },
});
await prisma.familyHistoryEntry.create({
data: {
userId: OWNER_ID,
relationship: "MOTHER",
condition: "Type 2 diabetes",
},
});
const episode = await prisma.illnessEpisode.create({
data: {
userId: OWNER_ID,
label: "Cold",
type: "INFECTION",
onsetAt: AT("2026-06-20T00:00:00.000Z"),
},
});
// A SECOND condition, and the one the document below is actually filed
// against. Two episodes is what makes the filing assertion mean something:
// with one, a restore that pointed every link at the first condition it
// found would return the right count and read as correct.
const filedEpisode = await prisma.illnessEpisode.create({
data: {
userId: OWNER_ID,
label: "Iron deficiency",
type: "CHRONIC",
onsetAt: AT("2026-03-01T00:00:00.000Z"),
// v1.39.2 — a condition's body site and side travel too.
bodySiteEncrypted: encryptToBytes("Stomach"),
laterality: "BOTH",
},
});
const illnessSymptom = await prisma.illnessSymptom.create({
data: { key: "round_trip_cough", labelKey: "illness.symptom.roundTrip" },
});
await prisma.illnessDayLog.create({
data: {
userId: OWNER_ID,
episodeId: episode.id,
date: "2026-06-21",
symptomLinks: { create: { symptomId: illnessSymptom.id, severity: 2 } },
},
});
// v1.40 — two of the person's own symptoms and three occurrences: one filed
// against the open cold, one with a note, one on its own in the gap.
const aura = await prisma.symptomDefinition.create({
data: {
userId: OWNER_ID,
labelEncrypted: encryptToBytes("Aura"),
icon: "Zap",
sortOrder: 0,
},
});
const headache = await prisma.symptomDefinition.create({
data: {
userId: OWNER_ID,
labelEncrypted: encryptToBytes("Headache"),
icon: "Brain",
sortOrder: 1,
isActive: false,
},
});
await prisma.symptomEvent.create({
data: {
userId: OWNER_ID,
definitionId: aura.id,
occurredAt: AT("2026-06-21T09:00:00.000Z"),
intensity: 4,
episodeId: episode.id,
},
});
await prisma.symptomEvent.create({
data: {
userId: OWNER_ID,
definitionId: headache.id,
occurredAt: AT("2026-06-21T11:00:00.000Z"),
intensity: 8,
noteEncrypted: encryptToBytes("left side, behind the eye"),
},
});
await prisma.symptomEvent.create({
data: {
userId: OWNER_ID,
definitionId: aura.id,
occurredAt: AT("2026-07-10T18:30:00.000Z"),
intensity: 2,
},
});
// v1.42 — two life events: a move known to the month, with a note, and a
// period known to the day.
await prisma.lifeEvent.create({
data: {
userId: OWNER_ID,
category: "HOME",
startDate: "2023-09-01",
precision: "MONTH",
titleEncrypted: encryptToBytes("Moved to the coast"),
noteEncrypted: encryptToBytes("the flat with the long hallway"),
},
});
await prisma.lifeEvent.create({
data: {
userId: OWNER_ID,
category: "WORK",
startDate: "2026-03-02",
endDate: "2026-05-29",
precision: "DAY",
titleEncrypted: encryptToBytes("Parental leave"),
},
});
await prisma.userHealthProfile.create({
data: {
userId: OWNER_ID,
aboutMeEncrypted: encryptToBytes("Runs three times a week"),
},
});
await prisma.healthProfileFactRevision.create({
data: {
userId: OWNER_ID,
kind: "SMOKING_STATUS",
valueEncrypted: encryptToBytes("never"),
validFrom: AT("2026-01-01T00:00:00.000Z"),
},
});
await prisma.cycleProfile.create({
data: { userId: OWNER_ID, cycleTrackingEnabled: true },
});
const cycle = await prisma.menstrualCycle.create({
data: { userId: OWNER_ID, startDate: "2026-06-01" },
});
const cycleCategory = await prisma.cycleSymptomCategory.findFirstOrThrow();
const cycleSymptom = await prisma.cycleSymptom.create({
data: {
userId: OWNER_ID,
categoryId: cycleCategory.id,
key: "custom:round-trip-cramps",
labelKey: "cycle.symptom.custom",
labelEncrypted: encrypt("Round-trip cramps"),
},
});
await prisma.cycleDayLog.create({
data: {
userId: OWNER_ID,
cycleId: cycle.id,
date: "2026-06-02",
flow: "HEAVY",
symptomLinks: { create: { symptomId: cycleSymptom.id } },
},
});
await prisma.customMetric.create({
data: {
userId: OWNER_ID,
name: "Grip strength",
unit: "kg",
// v1.39.3 — one reading whose note is still readable (a row the
// free-text backfill has not reached) and one sealed the current way.
// Both must come back sealed, and neither readable.
entries: {
create: [
{
userId: OWNER_ID,
value: 44,
unit: "kg",
measuredAt: AT("2026-07-01T18:00:00.000Z"),
note: METRIC_LEGACY_NOTE,
},
{
userId: OWNER_ID,
value: 45,
unit: "kg",
measuredAt: AT("2026-07-02T18:00:00.000Z"),
noteEncrypted: encryptToBytes(METRIC_SEALED_NOTE),
},
],
},
},
});
await prisma.correlationPattern.create({
data: {
userId: OWNER_ID,
canonicalKey: `p1:${"a1".repeat(32)}`,
family: "sleep",
factorKey: "sleep_duration",
outcomeKey: "weight",
lagDays: 1,
sampleSize: 30,
effectSize: 0.42,
pValue: 0.01,
evidenceHash: "b2".repeat(32),
lastComputedAt: AT("2026-07-01T00:00:00.000Z"),
},
});
await prisma.onboardingRecord.create({
data: {
userId: OWNER_ID,
needsJson: {
recordTarget: "me",
areas: ["blood-pressure", "sleep"],
medication: "yes",
sources: ["withings"],
visit: "within-a-month",
units: { glucoseUnit: "mmol/L", unitPreference: "metric" },
},
stepsJson: [
{ id: "who", status: "done" },
{ id: "areas", status: "done" },
{ id: "sources", status: "skipped" },
],
firstResultJson: {
task: "connect-source",
target: "withings",
completedAt: "2026-07-01T08:30:00.000Z",
},
// The once-only latch on the module derivation. Seeded non-null so the
// assertion after the restore can show it came back rather than
// defaulting — a null here would hand the next confirm permission to
// re-apply the questionnaire over every module decision made since.
modulesDerivedAt: AT("2026-07-01T08:00:00.000Z"),
completedAt: AT("2026-07-01T08:00:00.000Z"),
},
});
await prisma.healthScoreRecord.create({
data: {
userId: OWNER_ID,
dayKey: "2026-07-01",
timezone: "Europe/Berlin",
composite: 71,
band: "green",
scoreVersion: 1,
composition: ["activity", "sleep"],
pillarScores: { activity: 70, sleep: 72 },
inputFingerprint: "c3".repeat(32),
},
});
await prisma.workout.create({
data: {
userId: OWNER_ID,
sportType: "RUNNING",
startedAt: AT("2026-06-29T06:00:00.000Z"),
endedAt: AT("2026-06-29T06:30:00.000Z"),
durationSec: 1800,
},
});
await prisma.nutrientIntakeDay.create({
data: {
userId: OWNER_ID,
day: "2026-07-01",
nutrient: "WATER",
amount: 2100,
unit: "ml",
source: "MANUAL",
},
});
const documentBytes = encryptBytes(Buffer.from("round-trip document bytes"));
const contentEncrypted = new Uint8Array(
new ArrayBuffer(documentBytes.byteLength),
);
contentEncrypted.set(documentBytes);
const document = await prisma.inboundDocument.create({
data: {
userId: OWNER_ID,
kind: "LAB_RESULT",
title: "June labs",
filename: "june-labs.pdf",
mimeType: "application/pdf",
byteSize: documentBytes.byteLength,
contentEncrypted,
contentCodec: "binary2",
// Imported (#1038): the source key has to survive the trip, or a
// re-run of the importer after a restore stores the page twice.
sourceSystem: "PAPERLESS",
sourceId: "4711",
// Held back from automatic AI reading; a restore that dropped the
// marker would hand the page to the summary catch-up.
aiReadDeferred: true,
},
});
// The page filed under the SECOND condition, not the first. A restore that
// kept the count and lost the pairing would leave the vault sorted wrongly
// rather than visibly unsorted, which is harder to notice and worse.
await prisma.documentConditionLink.create({
data: {
userId: OWNER_ID,
documentId: document.id,
episodeId: filedEpisode.id,
createdAt: AT("2026-07-02T09:00:00.000Z"),
},
});
// One staged fact, already reviewed, approved and committed to the ferritin
// lab row above. Every column that records the decision is set AWAY from its
// schema default on purpose: `PENDING`, `needsReview: true` and two NULL
// commitment columns are what a restore that ignores them writes, and the
// assertion after the restore is what catches that. A fact handed back as
// PENDING is offered for review again, and approving it a second time writes
// a second lab result for a reading the account already has.
await prisma.extractedFact.create({
data: {
userId: OWNER_ID,
documentId: document.id,
factType: "OBSERVATION",
status: "APPROVED",
confidence: 0.94,
needsReview: false,
committedRecordId: labResult.id,
committedRecordType: "labResult",
dataEncrypted: encryptFactData({
label: "Ferritin",
code: null,
codeSystem: null,
value: 91,
valueText: null,
unit: "ng/mL",
referenceLow: 30,
referenceHigh: 400,
effectiveDate: "2026-06-30",
}),
provenanceEncrypted: encryptFactProvenance({
sourceText: EXTRACTED_FACT_SPAN,
anchored: true,
sourceOffset: 412,
page: 2,
confidence: 0.94,
}),
},
});
// Two Coach threads, and the pair is the point. One is an ordinary health
// conversation; the other is FENCED — `documentScoped` true, grounded in the
// document seeded above. A restore that let the flag default to false would
// return both threads, satisfy every count, and quietly hand the tool loop
// back to the one conversation whose history may contain document-derived
// text. That is why the assertion below reads the flag per thread rather
// than counting rows.
await prisma.coachConversation.create({
data: {
userId: OWNER_ID,
title: "How is my blood pressure trending?",
documentScoped: false,
summaryEncrypted: encryptToBytes(COACH_SUMMARY),
summaryUpdatedAt: AT("2026-07-20T10:00:00.000Z"),
summaryTurnCount: 4,
messages: {
create: [
{
role: "user",
encryptedContent: encryptToBytes(COACH_USER_TURN),
createdAt: AT("2026-07-20T09:58:00.000Z"),
},
{
role: "assistant",
encryptedContent: encryptToBytes(COACH_ASSISTANT_TURN),
resultsEncrypted: encryptToBytes(COACH_RESULTS_JSON),
trailEncrypted: encryptToBytes(COACH_TRAIL_JSON),
providerType: "anthropic",
model: "claude-opus-5",
tokensUsed: 812,
createdAt: AT("2026-07-20T09:59:00.000Z"),
},
],
},
},
});
await prisma.coachConversation.create({
data: {
userId: OWNER_ID,
// v1.39.3 — sealed the current way; the thread above keeps a readable
// legacy title, the shape of a row the backfill has not reached.
titleEncrypted: encryptToBytes("About my June labs"),
documentScoped: true,
messages: {
create: [
{
role: "user",
encryptedContent: encryptToBytes("What does the ferritin mean?"),
createdAt: AT("2026-07-21T08:00:00.000Z"),
},
],
},
attachments: {
create: [
{ documentId: document.id, addedAt: AT("2026-07-21T07:59:00.000Z") },
],
},
},
});
// What the Coach keeps between threads, seeded so both references are
// exercised: the fact and the plan point back at the thread they came out
// of, and the reminder points at the plan.
const healthThread = await prisma.coachConversation.findFirstOrThrow({
where: { userId: OWNER_ID, documentScoped: false },
});
const healthAnswer = await prisma.coachMessage.findFirstOrThrow({
where: { conversationId: healthThread.id, role: "assistant" },
});
await prisma.coachFact.create({
data: {
userId: OWNER_ID,
factEncrypted: encryptToBytes(COACH_FACT),
category: "constraint",
confidence: 80,
sourceConversationId: healthThread.id,
source: "user",
lastUsedAt: AT("2026-07-20T10:00:00.000Z"),
sourceMessageId: healthAnswer.id,
},
});
const plan = await prisma.coachPlan.create({
data: {
userId: OWNER_ID,
metric: "BLOOD_PRESSURE",
ifCueEncrypted: encryptToBytes(COACH_PLAN_CUE),
thenActionEncrypted: encryptToBytes(COACH_PLAN_ACTION),
targetEncrypted: encryptToBytes("under 130 by October"),
status: "active",
reviewDate: AT("2026-10-01T00:00:00.000Z"),
sourceConversationId: healthThread.id,
},
});
await prisma.coachReminder.create({
data: {
userId: OWNER_ID,
noteEncrypted: encryptToBytes(COACH_REMINDER_NOTE),
metric: "BLOOD_PRESSURE",
relatedPlanId: plan.id,
triggerKind: "date",
dueAt: AT("2026-09-01T07:00:00.000Z"),
status: "active",
source: "extractor",
sourceConversationId: healthThread.id,
surfaceCount: 2,
},
});
// One visit, the practice it was at, and one link of each of the three
// kinds. The links reach the document, the lab result and the condition
// episode seeded above rather than fresh rows, because a link to something
// the restore did not put back is the exact case the restore reports as a
// skip — seeding it against a row that does come back is what makes a
// returning count mean the filing survived.
const practitioner = await prisma.practitioner.create({
data: {
userId: OWNER_ID,
name: "Round-trip practice",
specialty: "Internal medicine",
noteEncrypted: encryptToBytes("ring the upper bell"),
},
});
// One Vorsorge reminder with the v1.37.20 skip/snooze columns all set, and
// two completion-ledger rows — one honest satisfy, one honest skip. Fat like
// the vaccination fixture below and for the same reason: the count-back
// proves the rows returned, the column-by-column assertion after the restore
// proves they returned WHOLE. The encounter and the dose below point their
// `reminderId` here, because both references now remap against the restored
// reminders and this fixture is what proves they survive rather than drop.
const reminder = await prisma.measurementReminder.create({
data: {
userId: OWNER_ID,
label: "Blutdruck messen",
measurementType: "BLOOD_PRESSURE_SYS",
intervalDays: 7,
notifyHour: 8,
location: "Zuhause",
nextDueAt: AT("2026-07-08T06:00:00.000Z"),
lastSatisfiedAt: AT("2026-07-01T06:05:00.000Z"),
vaccinationAntigen: "tetanus",
snoozedUntil: AT("2026-07-08T06:00:00.000Z"),
lastSkippedAt: AT("2026-06-24T06:00:00.000Z"),
skipCount: 2,
},
});
await prisma.measurementReminderEvent.create({
data: {
userId: OWNER_ID,
reminderId: reminder.id,
kind: "SATISFIED",
occurredAt: AT("2026-07-01T06:05:00.000Z"),
onTime: true,
source: "manual",
},
});
await prisma.measurementReminderEvent.create({
data: {
userId: OWNER_ID,
reminderId: reminder.id,
kind: "SKIPPED",
occurredAt: AT("2026-06-24T06:00:00.000Z"),
onTime: false,
source: "skip",
},
});
const encounter = await prisma.encounter.create({
data: {
userId: OWNER_ID,
occurredAt: AT("2026-06-30T08:00:00.000Z"),
status: "DONE",
kind: "ROUTINE",
practitionerId: practitioner.id,
reasonEncrypted: encryptToBytes("annual check"),
outcomeEncrypted: encryptToBytes(
"ferritin back in range, recheck in a year",
),
reminderId: reminder.id,
},
});
await prisma.encounterDocumentLink.create({
data: {
userId: OWNER_ID,
encounterId: encounter.id,
documentId: document.id,
},
});
await prisma.encounterLabLink.create({
data: {
userId: OWNER_ID,
encounterId: encounter.id,
labResultId: labResult.id,
},
});
await prisma.encounterConditionLink.create({
data: {
userId: OWNER_ID,
encounterId: encounter.id,
episodeId: episode.id,
},
});
// One dose with every optional column filled, and the page it was
// transcribed from. Fat on purpose, against this file's own minimal-fixture
// rule: a count-back proves the row returned, and the assertion after the
// restore reads each column back so it also proves the row returned WHOLE.
// The practitioner, the encounter and the reminder are the ones seeded
// above, because all three are remapped on the way back and a reference to
// something the restore did not put back is a different case (covered by
// the skip test below). `reminderId` used to be deliberately absent here —
// it was the one reference that could never resolve — and is deliberately
// present since v1.37.20, because resolving is now the behaviour under test.
// v1.42 (#1005) — the person's own vaccine definition the dose also names.
// The catalogue slug still wins on read; the reference is what has to
// survive the trip, and it can only if the definition comes back first.
const customVaccine = await prisma.customVaccine.create({
data: {
userId: OWNER_ID,
name: "Round-trip travel combo",
components: ["typhoid", "hepatitis-a"],
typicalSeriesDoses: 2,
boosterIntervalMonths: 36,
},
});
const vaccination = await prisma.vaccinationRecord.create({
data: {
userId: OWNER_ID,
occurredAt: AT("2026-05-14T00:00:00.000Z"),
customVaccineId: customVaccine.id,
antigenSlug: "tdap",
vaccineName: "Tetanus, diphtheria and pertussis",
doseNumber: 3,
seriesDoses: 3,
lotNumber: "RT-4471",
site: "LEFT_ARM",
practitionerId: practitioner.id,
encounterId: encounter.id,
reminderId: reminder.id,
noteEncrypted: encryptToBytes("sore arm for a day, nothing else"),
},
});
await prisma.vaccinationDocumentLink.create({
data: {
userId: OWNER_ID,
vaccinationId: vaccination.id,
documentId: document.id,
},
});
// v1.43 — one row in each of the five relation link tables, joining the
// records seeded above.
await prisma.labResultDocumentLink.create({
data: {
userId: OWNER_ID,
labResultId: labResult.id,
documentId: document.id,
},
});
await prisma.encounterMedicationLink.create({
data: {
userId: OWNER_ID,
encounterId: encounter.id,
medicationId: medication.id,
},
});
await prisma.encounterVaccinationLink.create({
data: {
userId: OWNER_ID,
encounterId: encounter.id,
vaccinationId: vaccination.id,
},
});
await prisma.medicationDocumentLink.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
documentId: document.id,
},
});
await prisma.medicationConditionLink.create({
data: {
userId: OWNER_ID,
medicationId: medication.id,
episodeId: episode.id,
},
});
// Three bests, chosen for what a count cannot see.
//
// The first points at the measurement it was found in. That column is a
// foreign key in the database while the Prisma schema declares no relation
// for it, so this row is what proves the pointer resolves against the
// measurements the restore wrote, and would prove the opposite loudly, by
// failing the constraint and taking the whole restore with it.
//
// The other two share a metric type AND an instant and differ only in the
// sport slot: the same 5 km and 10 km bests a runner has. A restore that
// dropped the slot would return two rows describing one record twice, and
// the faster time would be presented as the 10 km best.
await prisma.personalRecord.create({
data: {
userId: OWNER_ID,
metricType: "WEIGHT",
direction: "MIN",
value: 74.2,
unit: "kg",
achievedAt: AT("2026-07-01T07:00:00.000Z"),
sourceMeasurementId: weightMeasurement.id,
source: "MANUAL",
},
});
await prisma.personalRecord.createMany({
data: [
{
userId: OWNER_ID,
metricType: "WALKING_RUNNING_DISTANCE",
metricSlot: "running_5km_time",
direction: "MIN",
value: 1512,
unit: "s",
achievedAt: AT("2026-05-18T09:20:00.000Z"),
source: "APPLE_HEALTH",
externalId: "workout-pr-5km",
},
{
userId: OWNER_ID,
metricType: "WALKING_RUNNING_DISTANCE",
metricSlot: "running_10km_time",
direction: "MIN",
value: 3184,
unit: "s",
achievedAt: AT("2026-05-18T09:20:00.000Z"),
source: "APPLE_HEALTH",
externalId: "workout-pr-10km",
},
],
});
// Two badges, both earned long before this fixture runs. The second names a
// definition no catalogue in this build ships: a file can be older than the
// release reading it, and the restore must carry the row rather than judge
// it, exactly as it carries a retired antigen slug.
await prisma.userAchievement.createMany({
data: [
{
userId: OWNER_ID,
achievementId: "intake-total-10",
unlockedAt: AT("2026-02-21T10:15:00.000Z"),
},
{
userId: OWNER_ID,
achievementId: "retired-badge-from-an-older-release",
unlockedAt: AT("2025-11-05T08:00:00.000Z"),
},
],
});
// A fortnight in Barcelona, and one day of it recorded as a reading.
//
// The pair is the fixture. The home day and the trip day carry different
// coordinates and different weather, and the trip day's reading only makes
// sense next to the period that explains it: with the period gone, the next
// refresh re-resolves 2026-06-15 to Berlin and upserts Berlin's weather over
// it. The assertion after the restore reads the two back TOGETHER. The
// coordinates are at 1 decimal, the precision the app stores since v1.39.4;
// a restore rounds anything finer, so finer values would not round-trip.
// Since v1.42 the location is stored sealed (`locationEncrypted`), the way
// the writers store it; it is the only copy since v1.43.
// v1.43 — one Dawarich day abroad, with its sealed country.
await prisma.dawarichDay.create({
data: {
userId: OWNER_ID,
date: "2026-06-15",
totalDistanceM: 48_300,
drivingDistanceM: 12_000,
trainDistanceM: 30_500,
placesVisited: 4,
awayFromHomeMinutes: 1440,
countryCodeEncrypted: sealDayCountry("ES"),
isTravelDay: true,
fetchedAt: new Date("2026-06-16T03:20:00.000Z"),
},
});
await prisma.environmentTravelLocation.create({
data: {
userId: OWNER_ID,
startDate: "2026-06-10",
endDate: "2026-06-20",
locationEncrypted: sealLocation({
lat: 41.4,
lon: 2.2,
label: "Barcelona",
}),
},
});
await prisma.environmentContext.createMany({
data: [
{
userId: OWNER_ID,
date: "2026-06-15",
locationEncrypted: sealLocation({
lat: 41.4,
lon: 2.2,
label: "Barcelona",
}),
source: "TRAVEL",
tempMin: 19.4,
tempMax: 28.1,
tempMean: 23.6,
apparentMean: 25.2,
sunshineSec: 39_600,
daylightSec: 52_800,
precipSum: 0,
pressureMean: 1016.4,
pressureDelta: 3.2,
humidityMean: 63,
cloudMean: 12,
weatherCode: 1,
fetchedAt: AT("2026-06-16T03:15:00.000Z"),
},
{
userId: OWNER_ID,
date: "2026-07-01",
locationEncrypted: sealLocation({
lat: 52.5,
lon: 13.4,
label: "Berlin",
}),
source: "HOME",
tempMin: 13.1,
tempMax: 22.7,
tempMean: 17.9,
apparentMean: 17.1,
sunshineSec: 28_800,
daylightSec: 59_400,
precipSum: 4.6,
pressureMean: 1008.9,
pressureDelta: 7.8,
humidityMean: 74,
cloudMean: 68,
weatherCode: 61,
fetchedAt: AT("2026-07-02T03:15:00.000Z"),
},
],
});
}
export async function createOwner(prisma: PrismaClient) {
return prisma.user.create({
data: {
id: OWNER_ID,
username: "round-trip-owner",
email: "round-trip-owner@example.test",
},
});
}
export async function seedAdminSession(prisma: PrismaClient) {
const admin = await prisma.user.create({
data: {
username: "round-trip-admin",
email: "round-trip-admin@example.test",
role: "ADMIN",
},
});
const session = await prisma.session.create({
data: { userId: admin.id, expiresAt: new Date(Date.now() + 60_000) },
});
cookieJar.set("healthlog_session", session.id);
return admin;
}
/**
* Columns the restore is right not to bring back verbatim, by `Model.column`.
* Every other column of every two-ended model must come back with the value
* it left with. An entry here whose column no longer exists fails the run.
*/
export const COLUMN_EXCLUSIONS: Readonly<Record<string, string>> = {
"MeasurementReminder.lastNotifiedAt":
"operational repeat cursor, documented on the column as not in the backup: a restored reminder re-reminds at most once, on its next notify hour",
// When a join row was written. Nothing reads these: the links are read for
// which tag, symptom or context applies, never for when the row appeared,
// and the parent entry carries its own timestamps, which do travel.
"MoodContext.createdAt":
"insert stamp of the per-entry context row; no reader, the mood entry's own timestamps travel",
"MoodEntryTagLink.createdAt":
"insert stamp of a tag link; no reader, the mood entry's own timestamps travel",
"CycleSymptomLink.createdAt":
"insert stamp of a symptom link; no reader, the day log's own timestamps travel",
"IllnessSymptomLink.createdAt":
"insert stamp of a symptom link; no reader, the illness day log's own timestamps travel",
};
/**
* Columns the restore carries on purpose as "set where it was set" rather
* than the same value, each with the reason. A presence check still fails a
* column that stops coming back at all.
*/
export const COLUMN_PRESENCE_ONLY: Readonly<Record<string, string>> = {
"Medication.autoMarkTakenSince":
"the auto-take floor restarts at the restore, so doses a portable file cannot carry as removed are not filled again; pinned in the restore test for it",
};
/**
* Values for columns the fixture leaves empty where the type alone does not
* produce one the export and the restore accept.
*/
export const FILL_OVERRIDES: Readonly<Record<string, unknown>> = {
"MedicationSchedule.rrule": "FREQ=WEEKLY;BYDAY=MO,TH",
"InboundDocument.sourceInstance": "https://paperless.example.test",
// The digest of the fixture's own document bytes, so a restore that checks
// it against the content finds them consistent.
"InboundDocument.contentSha256": createHash("sha256")
.update("round-trip document bytes")
.digest("hex"),
"Biomarker.contextEncrypted": () =>
encryptContextToBytes("fasted, morning draw"),
"MoodEntry.tags": JSON.stringify(["round-trip-tag"]),
"CorrelationPattern.qValue": 0.04,
"CorrelationPattern.dismissedEvidenceHash": "ab".repeat(32),
// CHECK in migration 0408: the profile is built from samples or from hour buckets.
"IntradayCumulativeProfile.basis": "HOUR_BUCKETS",
// The posted hours are a whole day of slots (-1 for an hour not posted);
// the restore refuses any other length, as it does for the curve.
"IntradayCumulativeProfile.hourlyIncrements": Array.from(
{ length: 24 },
(_, hour) => (hour < 6 ? -1 : 350),
),
};
/**
* Rows for the columns a CHECK constraint ties to a shape the base fixture
* does not have: a stage only on a sleep reading, a context only on a glucose
* reading, a rolling interval only on a schedule without a calendar rule.
*/
export async function seedColumnShapes(prisma: PrismaClient): Promise<void> {
await prisma.measurement.create({
data: {
userId: OWNER_ID,
type: "BLOOD_GLUCOSE",
value: 5.4,
unit: "mmol/L",
measuredAt: AT("2026-07-03T06:45:00.000Z"),
source: "MANUAL",
glucoseContext: "FASTING",
},
});
await prisma.measurement.create({
data: {
userId: OWNER_ID,
type: "SLEEP_DURATION",
value: 95,
unit: "min",
measuredAt: AT("2026-07-03T02:00:00.000Z"),
source: "APPLE_HEALTH",
externalId: "round-trip-deep-sleep",
sleepStage: "DEEP",
},
});
// A start logged out of order and folded into the one before it: soft
// deleted, and pointing at the start that absorbed it so that removing that
// start can bring this one back.
const absorbing = await prisma.menstrualCycle.findFirstOrThrow({
where: { userId: OWNER_ID, startDate: "2026-06-01" },
});
await prisma.menstrualCycle.create({
data: {
userId: OWNER_ID,
startDate: "2026-06-03",
deletedAt: AT("2026-06-04T07:00:00.000Z"),
absorbedIntoId: absorbing.id,
},
});
const medication = await prisma.medication.findFirstOrThrow({
where: { userId: OWNER_ID },
});
await prisma.medicationSchedule.create({
data: {
medicationId: medication.id,
windowStart: "20:00",
windowEnd: "21:00",
label: "Every third evening",
rollingIntervalDays: 3,
},
});
}
export type Delegate = {
findMany(args: {
where: Record<string, unknown>;
}): Promise<Record<string, unknown>[]>;
update(args: {
where: Record<string, unknown>;
data: Record<string, unknown>;
}): Promise<unknown>;
};
export function delegateOf(prisma: PrismaClient, model: AuditModel): Delegate {
return (prisma as unknown as Record<string, Delegate>)[model.delegate];
}
export function primaryKeyWhere(
model: AuditModel,
row: Record<string, unknown>,
): Record<string, unknown> {
if (model.primaryKey.length === 1) {
return { [model.primaryKey[0]]: row[model.primaryKey[0]] };
}
return {
[model.primaryKey.join("_")]: Object.fromEntries(
model.primaryKey.map((k) => [k, row[k]]),
),
};
}
/** How a column is compared, or why it is not. */
export function comparison(
column: AuditColumn,
): "value" | "presence" | { excluded: string } {
const key = `${column.model}.${column.name}`;
if (key in COLUMN_EXCLUSIONS) return { excluded: COLUMN_EXCLUSIONS[key] };
if (column.isId) {
return { excluded: "primary key; the restore may mint a fresh one" };
}
if (column.name === "userId") {
return { excluded: "owner; re-derived from the restoring account" };
}
if (column.isUpdatedAt) {
return { excluded: "@updatedAt; Prisma stamps it on the restore's write" };
}
// A declared foreign key may be re-pointed at a row the restore minted, so
// it is held to being set where it was set, not to the same id.
if (column.isForeignKey) return "presence";
if (key in COLUMN_PRESENCE_ONLY) return "presence";
return "value";
}
/**
* A 1x1 PNG: the smallest picture the avatar upload, and so the restore,
* accepts.
*/
export const AVATAR_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
"base64",
);
/**
* Values for `User` columns where the type alone would produce one the
* restore rightly refuses: a vocabulary, a time zone, an AI endpoint, a
* threshold band, an image.
*/
export const USER_FILL_OVERRIDES: Readonly<Record<string, unknown>> = {
"User.gender": "FEMALE",
"User.locale": "fr",
"User.timezone": "America/Chicago",
"User.homeTimezone": "Asia/Tokyo",
"User.unitPreference": "imperial",
"User.glucoseUnit": "mmol/L",
"User.insightsPrivacyMode": "raw",
"User.aiProvider": "LOCAL",
"User.aiBaseUrl": "https://llm.example.com/v1",
"User.aiCompatBaseUrl": "https://gateway.example.com/v1",
"User.thresholdsJson": { WEIGHT: { min: 60, max: 80 } },
// The restore admits goal by goal against the goals schema, so the generic
// `{ roundTrip }` blob would be refused as an unknown key, not carried.
"User.activityGoalsJson": { workoutsPerWeek: 4, stepsPerMonth: 250_000 },
"User.globalExcludedInjectionSites": ["THIGH_LEFT"],
"User.avatarBytes": () => {
const bytes = new Uint8Array(new ArrayBuffer(AVATAR_PNG.byteLength));
bytes.set(AVATAR_PNG);
return bytes;
},
"User.avatarContentType": "image/png",
// v1.42 — a sealed home the way the writers seal it, so it opens.
"User.homeLocationEncrypted": () =>
sealLocation({ lat: 2.5, lon: 2.5, label: "round-trip home" }),
};