Add data layer with MongoDB and JSON fallback store
Introduce database config, MongoDB and JSON store implementations, and a datastore factory that connects to MongoDB by default and falls back to a local JSON file when MongoDB is unavailable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
const config = require('../../config/database')
|
||||
|
||||
|
||||
let MongoClient
|
||||
try {
|
||||
;({ MongoClient } = require('mongodb'))
|
||||
} catch (error) {
|
||||
MongoClient = null
|
||||
}
|
||||
|
||||
|
||||
const collectionNames = {
|
||||
users: 'users',
|
||||
messages: 'messages',
|
||||
}
|
||||
|
||||
function normalizeUser(doc) {
|
||||
if (!doc) return null
|
||||
const {
|
||||
_id,
|
||||
username,
|
||||
room,
|
||||
isOnline,
|
||||
joinedAt,
|
||||
lastActiveAt,
|
||||
} = doc
|
||||
return {
|
||||
id: _id,
|
||||
username,
|
||||
room,
|
||||
isOnline,
|
||||
joinedAt,
|
||||
lastActiveAt,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMessage(doc) {
|
||||
if (!doc) return null
|
||||
const {
|
||||
_id,
|
||||
room,
|
||||
userId,
|
||||
username,
|
||||
text,
|
||||
createdAt,
|
||||
} = doc
|
||||
return {
|
||||
id: _id,
|
||||
room,
|
||||
userId,
|
||||
username,
|
||||
text,
|
||||
createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
function extractDocument(result) {
|
||||
if (!result) return null
|
||||
return Object.prototype.hasOwnProperty.call(result, 'value')
|
||||
? result.value
|
||||
: result
|
||||
}
|
||||
|
||||
async function createMongoStore() {
|
||||
// Guard: fail fast with an actionable message if the driver is missing.
|
||||
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',
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const db = client.db(config.dbName)
|
||||
|
||||
const users = db.collection(collectionNames.users)
|
||||
const messages = db.collection(collectionNames.messages)
|
||||
|
||||
await Promise.all([
|
||||
users.createIndex({ room: 1, isOnline: -1, lastActiveAt: -1 }),
|
||||
messages.createIndex({ room: 1, createdAt: 1 }),
|
||||
])
|
||||
|
||||
console.log(
|
||||
`Connected to MongoDB at ${
|
||||
config.uri
|
||||
}/${config.dbName} (collections: ${Object.values(collectionNames).join(', ')})`,
|
||||
)
|
||||
|
||||
return {
|
||||
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',
|
||||
},
|
||||
)
|
||||
const document = extractDocument(result)
|
||||
if (document) {
|
||||
return normalizeUser(document)
|
||||
}
|
||||
|
||||
|
||||
const inserted = await users.findOne({ _id: id })
|
||||
return normalizeUser(inserted)
|
||||
},
|
||||
|
||||
async setUserOnline(userId, room) {
|
||||
const result = await users.findOneAndUpdate(
|
||||
{ _id: userId },
|
||||
{
|
||||
$set: {
|
||||
isOnline: true,
|
||||
room,
|
||||
lastActiveAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
)
|
||||
return normalizeUser(extractDocument(result))
|
||||
},
|
||||
|
||||
async setUserOffline(userId) {
|
||||
const result = await users.findOneAndUpdate(
|
||||
{ _id: userId },
|
||||
{
|
||||
$set: {
|
||||
isOnline: false,
|
||||
lastActiveAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
)
|
||||
return normalizeUser(extractDocument(result))
|
||||
},
|
||||
|
||||
async updateUsername(userId, username) {
|
||||
const result = await users.findOneAndUpdate(
|
||||
{ _id: userId },
|
||||
{
|
||||
$set: {
|
||||
username: username.trim(),
|
||||
lastActiveAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
)
|
||||
return normalizeUser(extractDocument(result))
|
||||
},
|
||||
|
||||
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)
|
||||
},
|
||||
|
||||
async getParticipants(room) {
|
||||
const docs = await users
|
||||
.find({ room })
|
||||
.sort({ isOnline: -1, lastActiveAt: -1 })
|
||||
.toArray()
|
||||
return docs.map(normalizeUser)
|
||||
},
|
||||
|
||||
async addMessage({ id, room, userId, username, text }) {
|
||||
const message = {
|
||||
_id: id,
|
||||
room,
|
||||
userId,
|
||||
username,
|
||||
text,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
await messages.insertOne(message)
|
||||
return normalizeMessage(message)
|
||||
},
|
||||
|
||||
async getMessages(room, limit = 100) {
|
||||
const docs = await messages
|
||||
.find({ room })
|
||||
.sort({ createdAt: 1 })
|
||||
.limit(limit)
|
||||
.toArray()
|
||||
return docs.map(normalizeMessage)
|
||||
},
|
||||
|
||||
async disconnect() {
|
||||
await client.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = createMongoStore
|
||||
Reference in New Issue
Block a user