word-waltz / files / scripts / cleanup-old-games.js
cleanup-old-games.js
f755bca6 2026-03-23 Pavel Safronov | 1 | #!/usr/bin/env node |
| 2 | /** | |
| 3 | * scripts/cleanup-old-games.js | |
| 4 | * | |
| 5 | * Deletes Firestore game documents whose `lastPlayed` timestamp is older than | |
| 6 | * RETENTION_DAYS days. Safe to run repeatedly (idempotent). | |
| 7 | * | |
| 8 | * `lastPlayed` is the only unencrypted field in each game document, written on | |
| 9 | * every game update. Game content (iv + ciphertext) is never read or logged here. | |
| 10 | * | |
| 11 | * Required environment variables: | |
| 12 | * FIREBASE_SERVICE_ACCOUNT_JSON — full service-account JSON (masked CI/CD variable) | |
82674e8a 2026-03-23 Pavel Safronov | 13 | * |
| 14 | * The Firebase project ID is read directly from src/firebase/config.ts in the repo. | |
f755bca6 2026-03-23 Pavel Safronov | 15 | * |
| 16 | * Schedule via a GitLab pipeline schedule (e.g. daily at 03:00 UTC). | |
| 17 | */ | |
| 18 | ||
82674e8a 2026-03-23 Pavel Safronov | 19 | import { readFileSync } from 'fs'; |
f755bca6 2026-03-23 Pavel Safronov | 20 | import { initializeApp, cert } from 'firebase-admin/app'; |
| 21 | import { getFirestore } from 'firebase-admin/firestore'; | |
| 22 | ||
| 23 | const RETENTION_DAYS = 30; | |
| 24 | const BATCH_SIZE = 500; // Firestore max batch-write size | |
| 25 | ||
| 26 | function requireEnv(name) { | |
a0df624b 2026-03-23 Pavel Safronov | 27 | const value = process.env[name]; |
| 28 | if (!value) { | |
| 29 | console.error(`Error: required environment variable "${name}" is not set.`); | |
| 30 | process.exit(1); | |
| 31 | } | |
| 32 | return value; | |
f755bca6 2026-03-23 Pavel Safronov | 33 | } |
| 34 | ||
82674e8a 2026-03-23 Pavel Safronov | 35 | // Extract projectId from the committed Firebase config (avoids a separate CI variable). |
| 36 | const configSource = readFileSync(new URL('../src/firebase/config.ts', import.meta.url), 'utf8'); | |
| 37 | const projectIdMatch = configSource.match(/projectId:\s*["']([^"']+)["']/); | |
| 38 | if (!projectIdMatch) { | |
| 39 | console.error('Error: could not extract projectId from src/firebase/config.ts'); | |
| 40 | process.exit(1); | |
| 41 | } | |
| 42 | const projectId = projectIdMatch[1]; | |
| 43 | ||
f755bca6 2026-03-23 Pavel Safronov | 44 | const serviceAccount = JSON.parse(requireEnv('FIREBASE_SERVICE_ACCOUNT_JSON')); |
| 45 | ||
| 46 | initializeApp({ credential: cert(serviceAccount), projectId }); | |
| 47 | const db = getFirestore(); | |
| 48 | ||
| 49 | const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000; | |
| 50 | ||
| 51 | async function deleteOldGames() { | |
a0df624b 2026-03-23 Pavel Safronov | 52 | console.log( |
| 53 | `Querying for games with lastPlayed < ${new Date(cutoffMs).toISOString()} ` + | |
| 54 | `(older than ${RETENTION_DAYS} days)…` | |
| 55 | ); | |
f755bca6 2026-03-23 Pavel Safronov | 56 | |
a0df624b 2026-03-23 Pavel Safronov | 57 | const snapshot = await db |
| 58 | .collection('games') | |
| 59 | .where('lastPlayed', '<', cutoffMs) | |
| 60 | .get(); | |
f755bca6 2026-03-23 Pavel Safronov | 61 | |
a0df624b 2026-03-23 Pavel Safronov | 62 | if (snapshot.empty) { |
| 63 | console.log('No stale games found. Nothing to delete.'); | |
| 64 | return; | |
| 65 | } | |
f755bca6 2026-03-23 Pavel Safronov | 66 | |
a0df624b 2026-03-23 Pavel Safronov | 67 | console.log(`Found ${snapshot.size} stale game(s). Deleting in batches…`); |
f755bca6 2026-03-23 Pavel Safronov | 68 | |
a0df624b 2026-03-23 Pavel Safronov | 69 | const docs = snapshot.docs; |
| 70 | for (let i = 0; i < docs.length; i += BATCH_SIZE) { | |
| 71 | const batch = db.batch(); | |
| 72 | docs.slice(i, i + BATCH_SIZE).forEach((doc) => batch.delete(doc.ref)); | |
| 73 | await batch.commit(); | |
| 74 | console.log(` Deleted ${Math.min(i + BATCH_SIZE, docs.length)} / ${docs.length}`); | |
| 75 | } | |
f755bca6 2026-03-23 Pavel Safronov | 76 | |
a0df624b 2026-03-23 Pavel Safronov | 77 | console.log('Cleanup complete.'); |
f755bca6 2026-03-23 Pavel Safronov | 78 | } |
| 79 | ||
| 80 | deleteOldGames().catch((err) => { | |
a0df624b 2026-03-23 Pavel Safronov | 81 | console.error('Cleanup failed:', err); |
| 82 | process.exit(1); | |
f755bca6 2026-03-23 Pavel Safronov | 83 | }); |