From 7bc14d7bbc230aa49d4d7307548e1753049f5622 Mon Sep 17 00:00:00 2001 From: Vezono Date: Thu, 1 Jan 2026 15:12:52 +0100 Subject: [PATCH] init --- .gitignore | 4 ++++ package.json | 22 ++++++++++++++++++++++ src/firebase.ts | 12 ++++++++++++ src/index.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/mongo.ts | 14 ++++++++++++++ src/sync.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/syncReverse.ts | 24 ++++++++++++++++++++++++ tsconfig.json | 14 ++++++++++++++ 8 files changed, 169 insertions(+) create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/firebase.ts create mode 100644 src/index.ts create mode 100644 src/mongo.ts create mode 100644 src/sync.ts create mode 100644 src/syncReverse.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c6bf63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +database-key.json +dist +node_modules diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b9123f --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "mongo-firebase-backuper", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@types/node": "^25.0.3", + "dotenv": "^17.2.3", + "firebase-admin": "^13.6.0", + "mongodb": "^7.0.0", + "node-cron": "^4.2.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/src/firebase.ts b/src/firebase.ts new file mode 100644 index 0000000..d11c40d --- /dev/null +++ b/src/firebase.ts @@ -0,0 +1,12 @@ +import admin from "firebase-admin"; +import serviceAccount from "../database-key.json"; + +admin.initializeApp({ + credential: admin.credential.cert( + serviceAccount as admin.ServiceAccount + ), + databaseURL: "https://pixel-battlegrounds-6c5a7-default-rtdb.firebaseio.com" +}); + +export const rtdb = admin.database(); + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..52b9420 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +import "dotenv/config"; +import cron from "node-cron"; +import { syncPlayers } from "./sync"; +import { syncMongoToFirebase } from "./syncReverse"; // new function + +async function main() { + const args = process.argv.slice(2); + + if (args.includes("--one-time")) { + console.log("🔹 Running one-time Mongo → Firebase sync..."); + try { + await syncMongoToFirebase(); + console.log("✅ One-time sync finished"); + } catch (e) { + console.error("One-time sync error:", e); + } + process.exit(0); // exit after one-time run + } + + // Default: cron + immediate run + console.log("Firebase → Mongo sync running (every 15 minutes)"); + + // Run immediately + try { + await syncPlayers(); + } catch (e) { + console.error("Players sync error:", e); + } + + // Schedule every 15 minutes + cron.schedule("*/15 * * * *", async () => { + try { + await syncPlayers(); + } catch (e) { + console.error("Players sync error:", e); + } + }); +} + +main(); + diff --git a/src/mongo.ts b/src/mongo.ts new file mode 100644 index 0000000..5af676d --- /dev/null +++ b/src/mongo.ts @@ -0,0 +1,14 @@ +import { MongoClient, Db } from "mongodb"; + +const uri = process.env.MONGO_URI!; +const client = new MongoClient(uri); +let db: Db | null = null; + +export async function connectMongo(): Promise { + if (db) return db; + + await client.connect(); + db = client.db(); // database name is taken from URI + return db; +} + diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 0000000..b3bfe2a --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,38 @@ +import { rtdb } from "./firebase"; +import { connectMongo } from "./mongo"; +import { Document } from "mongodb"; + +type PlayerDoc = Document & { _id: number }; + +export async function syncPlayers() { + console.log("Players sync started:", new Date().toISOString()); + + const db = await connectMongo(); + const collection = db.collection("players"); + + // 🔹 RTDB path + const snapshot = await rtdb.ref("players").once("value"); + + const playersDict = snapshot.val(); + + if (!playersDict) { + console.log("No players data found"); + return; + } + + const bulkOps = Object.entries(playersDict).map(([id, data]) => ({ + replaceOne: { + filter: { _id: Number(id) }, + replacement: { + _id: Number(id), + ...(data as object) + }, + upsert: true + } + })); + + await collection.bulkWrite(bulkOps, { ordered: false }); + + console.log("Players synced:", bulkOps.length); +} + diff --git a/src/syncReverse.ts b/src/syncReverse.ts new file mode 100644 index 0000000..4002e71 --- /dev/null +++ b/src/syncReverse.ts @@ -0,0 +1,24 @@ +import { rtdb } from "./firebase"; +import { connectMongo } from "./mongo"; + +export async function syncMongoToFirebase() { + console.log("Mongo → Firebase sync started:", new Date().toISOString()); + + const db = await connectMongo(); + const collection = db.collection("players"); + + const allPlayers = await collection.find().toArray(); + + // Convert to dictionary: { numericId: {...playerFields} } + const playersDict: Record = {}; + for (const player of allPlayers) { + const { _id, ...fields } = player; + playersDict[_id.toString()] = fields; + } + + // Push to Firebase Realtime Database + await rtdb.ref("players").set(playersDict); + + console.log("Mongo → Firebase sync finished:", allPlayers.length, "players"); +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c3e79c2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + + "resolveJsonModule": true, + "moduleResolution": "node" + } +} +