From 9aec3b9d720a1996941cf714b242f5be757aa8d3 Mon Sep 17 00:00:00 2001 From: Eric Zhang Date: Sun, 22 Dec 2024 16:17:54 -0500 Subject: [PATCH] Fix up scripts with batched iterator, add toggleAdmin (#170) --- scripts/src/calcStats.js | 14 ++++----- scripts/src/fixGames.js | 10 +++--- scripts/src/index.js | 20 ++++++++---- scripts/src/listUsers.js | 21 ------------- scripts/src/sanitizeNames.js | 4 +-- scripts/src/users.js | 61 ++++++++++++++++++++++++++++++++++++ scripts/src/utils.js | 37 ++++++++++++++++++++++ 7 files changed, 124 insertions(+), 43 deletions(-) delete mode 100644 scripts/src/listUsers.js create mode 100644 scripts/src/users.js create mode 100644 scripts/src/utils.js diff --git a/scripts/src/calcStats.js b/scripts/src/calcStats.js index eb41a02..29340c3 100644 --- a/scripts/src/calcStats.js +++ b/scripts/src/calcStats.js @@ -1,4 +1,4 @@ -import admin from "firebase-admin"; +import { getDatabase } from "firebase-admin/database"; const batchSize = 1000; @@ -14,7 +14,7 @@ const batchSize = 1000; */ export async function calcStats() { console.log("Loading users..."); - const users = await admin.database().ref("users").orderByKey().get(); + const users = await getDatabase().ref("users").orderByKey().get(); const userIds = Object.keys(users.val()); console.log("Done loading users!"); @@ -24,8 +24,7 @@ export async function calcStats() { await Promise.all( userIds.slice(i, i + batchSize).map(async (userId) => { - const games = await admin - .database() + const games = await getDatabase() .ref(`userGames/${userId}`) .once("value"); @@ -45,14 +44,13 @@ export async function calcStats() { await Promise.all( Object.keys(games.val()).map(async (gameId) => { - const game = await admin.database().ref(`games/${gameId}`).get(); + const game = await getDatabase().ref(`games/${gameId}`).get(); if (game.child("status").val() !== "done") { if (process.env.VERBOSE) console.log(`[${userId}] Skipping ongoing game ${gameId}`); return; } - const gameData = await admin - .database() + const gameData = await getDatabase() .ref(`gameData/${gameId}`) .get(); @@ -116,7 +114,7 @@ export async function calcStats() { updates[`${mode}/${variant}`] = stats[mode][variant]; } } - await admin.database().ref(`userStats/${userId}`).update(updates); + await getDatabase().ref(`userStats/${userId}`).update(updates); }), ); diff --git a/scripts/src/fixGames.js b/scripts/src/fixGames.js index ffcd498..54826d6 100644 --- a/scripts/src/fixGames.js +++ b/scripts/src/fixGames.js @@ -1,10 +1,9 @@ import assert from "assert"; -import admin from "firebase-admin"; +import { getDatabase } from "firebase-admin/database"; /** Fix games that have `endedAt === 0` due to a site migration bug in v3.0.0. */ export async function fixGames() { - const badGames = await admin - .database() + const badGames = await getDatabase() .ref("games") .orderByChild("endedAt") .equalTo(0) @@ -14,8 +13,7 @@ export async function fixGames() { console.log(`Fixing game ${gameId}...`); console.log({ [gameId]: game }); assert.strictEqual(game.status, "done"); - const events = await admin - .database() + const events = await getDatabase() .ref(`gameData/${gameId}/events`) .once("value"); let lastTime = 0; @@ -23,7 +21,7 @@ export async function fixGames() { lastTime = event.val().time; }); console.log(`Setting endedAt = ${lastTime}...`); - await admin.database().ref(`games/${gameId}/endedAt`).set(lastTime); + await getDatabase().ref(`games/${gameId}/endedAt`).set(lastTime); console.log("Done.\n"); } diff --git a/scripts/src/index.js b/scripts/src/index.js index 04cd940..05fbcb8 100644 --- a/scripts/src/index.js +++ b/scripts/src/index.js @@ -1,16 +1,24 @@ -import admin from "firebase-admin"; +import { cert, initializeApp } from "firebase-admin/app"; import inquirer from "inquirer"; +import process from "node:process"; import { calcStats } from "./calcStats.js"; import { fixGames } from "./fixGames.js"; -import { listAdmins, listPatrons } from "./listUsers.js"; import { sanitizeNames } from "./sanitizeNames.js"; +import { listAdmins, listPatrons, toggleAdmin } from "./users.js"; // Add scripts as functions to this array -const scripts = [listAdmins, listPatrons, sanitizeNames, fixGames, calcStats]; +const scripts = [ + listAdmins, + listPatrons, + toggleAdmin, + sanitizeNames, + fixGames, + calcStats, +]; -admin.initializeApp({ - credential: admin.credential.cert("./credential.json"), +initializeApp({ + credential: cert("./credential.json"), databaseURL: "https://setwithfriends.firebaseio.com", }); @@ -30,4 +38,4 @@ async function main() { } } -main().then(() => require("process").exit()); +main().then(() => process.exit()); diff --git a/scripts/src/listUsers.js b/scripts/src/listUsers.js deleted file mode 100644 index 84090ba..0000000 --- a/scripts/src/listUsers.js +++ /dev/null @@ -1,21 +0,0 @@ -import admin from "firebase-admin"; - -export async function listAdmins() { - const admins = await admin - .database() - .ref("users") - .orderByChild("admin") - .equalTo(true) - .once("value"); - return admins.val(); -} - -export async function listPatrons() { - const patrons = await admin - .database() - .ref("users") - .orderByChild("patron") - .startAt(true) - .once("value"); - return patrons.val(); -} diff --git a/scripts/src/sanitizeNames.js b/scripts/src/sanitizeNames.js index 87ee89a..88b623b 100644 --- a/scripts/src/sanitizeNames.js +++ b/scripts/src/sanitizeNames.js @@ -1,4 +1,4 @@ -import admin from "firebase-admin"; +import { getDatabase } from "firebase-admin/database"; function sanitize(name) { return ( @@ -9,7 +9,7 @@ function sanitize(name) { } export async function sanitizeNames() { - const users = await admin.database().ref("users").orderByKey().once("value"); + const users = await getDatabase().ref("users").orderByKey().once("value"); const updates = []; diff --git a/scripts/src/users.js b/scripts/src/users.js new file mode 100644 index 0000000..5a63c7d --- /dev/null +++ b/scripts/src/users.js @@ -0,0 +1,61 @@ +import { getDatabase } from "firebase-admin/database"; +import inquirer from "inquirer"; + +import { databaseIterator } from "./utils.js"; + +function displayUser(user) { + const name = user.child("name").val(); + const lastOnline = new Date( + user.child("lastOnline").val(), + ).toLocaleDateString(); + return `${name} (last online: ${lastOnline})`; +} + +export async function listAdmins() { + for await (const [userId, user] of databaseIterator("users")) { + if (user.child("admin").val()) { + console.log(userId, displayUser(user)); + } + } +} + +export async function listPatrons() { + for await (const [userId, user] of databaseIterator("users")) { + if (user.child("patron").val()) { + console.log(userId, displayUser(user)); + } + } +} + +export async function toggleAdmin() { + const { userId } = await inquirer.prompt([ + { type: "input", name: "userId", message: "Enter the user ID:" }, + ]); + + const user = await getDatabase().ref(`users/${userId}`).get(); + console.log(displayUser(user)); + + if (user.child("admin").val()) { + const { confirm } = await inquirer.prompt([ + { + type: "confirm", + name: "confirm", + message: "User is admin, do you want to remove admin status?", + }, + ]); + if (confirm) { + await getDatabase().ref(`users/${userId}/admin`).remove(); + } + } else { + const { confirm } = await inquirer.prompt([ + { + type: "confirm", + name: "confirm", + message: "User is not admin, do you want to grant admin status?", + }, + ]); + if (confirm) { + await getDatabase().ref(`users/${userId}/admin`).set(true); + } + } +} diff --git a/scripts/src/utils.js b/scripts/src/utils.js new file mode 100644 index 0000000..5f0507a --- /dev/null +++ b/scripts/src/utils.js @@ -0,0 +1,37 @@ +import { getDatabase } from "firebase-admin/database"; + +/** + * Iterate through a reference in the database, returning the children ordered + * by key in batches. + * + * @param {string} path The path to the reference to iterate through. + * @param {number} batchSize The number of children to fetch in each batch. + * @returns {AsyncGenerator<[string, import("firebase-admin/database").DataSnapshot]>} + */ +export async function* databaseIterator(path, batchSize = 1000) { + let lastKey = null; + while (true) { + const snap = lastKey + ? await getDatabase() + .ref(path) + .orderByKey() + .startAfter(lastKey) + .limitToFirst(batchSize) + .get() + : await getDatabase() + .ref(path) + .orderByKey() + .limitToFirst(batchSize) + .get(); + if (!snap.exists()) return; + + const childKeys = []; + snap.forEach((child) => { + childKeys.push(child.key); + lastKey = child.key; + }); + for (const key of childKeys) { + yield [key, snap.child(key)]; + } + } +}