fuzzyworld

word-waltz / files / scripts / cleanup-old-games.js

cleanup-old-games.js

2.7 KB · history · blame

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)
13 *
14 * The Firebase project ID is read directly from src/firebase/config.ts in the repo.
15 *
16 * Schedule via a GitLab pipeline schedule (e.g. daily at 03:00 UTC).
17 */
18
19import { readFileSync } from 'fs';
20import { initializeApp, cert } from 'firebase-admin/app';
21import { getFirestore } from 'firebase-admin/firestore';
22
23const RETENTION_DAYS = 30;
24const BATCH_SIZE = 500; // Firestore max batch-write size
25
26function requireEnv(name) {
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;
33}
34
35// Extract projectId from the committed Firebase config (avoids a separate CI variable).
36const configSource = readFileSync(new URL('../src/firebase/config.ts', import.meta.url), 'utf8');
37const projectIdMatch = configSource.match(/projectId:\s*["']([^"']+)["']/);
38if (!projectIdMatch) {
39 console.error('Error: could not extract projectId from src/firebase/config.ts');
40 process.exit(1);
41}
42const projectId = projectIdMatch[1];
43
44const serviceAccount = JSON.parse(requireEnv('FIREBASE_SERVICE_ACCOUNT_JSON'));
45
46initializeApp({ credential: cert(serviceAccount), projectId });
47const db = getFirestore();
48
49const cutoffMs = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000;
50
51async function deleteOldGames() {
52 console.log(
53 `Querying for games with lastPlayed < ${new Date(cutoffMs).toISOString()} ` +
54 `(older than ${RETENTION_DAYS} days)…`
55 );
56
57 const snapshot = await db
58 .collection('games')
59 .where('lastPlayed', '<', cutoffMs)
60 .get();
61
62 if (snapshot.empty) {
63 console.log('No stale games found. Nothing to delete.');
64 return;
65 }
66
67 console.log(`Found ${snapshot.size} stale game(s). Deleting in batches…`);
68
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 }
76
77 console.log('Cleanup complete.');
78}
79
80deleteOldGames().catch((err) => {
81 console.error('Cleanup failed:', err);
82 process.exit(1);
83});