mmday-firebase/scripts/backfill-user-votes-by-date.ts
윤정민 90d6895de4 Pin script RTDB region and gate empty-source backfill marker
- 스크립트 부트스트랩의 databaseURL 기본값에 리전 포함 — 기존 `...firebaseio.com`은 SDK가 "Database lives in a different region" 경고만 내고 다른 리전의 없는 DB를 보게 해서, 조회가 조용히 빈 결과를 돌려주고 실행이 멈췄다. 지금까지 스크립트가 RTDB를 안 써서 드러나지 않던 문제다. FIREBASE_DATABASE_REGION으로 덮어쓸 수 있다
- 백필 스크립트가 접속한 database URL을 먼저 출력하도록 변경
- 소스가 비었을 때 완료 마커를 자동으로 남기지 않고 --mark-empty를 요구하도록 변경 — 빈 조회는 "정말 투표가 없다"와 "엉뚱한 DB에 붙었다"를 구분하지 못하는데, 후자에서 마커가 켜지면 이후 dailyArchive가 인덱스만 믿고 전 유저를 건너뛴다
2026-07-27 13:39:10 +09:00

102 lines
3.7 KiB
TypeScript

/**
* `/userVotes/{uid}/{date}/{gameId}` → `/userVotesByDate/{date}/{uid}/{gameId}` 백필.
*
* `dailyArchive`는 날짜별 인덱스만 읽는다(원래는 하루치를 위해 `/userVotes` 트리
* 전체를 내려받았다). 인덱스 도입 이전에 기록된 투표는 이 스크립트로 옮겨야
* 잡의 레거시 폴백 경로를 타지 않는다.
*
* 멱등하다 — 여러 번 돌려도 같은 값으로 덮어쓸 뿐이다.
*
* 실행:
* npx tsx scripts/backfill-user-votes-by-date.ts # 미리보기
* npx tsx scripts/backfill-user-votes-by-date.ts --apply # 실제 쓰기
* npx tsx scripts/backfill-user-votes-by-date.ts --apply --mark-empty # 소스가 빈 상태에서 마커만
*
* ⚠️ 소스(/userVotes)가 비었을 때는 마커를 자동으로 남기지 않는다. 빈 조회는
* "정말 투표가 없다"와 "엉뚱한 DB에 붙었다"를 구분하지 못하는데, 후자에서 마커가
* 켜지면 이후 아카이브가 인덱스만 믿고 전 유저를 건너뛴다. 비었다는 것을 사람이
* 확인했을 때만 --mark-empty로 명시한다.
*/
import "./_bootstrap";
import { rtdb } from "../src/firebase";
type VoteEntry = { team: string; result?: boolean; cancelled?: boolean };
type ByUid = Record<string, Record<string, Record<string, VoteEntry>>>;
/** RTDB update는 한 번에 너무 많은 경로를 담으면 실패하므로 나눠 커밋한다. */
const CHUNK = 500;
async function main(): Promise<void> {
const apply = process.argv.includes("--apply");
const markEmpty = process.argv.includes("--mark-empty");
console.log(`database: ${rtdb.app.options.databaseURL}`);
const snap = await rtdb.ref("/userVotes").get();
if (!snap.exists()) {
console.log("no /userVotes data — nothing to backfill");
if (!apply) return;
if (!markEmpty) {
console.log(
"마커를 남기지 않았다. 위 database URL이 맞고 정말 투표가 없다면 --mark-empty를 붙여 다시 실행할 것."
);
return;
}
await markBackfilled();
return;
}
const byUid = snap.val() as ByUid;
const updates: Record<string, unknown> = {};
let uidCount = 0;
let entryCount = 0;
const dates = new Set<string>();
for (const uid of Object.keys(byUid)) {
const byDate = byUid[uid] ?? {};
let touched = false;
for (const date of Object.keys(byDate)) {
const games = byDate[date] ?? {};
for (const gameId of Object.keys(games)) {
updates[`/userVotesByDate/${date}/${uid}/${gameId}`] = games[gameId];
entryCount += 1;
dates.add(date);
touched = true;
}
}
if (touched) uidCount += 1;
}
const paths = Object.keys(updates);
console.log(
`${entryCount} entries / ${uidCount} uids / ${dates.size} dates` +
(apply ? "" : " (dry run — pass --apply to write)")
);
if (!apply) return;
for (let i = 0; i < paths.length; i += CHUNK) {
const slice: Record<string, unknown> = {};
for (const p of paths.slice(i, i + CHUNK)) slice[p] = updates[p];
await rtdb.ref().update(slice);
console.log(` written ${Math.min(i + CHUNK, paths.length)}/${paths.length}`);
}
await markBackfilled();
console.log("backfill complete");
}
/**
* 완료 표시. 이 값이 있으면 `dailyArchive`는 인덱스가 비어 있어도
* "그날 투표가 없었다"로 해석하고 레거시 전체 스캔 폴백을 쓰지 않는다.
*/
async function markBackfilled(): Promise<void> {
await rtdb.ref("/userVotesByDateMeta/backfilledAt").set(new Date().toISOString());
console.log("marked /userVotesByDateMeta/backfilledAt");
}
main()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});