import { mkdir, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { BSON, MongoClient, type Db } from 'mongodb'; import { LEGACY_EVENT_TYPES } from '../constants/legacy-data.js'; import { loadServerConfig } from '../src/server/config.js'; // Run while the old app is stopped. Never called by server startup. export async function cleanupLegacyData(db: Db, backupPath?: string) { const matchesExist = (await db.listCollections({ name: 'matches' }).toArray()).length > 0; const matches = await db.collection('matches').find().toArray(); const events = await db.collection('analytics_events').find({ source: { $ne: 'client' }, eventType: { $in: LEGACY_EVENT_TYPES } }).toArray(); const samples = await db.collection('analytics_presence_samples').find({ source: { $exists: false } }).toArray(); const players = await db.collection('players').find({ firstMatchAt: { $exists: true } }, { projection: { _id: 1, firstMatchAt: 1 } }).toArray(); const counts = { matches: matches.length, events: events.length, samples: samples.length, playerFields: players.length }; if (!backupPath) return { applied: false, database: db.databaseName, counts }; // Persist BSON types and indexes before the first mutation. Refuse overwriting a backup. const backup = { database: db.databaseName, createdAt: new Date(), matches, events, samples, players, matchIndexes: matchesExist ? await db.collection('matches').indexes() : [] }; await mkdir(dirname(backupPath), { recursive: true }); await writeFile(backupPath, BSON.EJSON.stringify(backup, { relaxed: false }), { flag: 'wx' }); if (matchesExist) { if (await db.collection('matches').countDocuments() !== matches.length) throw new Error('MATCHES_CHANGED_STOP_OLD_SERVER'); await db.collection('matches').drop(); } if (events.length) await db.collection('analytics_events').deleteMany({ _id: { $in: events.map(e => e._id) }, source: { $ne: 'client' }, eventType: { $in: LEGACY_EVENT_TYPES } }); if (samples.length) await db.collection('analytics_presence_samples').deleteMany({ _id: { $in: samples.map(s => s._id) }, source: { $exists: false } }); if (players.length) await db.collection('players').updateMany({ _id: { $in: players.map(p => p._id) } }, { $unset: { firstMatchAt: '' } }); return { applied: true, database: db.databaseName, counts, backupPath }; } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { const args = process.argv.slice(2), apply = args.includes('--apply'); const database = args[args.indexOf('--database') + 1]; const config = loadServerConfig(); const configured = config.MONGODB_DATABASE; if (!args.includes('--database') || database !== configured || ['admin', 'config', 'local'].includes(database)) throw new Error('Specify --database matching MONGODB_DATABASE'); const backup = args.includes('--backup') ? args[args.indexOf('--backup') + 1] : undefined; if (apply && (!backup || backup.startsWith('--'))) throw new Error('--apply requires --backup '); const client = new MongoClient(config.MONGODB_URI, { serverSelectionTimeoutMS: 5000 }); try { await client.connect(); console.log(JSON.stringify(await cleanupLegacyData(client.db(database), apply ? resolve(backup!) : undefined), null, 2)); } finally { await client.close(); } }