From 4e1834c2a57b3909fbf9c7a10ded539f934551de Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 13 Mar 2026 16:20:03 +0100 Subject: [PATCH] add cleanup script --- scripts/cleanup-seed-from-prod.ts | 236 ++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 scripts/cleanup-seed-from-prod.ts diff --git a/scripts/cleanup-seed-from-prod.ts b/scripts/cleanup-seed-from-prod.ts new file mode 100644 index 0000000..2a256ea --- /dev/null +++ b/scripts/cleanup-seed-from-prod.ts @@ -0,0 +1,236 @@ +/** + * Surgically remove seed data from production database. + * Only deletes rows created by seed-staging.ts, leaves real data intact. + * + * Usage: + * DATABASE_URL="postgresql://..." bun scripts/cleanup-seed-from-prod.ts + * DATABASE_URL="postgresql://..." bun scripts/cleanup-seed-from-prod.ts --dry-run + */ + +const DATABASE_URL = process.env.DATABASE_URL; +if (!DATABASE_URL) throw new Error('DATABASE_URL not set'); + +import { SQL as BunSQL } from 'bun:sql'; + +const client = new BunSQL(DATABASE_URL); + +const DRY_RUN = process.argv.includes('--dry-run'); + +async function query(sql: string, params: any[] = []) { + if (DRY_RUN) { + // For dry run, just count affected rows with a SELECT + return []; + } + return client.unsafe(sql, params); +} + +async function count(sql: string, params: any[] = []): Promise { + const rows = await client.unsafe(sql, params); + return Number(rows[0]?.count ?? 0); +} + +async function exec(description: string, sql: string, params: any[] = []) { + // Count first + const countSql = sql + .replace(/^DELETE FROM/, 'SELECT COUNT(*) as count FROM') + .replace(/\s+RETURNING\b.*$/i, ''); + const n = await count(countSql, params); + + if (n === 0) { + console.log(` โญ ${description}: 0 rows (skipped)`); + return 0; + } + + if (DRY_RUN) { + console.log(` ๐Ÿ” ${description}: ${n} rows (dry run, not deleted)`); + return n; + } + + await client.unsafe(sql, params); + console.log(` โœ… ${description}: ${n} rows deleted`); + return n; +} + +async function main() { + console.log(`\n๐Ÿงน Cleaning up seed data from database`); + console.log(` Mode: ${DRY_RUN ? 'DRY RUN (no changes)' : 'โš ๏ธ LIVE โ€” will delete rows'}`); + console.log(` Database: ${DATABASE_URL!.replace(/\/\/.*@/, '//***@')}\n`); + + // Safety check: verify this looks like prod (has non-seed users) + const realUsers = await count( + `SELECT COUNT(*) as count FROM "user" WHERE id NOT LIKE 'seed-user-%'` + ); + const seedUsers = await count( + `SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'` + ); + console.log(` Real users: ${realUsers}`); + console.log(` Seed users: ${seedUsers}\n`); + + if (seedUsers === 0) { + console.log('โœ… No seed data found. Nothing to clean up.'); + process.exit(0); + } + + // โ”€โ”€ Delete in reverse FK dependency order (leaves โ†’ roots) โ”€โ”€ + + // 1. Match game scores โ€” joined through match_game โ†’ match โ†’ created_by + console.log('Phase 1: Match game scores'); + await exec( + 'match_game_score (from seed matches)', + `DELETE FROM match_game_score WHERE match_game_id IN ( + SELECT mg.id FROM match_game mg + JOIN match m ON mg.match_id = m.id + WHERE m.created_by LIKE 'seed-user-%' + )` + ); + + // 2. Match participant players โ€” joined through match_participant โ†’ match + console.log('Phase 2: Match participant players'); + await exec( + 'match_participant_player (from seed matches)', + `DELETE FROM match_participant_player WHERE participant_id IN ( + SELECT mp.id FROM match_participant mp + JOIN match m ON mp.match_id = m.id + WHERE m.created_by LIKE 'seed-user-%' + )` + ); + + // 3. Match games + console.log('Phase 3: Match games'); + await exec( + 'match_game (from seed matches)', + `DELETE FROM match_game WHERE match_id IN ( + SELECT id FROM match WHERE created_by LIKE 'seed-user-%' + )` + ); + + // 4. Match participants + console.log('Phase 4: Match participants'); + await exec( + 'match_participant (from seed matches)', + `DELETE FROM match_participant WHERE match_id IN ( + SELECT id FROM match WHERE created_by LIKE 'seed-user-%' + )` + ); + + // 5. Match invites (reference both match_id and team/mappool) + console.log('Phase 5: Match invites'); + await exec( + 'match_invite (from seed users)', + `DELETE FROM match_invite WHERE created_by LIKE 'seed-user-%'` + ); + + // 6. Notifications + console.log('Phase 6: Notifications'); + await exec( + 'notification (from seed users)', + `DELETE FROM notification WHERE user_id LIKE 'seed-user-%'` + ); + + // 7. Match queue entries (if any) + console.log('Phase 7: Match queue'); + await exec( + 'match_queue (from seed users)', + `DELETE FROM match_queue WHERE user_id LIKE 'seed-user-%'` + ); + + // 8. Matches + console.log('Phase 8: Matches'); + await exec( + 'match (created by seed users)', + `DELETE FROM match WHERE created_by LIKE 'seed-user-%'` + ); + + // 9. Mappool slots (from seed mappools) + console.log('Phase 9: Mappool slots'); + await exec( + 'mappool_slot (from seed mappools)', + `DELETE FROM mappool_slot WHERE mappool_id IN ( + SELECT id FROM mappool WHERE created_by LIKE 'seed-user-%' + )` + ); + + // 10. Mappools + console.log('Phase 10: Mappools'); + await exec( + 'mappool (created by seed users)', + `DELETE FROM mappool WHERE created_by LIKE 'seed-user-%'` + ); + + // 11. Player ratings + console.log('Phase 11: Player ratings'); + await exec( + 'player_rating (from seed users)', + `DELETE FROM player_rating WHERE user_id LIKE 'seed-user-%'` + ); + + // 12. Team members (from seed-owned teams) + console.log('Phase 12: Team members'); + await exec( + 'team_member (from seed teams)', + `DELETE FROM team_member WHERE team_id IN ( + SELECT id FROM team WHERE owner_id LIKE 'seed-user-%' + )` + ); + + // 13. Teams + console.log('Phase 13: Teams'); + await exec( + 'team (owned by seed users)', + `DELETE FROM team WHERE owner_id LIKE 'seed-user-%'` + ); + + // 14. Sessions (cascade from user delete, but clean up explicitly) + console.log('Phase 14: Sessions'); + await exec( + 'session (from seed users)', + `DELETE FROM session WHERE user_id LIKE 'seed-user-%'` + ); + + // 15. Accounts + console.log('Phase 15: Accounts'); + await exec( + 'account (from seed users)', + `DELETE FROM account WHERE user_id LIKE 'seed-user-%'` + ); + + // 16. Users (the root) + console.log('Phase 16: Users'); + await exec( + 'user (seed users)', + `DELETE FROM "user" WHERE id LIKE 'seed-user-%'` + ); + + // โ”€โ”€ Verify โ”€โ”€ + console.log('\nโ”€โ”€ Verification โ”€โ”€'); + const remaining = await count( + `SELECT COUNT(*) as count FROM "user" WHERE id LIKE 'seed-user-%'` + ); + const remainingMatches = await count( + `SELECT COUNT(*) as count FROM match WHERE created_by LIKE 'seed-user-%'` + ); + const remainingPools = await count( + `SELECT COUNT(*) as count FROM mappool WHERE created_by LIKE 'seed-user-%'` + ); + const remainingTeams = await count( + `SELECT COUNT(*) as count FROM team WHERE owner_id LIKE 'seed-user-%'` + ); + + console.log(` Remaining seed users: ${remaining}`); + console.log(` Remaining seed matches: ${remainingMatches}`); + console.log(` Remaining seed mappools: ${remainingPools}`); + console.log(` Remaining seed teams: ${remainingTeams}`); + + if (!DRY_RUN && remaining === 0 && remainingMatches === 0) { + console.log('\nโœ… All seed data cleaned up successfully!'); + } else if (DRY_RUN) { + console.log('\n๐Ÿ” Dry run complete. Re-run without --dry-run to delete.'); + } + + await client.close(); +} + +main().catch((err) => { + console.error('โŒ Cleanup failed:', err); + process.exit(1); +});