Files
HealthLog/scripts/backfill-rollups.ts
T
Marc-André Bombeck 29bfcc674b refactor(rollups): move measurement-rollups under src/lib/rollups umbrella
Creates the new src/lib/rollups/ aggregator directory and relocates
the main measurement rollup module:

- src/lib/measurements/rollups.ts → src/lib/rollups/measurement-rollups.ts
- src/lib/measurements/__tests__/rollups.test.ts →
  src/lib/rollups/__tests__/measurement-rollups.test.ts

Every importer of @/lib/measurements/rollups is rewritten to
@/lib/rollups/measurement-rollups. The orphan sibling reference inside
src/lib/measurements/rollup-read.ts (still in its old home until the
next batch) is promoted to the @/lib alias so the chain keeps
typechecking through the in-flight move.

Pure relocation — no behaviour change, no exported-surface change. The
umbrella exists so the rollup contract is discoverable from a single
directory before the iOS sprint locks the boundary.
2026-05-21 12:19:45 +02:00

103 lines
2.8 KiB
TypeScript

/**
* v1.5.0 — one-shot backfill for `measurement_rollups`.
*
* Walks every user and folds their measurement history into the
* persistent rollup table across all four granularities (DAY, WEEK,
* MONTH, YEAR). Bounded — one user at a time, no parallel users — so
* the script never crowds the Prisma pool. Idempotent — re-running it
* upserts the same rows.
*
* Usage
* -----
* pnpm tsx scripts/backfill-rollups.ts
* pnpm tsx scripts/backfill-rollups.ts --user <userId>
*
* The `--user` flag targets a single user; without it the script
* iterates every account in the database.
*/
import { prisma } from "@/lib/db";
import {
ALL_GRANULARITIES,
recomputeUserRollups,
} from "@/lib/rollups/measurement-rollups";
interface CliOptions {
userId: string | null;
}
function parseArgs(argv: string[]): CliOptions {
const opts: CliOptions = { userId: null };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === "--user" && argv[i + 1]) {
opts.userId = argv[i + 1];
i += 1;
}
}
return opts;
}
async function main(): Promise<void> {
const opts = parseArgs(process.argv.slice(2));
const where = opts.userId ? { id: opts.userId } : {};
const users = await prisma.user.findMany({
where,
select: { id: true, username: true },
orderBy: { createdAt: "asc" },
});
if (users.length === 0) {
console.log(
opts.userId
? `No user matched id=${opts.userId}`
: "No users in database — nothing to backfill",
);
return;
}
console.log(
`[backfill-rollups] Folding ${users.length} user(s) across ` +
`${ALL_GRANULARITIES.join(", ")} granularities`,
);
let totalRowsUpserted = 0;
const totalStarted = Date.now();
for (const [index, user] of users.entries()) {
const userStarted = Date.now();
try {
const { rowsUpserted, durationMs } = await recomputeUserRollups(
user.id,
{
granularities: ALL_GRANULARITIES,
},
);
totalRowsUpserted += rowsUpserted;
console.log(
`[${index + 1}/${users.length}] user=${user.username} ` +
`(${user.id}) rows=${rowsUpserted} duration=${durationMs}ms`,
);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(
`[${index + 1}/${users.length}] user=${user.username} ` +
`(${user.id}) FAILED after ${Date.now() - userStarted}ms: ${message}`,
);
}
}
console.log(
`[backfill-rollups] DONE — ${totalRowsUpserted} rows upserted in ` +
`${Date.now() - totalStarted}ms across ${users.length} user(s)`,
);
}
main()
.catch((err) => {
console.error("[backfill-rollups] fatal error:", err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});