mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
The title encryption backfill moved each Coach conversation's title into ciphertext with a plain Prisma update, which stamps updatedAt. The Coach panel orders and groups conversations by that column, so threads untouched for months showed up under Today. The memory summary refresh did the same in the background. Storage-only rewrites now carry the row's own updatedAt and match on it, so a turn that lands meanwhile wins and the row is retried on the next pass: conversation titles, practitioner contacts, measurement and mood notes (where updatedAt is the sync cursor), inventory notes, and the mood-note script. The summary refresh writes through raw SQL. A guard freezes both rules: every backfill write on a model with @updatedAt names the column or is listed with the reason the stamp is wanted, and every conversation write outside the person's own actions keeps it.
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* scripts/backfill-mood-note-column.ts
|
|
*
|
|
* One-shot backfill that lifts existing `tags: ["note:<text>"]`
|
|
* entries out of `mood_entries.tags` into the new `mood_entries.note`
|
|
* column. Operator runs this once after the v1.4.30 schema migration
|
|
* applies.
|
|
*
|
|
* Strategy:
|
|
* 1. SELECT every MoodEntry whose `tags` JSON contains an entry
|
|
* that begins with `note:`.
|
|
* 2. Decode the JSON array, pull the first `note:<text>` entry as
|
|
* the new note value, drop the entry from the array.
|
|
* 3. UPDATE the row in a transaction: `note = <decoded text>`,
|
|
* `tags = <slimmed JSON or null if empty>`.
|
|
*
|
|
* Idempotent — a row whose `tags` no longer carries any `note:`
|
|
* entries is left alone. Re-running on a clean dataset is a no-op.
|
|
*
|
|
* Usage:
|
|
* pnpm tsx scripts/backfill-mood-note-column.ts # dry-run
|
|
* pnpm tsx scripts/backfill-mood-note-column.ts --confirm # commit
|
|
*/
|
|
import "dotenv/config";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { extractNoteAndSlimTags } from "@/lib/mood/note-backfill";
|
|
|
|
interface ParsedArgs {
|
|
confirm: boolean;
|
|
}
|
|
|
|
function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
const out: ParsedArgs = { confirm: false };
|
|
for (const arg of argv) {
|
|
if (arg === "--confirm") out.confirm = true;
|
|
else if (arg === "--help" || arg === "-h") {
|
|
console.log(
|
|
"Usage: pnpm tsx scripts/backfill-mood-note-column.ts [--confirm]",
|
|
);
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`Unknown argument: ${arg}`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
interface RowSnapshot {
|
|
id: string;
|
|
tags: string | null;
|
|
note: string | null;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const dryRun = !args.confirm;
|
|
|
|
if (dryRun) {
|
|
console.log(
|
|
"[backfill-note] DRY RUN — no rows will be written. Pass --confirm to commit.",
|
|
);
|
|
} else {
|
|
console.log("[backfill-note] COMMIT MODE — updating rows in place.");
|
|
}
|
|
|
|
// Naive scan: filter in-memory rather than via JSON predicates so
|
|
// the query stays portable across Postgres minor versions. The
|
|
// mood_entries table is small (per-user lifetime budget).
|
|
const candidates: RowSnapshot[] = await prisma.moodEntry.findMany({
|
|
where: {
|
|
tags: { not: null },
|
|
note: null,
|
|
},
|
|
select: { id: true, tags: true, note: true, updatedAt: true },
|
|
});
|
|
|
|
console.log(`[backfill-note] scanned ${candidates.length} candidate rows`);
|
|
|
|
let updated = 0;
|
|
for (const row of candidates) {
|
|
if (!row.tags) continue;
|
|
const { note, newTags } = extractNoteAndSlimTags(row.tags);
|
|
if (note === null) continue;
|
|
|
|
updated += 1;
|
|
if (!dryRun) {
|
|
// The entry did not change for its reader; it keeps its `updatedAt`
|
|
// (the sync cursor) rather than taking the script's run time.
|
|
await prisma.moodEntry.update({
|
|
where: { id: row.id },
|
|
data: { note, tags: newTags, updatedAt: row.updatedAt },
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`[backfill-note] ${updated} row${updated === 1 ? "" : "s"} ${dryRun ? "would be" : "were"} updated`,
|
|
);
|
|
await prisma.$disconnect();
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
console.error("[backfill-note] failed:", err);
|
|
process.exit(1);
|
|
});
|