const config = require('../../config/database') // Sicheres Laden des MongoDB-Treibers — wenn das Paket nicht installiert ist, // wird der Fehler erst in createMongoStore() mit einer klaren Meldung geworfen. let MongoClient try { ;({ MongoClient } = require('mongodb')) } catch (error) { MongoClient = null } // Collection-Namen als Konstanten — wenn sich ein Name ändert, muss er nur hier angepasst werden. const collectionNames = { users: 'users', messages: 'messages', } // MongoDB speichert den Primärschlüssel intern als "_id". // Diese Funktion übersetzt das in "id" damit der Rest der App nichts von MongoDB-Interna wissen muss. function normalizeUser(doc) { if (!doc) return null const { _id, username, room, isOnline, joinedAt, lastActiveAt, } = doc return { id: _id, username, room, isOnline, joinedAt, lastActiveAt, } } // Gleiche Logik wie normalizeUser — username wird redundant in der Nachricht gespeichert // damit beim Laden keine extra DB-Abfrage für den Usernamen nötig ist. function normalizeMessage(doc) { if (!doc) return null const { _id, room, userId, username, text, createdAt, } = doc return { id: _id, room, userId, username, text, createdAt, } } // Kompatibilitätsfunktion für ältere MongoDB-Treiber (< 4.0). // findOneAndUpdate gibt dort das Ergebnis in einem "value"-Wrapper zurück — ab 4.0 direkt. function extractDocument(result) { if (!result) return null return Object.prototype.hasOwnProperty.call(result, 'value') ? result.value : result } // async damit der Start der App nicht blockiert während MongoDB-Verbindung und Indizes aufgebaut werden. async function createMongoStore() { if (!MongoClient) { const message = 'MongoDB driver is not installed. Run `npm install mongodb` in Backend/.' const error = new Error(message) error.code = 'MODULE_NOT_INSTALLED' throw error } const client = new MongoClient(config.uri, { ...config.options, appName: 'chat-app', // Wird in MongoDB Atlas angezeigt um zu sehen welche App verbunden ist. }) await client.connect() const db = client.db(config.dbName) // Entspricht "USE chatapp" in SQL. const users = db.collection(collectionNames.users) const messages = db.collection(collectionNames.messages) // Indizes werden einmalig beim Start angelegt — ohne Index durchsucht MongoDB jede Abfrage // alle Dokumente (Full Collection Scan). Promise.all legt beide parallel an. await Promise.all([ users.createIndex({ room: 1, isOnline: -1, lastActiveAt: -1 }), // Für getParticipants() messages.createIndex({ room: 1, createdAt: 1 }), // Für getMessages() ]) console.log( `Connected to MongoDB at ${ config.uri }/${config.dbName} (collections: ${Object.values(collectionNames).join(', ')})`, ) // Öffentliches Interface des Stores — der Rest der App kennt keine MongoDB-Details (Repository-Pattern). return { // Upsert = Update + Insert in einem Schritt (atomare Operation). // Wenn User existiert: Felder aktualisieren. Wenn nicht: neu anlegen. // $setOnInsert stellt sicher dass joinedAt nur beim ersten Anlegen gesetzt wird, nicht bei Updates. async upsertUser({ id, username, room }) { const now = new Date() const result = await users.findOneAndUpdate( { _id: id }, { $set: { username: username.trim(), room, isOnline: true, lastActiveAt: now, updatedAt: now, }, $setOnInsert: { joinedAt: now, }, }, { upsert: true, returnDocument: 'after', // Aktualisiertes Dokument zurückgeben, nicht das alte. }, ) const document = extractDocument(result) if (document) { return normalizeUser(document) } // Fallback: Manche MongoDB-Versionen geben bei einem Upsert-Insert kein Dokument zurück. const inserted = await users.findOne({ _id: id }) return normalizeUser(inserted) }, // $set aktualisiert nur die angegebenen Felder — der Rest (z.B. username) bleibt unberührt. async setUserOnline(userId, room) { const result = await users.findOneAndUpdate( { _id: userId }, { $set: { isOnline: true, room, lastActiveAt: new Date(), }, }, { returnDocument: 'after' }, ) return normalizeUser(extractDocument(result)) }, // lastActiveAt wird auch beim Offline-Setzen aktualisiert — zeigt wann der User zuletzt aktiv war. async setUserOffline(userId) { const result = await users.findOneAndUpdate( { _id: userId }, { $set: { isOnline: false, lastActiveAt: new Date(), }, }, { returnDocument: 'after' }, ) return normalizeUser(extractDocument(result)) }, // trim() auf Datenbankebene — auch wenn die Frontend-Validierung fehlt, landen keine schmutzigen Daten in der DB. async updateUsername(userId, username) { const result = await users.findOneAndUpdate( { _id: userId }, { $set: { username: username.trim(), lastActiveAt: new Date(), }, }, { returnDocument: 'after' }, ) return normalizeUser(extractDocument(result)) }, // findOneAndDelete löscht und gibt das Dokument in einem Schritt zurück — kein extra findOne() nötig. async removeUser(userId) { const result = await users.findOneAndDelete({ _id: userId }) return normalizeUser(extractDocument(result)) }, async getUser(userId) { const doc = await users.findOne({ _id: userId }) return normalizeUser(doc) }, // Online-User zuerst, dann nach letzter Aktivität sortiert. // toArray() lädt alle Cursor-Ergebnisse in den Speicher — bei sehr großen Datenmengen wäre ein Cursor besser. async getParticipants(room) { const docs = await users .find({ room }) .sort({ isOnline: -1, lastActiveAt: -1 }) // -1 = absteigend, 1 = aufsteigend .toArray() return docs.map(normalizeUser) }, // _id wird von außen als UUID übergeben statt von MongoDB generiert — // so ist die ID bereits bekannt bevor die DB antwortet (z.B. für den WebSocket-Broadcast). // createdAt wird serverseitig gesetzt — dem Client-Timestamp wird nicht vertraut. async addMessage({ id, room, userId, username, text }) { const message = { _id: id, room, userId, username, text, createdAt: new Date(), } await messages.insertOne(message) // Lokales Objekt zurückgeben statt nochmal aus der DB lesen — spart einen Query. return normalizeMessage(message) }, // limit = 100 verhindert dass bei einem langen Chatverlauf alles auf einmal geladen wird. async getMessages(room, limit = 100) { const docs = await messages .find({ room }) .sort({ createdAt: 1 }) .limit(limit) .toArray() return docs.map(normalizeMessage) }, // Aggregation Pipeline: MongoDB berechnet das Ergebnis serverseitig — effizienter als // alle Daten laden und in JavaScript auswerten. // $group: Gruppiert User nach Room (wie GROUP BY in SQL). // $cond: Zählt nur Online-User pro Room. // $project: Blendet _id aus und benennt es in "name" um. async getRooms() { const result = await users.aggregate([ { $group: { _id: '$room', participantCount: { $sum: { $cond: ['$isOnline', 1, 0] } }, }, }, { $project: { _id: 0, name: '$_id', participantCount: 1 } }, { $sort: { name: 1 } }, ]).toArray() return result }, // Verbindung sauber trennen beim Herunterfahren der App — ohne close() bleiben // offene Verbindungen auf dem MongoDB-Server hängen. async disconnect() { await client.close() }, } } module.exports = createMongoStore