mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
A reading sent with a measurements:write token keeps its EXTERNAL source and now points at the token that carried it (measurements.ingest_token_id). Every stored row answers a server-resolved appleHealthMirror: true for WITHINGS, IMPORT and MANUAL, the token's choice for EXTERNAL, false otherwise. The plain list accepts appleHealthMirror=true so a client can page exactly those rows. Migration 0476 adds the columns and links existing EXTERNAL readings only where one measurement token provably sent them; 0477 builds the index concurrently. Backups carry the link and a restore keeps it only for a token the account still holds. A guard holds every EXTERNAL writer to setting the link.
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
/**
|
|
* Migration 0476 links existing EXTERNAL readings to the token that carried
|
|
* them only where that is provable (#1272): the account holds exactly one
|
|
* measurement token, revoked ones counted, and the reading is no older than
|
|
* the token. Everything else stays unlinked. The pass writes nothing but the
|
|
* link, so no reading's `updatedAt` or `syncVersion` moves, and a rerun
|
|
* changes nothing.
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import { MEASUREMENTS_WRITE_SCOPE } from "@/lib/measurements/scopes";
|
|
|
|
import { makeToken, makeUser } from "../kit/factories";
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
const prisma = getPrismaClient();
|
|
|
|
const MIGRATION = readFileSync(
|
|
resolve(
|
|
process.cwd(),
|
|
"prisma",
|
|
"migrations",
|
|
"0476_token_apple_health_mirror",
|
|
"migration.sql",
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
/** The file is three `DO` blocks; each goes through as its own statement. */
|
|
async function runMigration(): Promise<void> {
|
|
const blocks = MIGRATION.match(/DO \$migration\$[\s\S]*?\$migration\$;/g);
|
|
expect(blocks).toHaveLength(3);
|
|
for (const block of blocks!) await prisma.$executeRawUnsafe(block);
|
|
}
|
|
|
|
const TOKEN_AT = new Date("2026-06-01T00:00:00.000Z");
|
|
|
|
async function externalReading(
|
|
userId: string,
|
|
createdAt: Date,
|
|
source: "EXTERNAL" | "MANUAL" = "EXTERNAL",
|
|
) {
|
|
return prisma.measurement.create({
|
|
data: {
|
|
userId,
|
|
type: "WEIGHT",
|
|
value: 80,
|
|
unit: "kg",
|
|
source,
|
|
measuredAt: createdAt,
|
|
createdAt,
|
|
updatedAt: new Date("2026-07-01T00:00:00.000Z"),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
}
|
|
|
|
async function measurementToken(userId: string, revoked = false) {
|
|
const token = await makeToken(
|
|
{ id: userId },
|
|
{ permissions: [MEASUREMENTS_WRITE_SCOPE] },
|
|
);
|
|
await prisma.apiToken.update({
|
|
where: { id: token.tokenId },
|
|
data: { createdAt: TOKEN_AT, revoked },
|
|
});
|
|
return token.tokenId;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(prisma);
|
|
});
|
|
|
|
describe("migration 0476 backfill", () => {
|
|
it("links only what one token provably carried", async () => {
|
|
const sole = await makeUser({ timezone: "UTC" });
|
|
const soleToken = await measurementToken(sole.id, true);
|
|
// A wildcard sign-in token does not make the measurement token ambiguous.
|
|
await makeToken(sole, { permissions: ["*"] });
|
|
const after = await externalReading(
|
|
sole.id,
|
|
new Date("2026-06-02T08:00:00.000Z"),
|
|
);
|
|
const before = await externalReading(
|
|
sole.id,
|
|
new Date("2026-05-30T08:00:00.000Z"),
|
|
);
|
|
const manual = await externalReading(
|
|
sole.id,
|
|
new Date("2026-06-03T08:00:00.000Z"),
|
|
"MANUAL",
|
|
);
|
|
|
|
const two = await makeUser({ timezone: "UTC" });
|
|
await measurementToken(two.id);
|
|
await measurementToken(two.id);
|
|
const ambiguous = await externalReading(
|
|
two.id,
|
|
new Date("2026-06-02T08:00:00.000Z"),
|
|
);
|
|
|
|
const stamps = await prisma.measurement.findMany({
|
|
select: { id: true, updatedAt: true, syncVersion: true },
|
|
orderBy: { id: "asc" },
|
|
});
|
|
|
|
await runMigration();
|
|
await runMigration();
|
|
|
|
const links = Object.fromEntries(
|
|
(
|
|
await prisma.measurement.findMany({
|
|
select: { id: true, ingestTokenId: true },
|
|
})
|
|
).map((r) => [r.id, r.ingestTokenId]),
|
|
);
|
|
expect(links).toEqual({
|
|
[after.id]: soleToken,
|
|
[before.id]: null,
|
|
[manual.id]: null,
|
|
[ambiguous.id]: null,
|
|
});
|
|
expect(
|
|
await prisma.measurement.findMany({
|
|
select: { id: true, updatedAt: true, syncVersion: true },
|
|
orderBy: { id: "asc" },
|
|
}),
|
|
).toEqual(stamps);
|
|
});
|
|
});
|