mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(backup): keep an empty provenance empty on restore
A current disaster-recovery backup writes aggregationProvenance for every measurement, null included. The restore treated null like a missing field and marked every Apple Health stats: row without a provenance as LEGACY_UNKNOWN, which is meant only for backups too old to carry the field. The nightly daily-mean fold writes exactly such rows, so on a large Apple Health account a restore changed tens of thousands of readings it was supposed to put back as they were (#1031). Only an absent field now takes the legacy path.
This commit is contained in:
1 parent
af65f01fec
commit
fabd691128
2 files changed
+58
-4
No files matched your search
@@ -768,11 +768,18 @@ export async function restoreBackup(
|
||||
// their cumulative dailies too (Fitbit, Google Health) and have
|
||||
// never carried provenance — marking those would invent a state
|
||||
// they were never in. Ordinary point rows keep NULL honestly.
|
||||
aggregationProvenance: (measurement.aggregationProvenance ??
|
||||
(measurement.source === "APPLE_HEALTH" &&
|
||||
measurement.externalId?.startsWith("stats:")
|
||||
// Only an ABSENT field is the old-backup case. A current backup
|
||||
// writes the field for every row, and an explicit null is the row's
|
||||
// real state: the nightly daily-mean fold writes Apple Health
|
||||
// `stats:` rows with no provenance. Treating that null as absent
|
||||
// turned every such row into LEGACY_UNKNOWN on restore (#1031).
|
||||
aggregationProvenance: (measurement.aggregationProvenance !==
|
||||
undefined
|
||||
? measurement.aggregationProvenance
|
||||
: measurement.source === "APPLE_HEALTH" &&
|
||||
measurement.externalId?.startsWith("stats:")
|
||||
? "LEGACY_UNKNOWN"
|
||||
: null)) as never,
|
||||
: null) as never,
|
||||
glucoseContext: (measurement.glucoseContext ?? null) as never,
|
||||
sleepStage: (measurement.sleepStage ?? null) as never,
|
||||
rhythmClassification: (measurement.rhythmClassification ??
|
||||
|
||||
@@ -1324,4 +1324,51 @@ describe("canonical disaster-recovery backup round-trip", () => {
|
||||
{ id: "legacy-point-row", aggregationProvenance: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a provenance the backup states as empty, rather than marking it legacy", async () => {
|
||||
// The nightly daily-mean fold writes Apple Health `stats:` rows with no
|
||||
// provenance (respiratory rate, walking speed, …). A current backup says
|
||||
// so with an explicit null, which is not the same as a backup too old to
|
||||
// carry the field. Restoring those rows as LEGACY_UNKNOWN changed 18 618
|
||||
// rows of a 2.6 million-reading account on every restore (#1031).
|
||||
const prisma = getPrismaClient();
|
||||
const admin = await seedAdminSession();
|
||||
const payload = backupPayloadSchema.parse({
|
||||
schemaVersion: "2",
|
||||
exportedAt: "2026-07-25T00:00:00.000Z",
|
||||
userId: admin.id,
|
||||
measurements: [
|
||||
{
|
||||
id: "daily-mean-stats",
|
||||
type: "RESPIRATORY_RATE",
|
||||
value: 14.2,
|
||||
unit: "breaths/min",
|
||||
measuredAt: "2026-07-20T10:00:00.000Z",
|
||||
source: "APPLE_HEALTH",
|
||||
externalId:
|
||||
"stats:HKQuantityTypeIdentifierRespiratoryRate:2026-07-20",
|
||||
aggregationProvenance: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const backup = await prisma.dataBackup.create({
|
||||
data: {
|
||||
userId: admin.id,
|
||||
type: "PROVENANCE_EXPLICIT_NULL",
|
||||
data: encrypt(JSON.stringify(payload)),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
makeRequest(backup.id) as unknown as Parameters<typeof POST>[0],
|
||||
{ params: Promise.resolve({ id: backup.id }) },
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
expect(
|
||||
await prisma.measurement.findMany({
|
||||
where: { userId: admin.id },
|
||||
select: { id: true, aggregationProvenance: true },
|
||||
}),
|
||||
).toEqual([{ id: "daily-mean-stats", aggregationProvenance: null }]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user