server/providers/pouchdb.js
const listeners = {}
let addListenersQueue = []
let wargameName = ''
const { wargameSettings, INFO_MESSAGE, dbSuffix, settings, CUSTOM_MESSAGE, databaseUrlPrefix } = require('../consts')
const pouchDb = (app, io, pouchOptions) => {
const PouchDB = require('pouchdb-core')
.plugin(require('pouchdb-adapter-node-websql'))
.plugin(require('pouchdb-adapter-http'))
.plugin(require('pouchdb-mapreduce'))
.plugin(require('pouchdb-replication'))
.defaults(pouchOptions)
require('pouchdb-all-dbs')(PouchDB)
const pouchHandle = require('express-pouchdb')(PouchDB, {
overrideMode: {
include: ['routes/fauxton']
}
})
const fauxtonIntercept = (req, res, next) => {
const FauxtonBundlePath = 'js/bundle-34997e32896293a1fa5d71f79eb1b4f7.js'
if (req.url.endsWith(`_utils/dashboard.assets/${FauxtonBundlePath}`)) {
const bundlePath = require('path').join(__dirname, '../node_modules/pouchdb-fauxton/www/dashboard.assets/', FauxtonBundlePath)
let jsFile
try {
jsFile = require('fs').readFileSync(bundlePath).toString()
} catch (err) {
console.error(`Could not read Fauxton bundle file at ${bundlePath}: ${err.message}`)
jsFile = ''
}
/* eslint-disable no-useless-escape */
res.send(jsFile
.replace('host:"../.."', 'host:".."')
.replace('root:"/_utils"', `root:"${databaseUrlPrefix}/_utils"`)
.replace(/url:\"\/_session/g, `url:"${databaseUrlPrefix}/_session`)
.replace(/url:\"\/_replicator/g, `url:"${databaseUrlPrefix}/_replicator`)
.replace(/window\.location\.origin\+\"\/_replicator/g, `window.location.origin+"${databaseUrlPrefix}/_replicator`)
.replace(/url:\"\/_users/g, `url:"${databaseUrlPrefix}/_users`)
.replace('window.location.origin+"/"+o.default.utils.safeURLName', `window.location.origin+"${databaseUrlPrefix}/"+o.default.utils.safeURLName`))
return
}
return pouchHandle(req, res, next)
}
app.use(databaseUrlPrefix, fauxtonIntercept)
// changesListener
const initChangesListener = (dbName) => {
const db = new PouchDB(dbName, pouchOptions)
// saving listener
listeners[dbName] = db.changes({
since: 'now',
live: true,
timeout: false,
heartbeat: false,
include_docs: true
}).on('change', (result) => io.emit(wargameName, result.doc))
}
// check listeners queue to add a new listenr
setInterval(() => {
if (addListenersQueue.length) {
for (const dbName of addListenersQueue) {
initChangesListener(dbName)
}
// clean queue
addListenersQueue = []
}
}, 5000)
PouchDB.allDbs().then(dbs => {
dbs.forEach(db => initChangesListener(db))
}).catch(err => console.log('Error on load alldbs', err))
const checkSqliteExists = (dbName) => {
return dbName.indexOf('wargame') !== -1 && dbName.indexOf(dbSuffix) === -1 ? dbName + dbSuffix : dbName
}
app.put('/:wargame', async (req, res) => {
// TODO: if this req is an activity document (or list of them)
// then we should actually push it to the player logs database
const databaseName = checkSqliteExists(req.params.wargame)
const db = new PouchDB(databaseName, pouchOptions)
const putData = req.body
wargameName = req.params.wargame
if (!listeners[databaseName]) {
addListenersQueue.push(databaseName)
}
const retryUntilWritten = (db, doc) => {
return db.get(doc._id).then((origDoc) => {
doc._rev = origDoc._rev
return db.put(doc).then(async () => {
await db.compact()
res.send({ msg: 'Updated', data: doc })
})
}).catch(err => {
if (err.status === 409) {
return retryUntilWritten(db, doc)
} else { // new doc
return db.put(doc)
.then(() => res.send({ msg: 'Saved', data: doc }))
.catch(() => {
const settingsDoc = {
...doc,
// TODO: this seems to be changing the doc name from date-time (or 'initial-settings')
// TODO: to 'settings'
_id: settings
}
return retryUntilWritten(db, settingsDoc)
})
}
})
}
retryUntilWritten(db, putData)
})
// Define a route to handle bulk document updates in a specified database
app.put('/bulkDocs/:dbname', async (req, res) => {
const databaseName = checkSqliteExists(req.params.dbname)
const db = new PouchDB(databaseName, pouchOptions)
// Get the array of documents from the request body
const docs = req.body
if (!listeners[databaseName]) {
addListenersQueue.push(databaseName)
}
// Check if there are any documents to update
if (docs.length === 0) {
// nothing to do
res.send({ msg: 'OK' })
} else {
// If there are documents, update them in bulk
return db.bulkDocs(docs).then(async () => {
// If the bulk update succeeds, emit an event to notify clients of the update
io.emit(req.params.dbname, docs)
// Compact the database to free up disk space
await db.compact()
res.send({ msg: 'OK' })
}).catch(err => {
// If there is an error with the bulk update, send a response with an error message and data
res.send({ msg: 'err', data: err })
})
}
})
app.get('/replicate/:replicate/:dbname', (req, res) => {
const newDbName = checkSqliteExists(req.params.replicate) // new db name
const newDb = new PouchDB(newDbName, pouchOptions)
const existingDatabase = checkSqliteExists(req.params.dbname) // copy data from
newDb.replicate.from(existingDatabase).then(() => {
res.send('Replicated')
}).catch(err => res.status(400).send({ msg: 'Error on replication', data: err }))
})
app.delete('/delete/:dbName', (req, res) => {
const dbName = checkSqliteExists(req.params.dbName)
const db = new PouchDB(dbName, pouchOptions)
db.destroy().then(() => {
res.send({ msg: 'ok', data: dbName })
}).catch((err) => res.status(400).send({ msg: 'error', data: err }))
})
app.delete('/clearAll', (req, res) => {
PouchDB.resetAllDbs()
.then(() => res.send())
.catch(err => res.status(500).send(`Error on clearAll ${err}`))
})
// get all wargame names
app.get('/allDbs', async (req, res) => {
PouchDB.allDbs().then(dbs => {
const dbList = dbs.map(dbName => dbName.replace(dbSuffix, ''))
res.send({ msg: 'ok', data: dbList || [] })
}).catch(() => res.send([]))
})
// get all message documents for wargame
app.get('/:wargame', async (req, res) => {
const databaseName = checkSqliteExists(req.params.wargame)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Wargame Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
db.allDocs({ include_docs: true, attachments: true })
.then(result => {
// unpack the documents
const docs = result.rows.map((item) => item.doc)
// drop wargame & info messages
// NO, don't. We need the info messages, for the turn markers
// const ignoreTypes = [] //INFO_MESSAGE, COUNTER_MESSAGE]
// const messages = docs.filter((doc) => !ignoreTypes.includes(doc.messageType))
res.send({ msg: 'ok', data: docs })
}).catch(() => res.send([]))
})
app.get('/:wargame/last', (req, res) => {
const databaseName = checkSqliteExists(req.params.wargame)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Wargame Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
// NOTE: if we end up with a performance problem from the "reverse sort" processing
// NOTE: here is a suggested alternate strategy:
// NOTE: for each "new wargame" we push two documents: the wargame with date-time id
// NOTE: "and" one with a fixed id "settings"
// NOTE: So, when calling 'last' we first try to retrieve "settings", if it's not there
// NOTE: then we do reverse-sort to find the latest one.
// NOTE: If we do "wind-back" of wargame, delete "settings"
db.find({
selector: {
messageType: INFO_MESSAGE,
_id: { $ne: wargameSettings }
},
limit: 1,
sort: [{ _id: 'desc' }]
}).then((result) => res.send({ msg: 'ok', data: result.docs }))
.catch(() => res.send([]))
})
app.get('/:wargame/turns', (req, res) => {
const databaseName = checkSqliteExists(req.params.wargame)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Wargame Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
db.find({
selector: {
adjudicationStartTime: { $exists: true }
},
fields: ['data', 'gameTurn']
}).then((result) => {
const uniqBy = (data, key) => {
return [
...new Map(
data.map(x => [key(x),
{
gameTurn: x.gameTurn,
gameTurnTime: x.data.overview.gameTurnTime,
gameDate: x.data.overview.gameDate
}])
).values()
]
}
const resaultData = uniqBy(result.docs, it => it.gameTurn)
res.send({ msg: 'ok', data: resaultData })
})
.catch(() => res.send([]))
})
app.get('/:wargame/:dbname/logs', (req, res) => {
const databaseName = checkSqliteExists(req.params.dbname)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Player Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
db.find({
selector: {
wargame: req.params.wargame
}
}).then((result) => {
res.send({ msg: 'ok', data: result.docs })
})
.catch(() => res.send([]))
})
app.get('/:wargame/:force/:id/counter', (req, res) => {
const databaseName = checkSqliteExists(req.params.wargame)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Wargame Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
let messageDefaultCount = 1
db.get(req.params.id)
.then(data => res.send({ msg: 'ok', data: data.details.counter }))
.catch(() => {
db.find({
selector: {
messageType: CUSTOM_MESSAGE,
details: {
from: { force: req.params.force },
counter: { $exists: true }
},
_id: { $ne: settings }
},
fields: ['details.counter']
}).then((result) => {
if (result.docs.length) {
const Biggestcount = Math.max(...result.docs.map(data => data.details.counter))
if (Biggestcount) {
messageDefaultCount += Biggestcount
}
}
res.send({ msg: 'ok', data: messageDefaultCount })
})
.catch(() => res.send([]))
})
})
app.get('/:wargame/:dbname/logs-latest', (req, res) => {
const databaseName = checkSqliteExists(req.params.dbname)
if (!databaseName) {
res.status(404).send({ msg: 'Wrong Player Name', data: null })
}
const db = new PouchDB(databaseName, pouchOptions)
db.find({
selector: {
wargame: req.params.wargame
},
fields: ['role', 'activityTime', 'activityType']
}).then((result) => {
const uniqByKeepLast = (data, key) => {
return [
...new Map(
data.map(x => [key(x), x])
).values()
]
}
const lastLogs = result.docs && uniqByKeepLast(result.docs, it => it.role)
res.send({ msg: 'ok', data: lastLogs })
})
.catch(() => res.send([]))
})
// get document for wargame
app.get('/get/:wargame/:id', (req, res) => {
const databaseName = checkSqliteExists(req.params.wargame)
const db = new PouchDB(databaseName, pouchOptions)
const id = `${req.params.id}`
if (!id || !databaseName) {
res.status(404).send({ msg: 'Wrong Id or Wargame', data: null })
}
db.get(id)
.then(data => res.send({ msg: 'ok', data: data }))
.catch(() => {
// TODO: if the id doesn't exist, it looks for 'settings', but we
// TODO: won't have a 'settings' document.
db.get(settings)
.then(data => res.send({ msg: 'ok', data: data }))
.catch((err) => res.send({ msg: 'err', data: err }))
})
})
}
module.exports = pouchDb