fix(backup): the restore states a level-A value only when the file has one

Matches what syncVersion, syncedAt and the row stamps already do a few
lines above: a key the file does not carry is a key the file predates,
not a key the file says is empty. Reachable when one file addresses the
same row twice, where the second entry now adds without erasing what
the first one set; an explicit null still clears.

Worth stating plainly, because the audit that asked for this expected it
to protect live rows from an older file: it does not, and it cannot. The
restore deletes the owner's whole mood partition before it rebuilds, so
by the time the update arm runs there is nothing live left to protect. A
test now pins that contract too, so nobody reads the omission as a
guard it is not.
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 02:52:41 +02:00
1 parent d3577d2557
commit aa56246ba2
2 files changed
+153 -9

No files matched your search

@@ -774,15 +774,19 @@ const handler = apiHandler(
date: entry.date,
mood: entry.mood,
score: entry.score,
// The five level-A values. `?? null` rather than a derivation:
// a file that carries none is a file from before they existed,
// and filling them in here would put a number the person never
// gave into a restored row and call it their answer.
moodA1: entry.a1 ?? null,
stressA2: entry.a2 ?? null,
energyA3: entry.a3 ?? null,
connectionA4: entry.a4 ?? null,
stabilityA5: entry.a5 ?? null,
// The five level-A values, carried the same way `syncVersion`
// is: absent in the file means the file predates the columns,
// and an upsert whose update arm stated them would wipe live
// answers off every matched row when an older backup is
// restored over a going concern. A file that has them writes
// them, including an explicit null; a file that does not says
// nothing. Never derived — filling one in here would put a
// number the person never gave into a restored row.
...(entry.a1 === undefined ? {} : { moodA1: entry.a1 }),
...(entry.a2 === undefined ? {} : { stressA2: entry.a2 }),
...(entry.a3 === undefined ? {} : { energyA3: entry.a3 }),
...(entry.a4 === undefined ? {} : { connectionA4: entry.a4 }),
...(entry.a5 === undefined ? {} : { stabilityA5: entry.a5 }),
tags: entry.tags ?? null,
// The note comes back the way every other dual-column note
// does: ciphertext verbatim when the file carries it, and a