first commit
This commit is contained in:
112
services/notifications/app/js/HealthCheckController.js
Normal file
112
services/notifications/app/js/HealthCheckController.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* eslint-disable
|
||||
no-dupe-keys,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const { db, ObjectId } = require('./mongodb')
|
||||
const request = require('request')
|
||||
const async = require('async')
|
||||
const settings = require('@overleaf/settings')
|
||||
const { port } = settings.internal.notifications
|
||||
const logger = require('@overleaf/logger')
|
||||
|
||||
module.exports = {
|
||||
check(callback) {
|
||||
const userId = new ObjectId()
|
||||
const cleanupNotifications = callback =>
|
||||
db.notifications.deleteOne({ user_id: userId }, callback)
|
||||
|
||||
let notificationKey = `smoke-test-notification-${new ObjectId()}`
|
||||
const getOpts = endPath => ({
|
||||
url: `http://127.0.0.1:${port}/user/${userId}${endPath}`,
|
||||
timeout: 5000,
|
||||
})
|
||||
logger.debug(
|
||||
{ userId, opts: getOpts(), key: notificationKey, userId },
|
||||
'Health Check: running'
|
||||
)
|
||||
const jobs = [
|
||||
function (cb) {
|
||||
const opts = getOpts('/')
|
||||
opts.json = {
|
||||
key: notificationKey,
|
||||
messageOpts: '',
|
||||
templateKey: 'f4g5',
|
||||
user_id: userId,
|
||||
}
|
||||
return request.post(opts, cb)
|
||||
},
|
||||
function (cb) {
|
||||
const opts = getOpts('/')
|
||||
opts.json = true
|
||||
return request.get(opts, function (err, res, body) {
|
||||
if (err != null) {
|
||||
logger.err({ err }, 'Health Check: error getting notification')
|
||||
return callback(err)
|
||||
} else if (res.statusCode !== 200) {
|
||||
const e = `status code not 200 ${res.statusCode}`
|
||||
logger.err({ err }, e)
|
||||
return cb(e)
|
||||
}
|
||||
const hasNotification = body.some(
|
||||
notification =>
|
||||
notification.key === notificationKey &&
|
||||
notification.user_id === userId.toString()
|
||||
)
|
||||
if (hasNotification) {
|
||||
return cb(null, body)
|
||||
} else {
|
||||
logger.err(
|
||||
{ body, notificationKey },
|
||||
'Health Check: notification not in response'
|
||||
)
|
||||
return cb(new Error('notification not found in response'))
|
||||
}
|
||||
})
|
||||
},
|
||||
]
|
||||
return async.series(jobs, function (err, body) {
|
||||
if (err != null) {
|
||||
logger.err({ err }, 'Health Check: error running health check')
|
||||
return cleanupNotifications(() => callback(err))
|
||||
} else {
|
||||
const notificationId = body[1][0]._id
|
||||
notificationKey = body[1][0].key
|
||||
let opts = getOpts(`/notification/${notificationId}`)
|
||||
logger.debug(
|
||||
{ notificationId, notificationKey },
|
||||
'Health Check: doing cleanup'
|
||||
)
|
||||
return request.del(opts, function (err, res, body) {
|
||||
if (err != null) {
|
||||
logger.err(
|
||||
err,
|
||||
opts,
|
||||
'Health Check: error cleaning up notification'
|
||||
)
|
||||
return callback(err)
|
||||
}
|
||||
opts = getOpts('')
|
||||
opts.json = { key: notificationKey }
|
||||
return request.del(opts, function (err, res, body) {
|
||||
if (err != null) {
|
||||
logger.err(
|
||||
err,
|
||||
opts,
|
||||
'Health Check: error cleaning up notification'
|
||||
)
|
||||
return callback(err)
|
||||
}
|
||||
return cleanupNotifications(callback)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
132
services/notifications/app/js/Notifications.js
Normal file
132
services/notifications/app/js/Notifications.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const logger = require('@overleaf/logger')
|
||||
const { db, ObjectId } = require('./mongodb')
|
||||
|
||||
module.exports = {
|
||||
getUserNotifications(userId, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const query = {
|
||||
user_id: new ObjectId(userId),
|
||||
templateKey: { $exists: true },
|
||||
}
|
||||
db.notifications.find(query).toArray(callback)
|
||||
},
|
||||
|
||||
_countExistingNotifications(userId, notification, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
const query = {
|
||||
user_id: new ObjectId(userId),
|
||||
key: notification.key,
|
||||
}
|
||||
return db.notifications.count(query, function (err, count) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
return callback(null, count)
|
||||
})
|
||||
},
|
||||
|
||||
addNotification(userId, notification, callback) {
|
||||
return this._countExistingNotifications(
|
||||
userId,
|
||||
notification,
|
||||
function (err, count) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
if (count !== 0 && !notification.forceCreate) {
|
||||
return callback()
|
||||
}
|
||||
const doc = {
|
||||
user_id: new ObjectId(userId),
|
||||
key: notification.key,
|
||||
messageOpts: notification.messageOpts,
|
||||
templateKey: notification.templateKey,
|
||||
}
|
||||
// TTL index on the optional `expires` field, which should arrive as an iso date-string, corresponding to
|
||||
// a datetime in the future when the document should be automatically removed.
|
||||
// in Mongo, TTL indexes only work on date fields, and ignore the document when that field is missing
|
||||
// see `README.md` for instruction on creating TTL index
|
||||
if (notification.expires != null) {
|
||||
try {
|
||||
doc.expires = new Date(notification.expires)
|
||||
const _testValue = doc.expires.toISOString()
|
||||
} catch (error) {
|
||||
err = error
|
||||
logger.error(
|
||||
{ userId, expires: notification.expires },
|
||||
'error converting `expires` field to Date'
|
||||
)
|
||||
return callback(err)
|
||||
}
|
||||
}
|
||||
db.notifications.updateOne(
|
||||
{ user_id: doc.user_id, key: notification.key },
|
||||
{ $set: doc },
|
||||
{ upsert: true },
|
||||
callback
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
removeNotificationId(userId, notificationId, callback) {
|
||||
const searchOps = {
|
||||
user_id: new ObjectId(userId),
|
||||
_id: new ObjectId(notificationId),
|
||||
}
|
||||
const updateOperation = { $unset: { templateKey: true, messageOpts: true } }
|
||||
db.notifications.updateOne(searchOps, updateOperation, callback)
|
||||
},
|
||||
|
||||
removeNotificationKey(userId, notificationKey, callback) {
|
||||
const searchOps = {
|
||||
user_id: new ObjectId(userId),
|
||||
key: notificationKey,
|
||||
}
|
||||
const updateOperation = { $unset: { templateKey: true } }
|
||||
db.notifications.updateOne(searchOps, updateOperation, callback)
|
||||
},
|
||||
|
||||
removeNotificationByKeyOnly(notificationKey, callback) {
|
||||
const searchOps = { key: notificationKey }
|
||||
const updateOperation = { $unset: { templateKey: true } }
|
||||
db.notifications.updateOne(searchOps, updateOperation, callback)
|
||||
},
|
||||
|
||||
countNotificationsByKeyOnly(notificationKey, callback) {
|
||||
const searchOps = { key: notificationKey, templateKey: { $exists: true } }
|
||||
db.notifications.count(searchOps, callback)
|
||||
},
|
||||
|
||||
deleteUnreadNotificationsByKeyOnlyBulk(notificationKey, callback) {
|
||||
if (typeof notificationKey !== 'string') {
|
||||
throw new Error('refusing to bulk delete arbitrary notifications')
|
||||
}
|
||||
const searchOps = { key: notificationKey, templateKey: { $exists: true } }
|
||||
db.notifications.deleteMany(searchOps, (err, result) => {
|
||||
if (err) return callback(err)
|
||||
callback(null, result.deletedCount)
|
||||
})
|
||||
},
|
||||
|
||||
// hard delete of doc, rather than removing the templateKey
|
||||
deleteNotificationByKeyOnly(notificationKey, callback) {
|
||||
const searchOps = { key: notificationKey }
|
||||
db.notifications.deleteOne(searchOps, callback)
|
||||
},
|
||||
}
|
117
services/notifications/app/js/NotificationsController.js
Normal file
117
services/notifications/app/js/NotificationsController.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const Notifications = require('./Notifications')
|
||||
const logger = require('@overleaf/logger')
|
||||
const metrics = require('@overleaf/metrics')
|
||||
|
||||
module.exports = {
|
||||
getUserNotifications(req, res, next) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id },
|
||||
'getting user unread notifications'
|
||||
)
|
||||
metrics.inc('getUserNotifications')
|
||||
return Notifications.getUserNotifications(
|
||||
req.params.user_id,
|
||||
(err, notifications) => {
|
||||
if (err) return next(err)
|
||||
res.json(notifications)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
addNotification(req, res) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id, notification: req.body },
|
||||
'adding notification'
|
||||
)
|
||||
metrics.inc('addNotification')
|
||||
return Notifications.addNotification(
|
||||
req.params.user_id,
|
||||
req.body,
|
||||
function (err, notifications) {
|
||||
if (err != null) {
|
||||
return res.sendStatus(500)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
removeNotificationId(req, res, next) {
|
||||
logger.debug(
|
||||
{
|
||||
userId: req.params.user_id,
|
||||
notificationId: req.params.notification_id,
|
||||
},
|
||||
'mark id notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationId')
|
||||
return Notifications.removeNotificationId(
|
||||
req.params.user_id,
|
||||
req.params.notification_id,
|
||||
err => {
|
||||
if (err) return next(err)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
removeNotificationKey(req, res, next) {
|
||||
logger.debug(
|
||||
{ userId: req.params.user_id, notificationKey: req.body.key },
|
||||
'mark key notification as read'
|
||||
)
|
||||
metrics.inc('removeNotificationKey')
|
||||
return Notifications.removeNotificationKey(
|
||||
req.params.user_id,
|
||||
req.body.key,
|
||||
(err, notifications) => {
|
||||
if (err) return next(err)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
removeNotificationByKeyOnly(req, res, next) {
|
||||
const notificationKey = req.params.key
|
||||
logger.debug({ notificationKey }, 'mark notification as read by key only')
|
||||
metrics.inc('removeNotificationKey')
|
||||
return Notifications.removeNotificationByKeyOnly(notificationKey, err => {
|
||||
if (err) return next(err)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
},
|
||||
|
||||
countNotificationsByKeyOnly(req, res) {
|
||||
const notificationKey = req.params.key
|
||||
Notifications.countNotificationsByKeyOnly(notificationKey, (err, count) => {
|
||||
if (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot count by key')
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
res.json({ count })
|
||||
})
|
||||
},
|
||||
|
||||
deleteUnreadNotificationsByKeyOnlyBulk(req, res) {
|
||||
const notificationKey = req.params.key
|
||||
Notifications.deleteUnreadNotificationsByKeyOnlyBulk(
|
||||
notificationKey,
|
||||
(err, count) => {
|
||||
if (err) {
|
||||
logger.err({ err, notificationKey }, 'cannot bulk remove by key')
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
res.json({ count })
|
||||
}
|
||||
)
|
||||
},
|
||||
}
|
18
services/notifications/app/js/mongodb.js
Normal file
18
services/notifications/app/js/mongodb.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const { MongoClient, ObjectId } = require('mongodb-legacy')
|
||||
|
||||
const mongoClient = new MongoClient(Settings.mongo.url, Settings.mongo.options)
|
||||
const mongoDb = mongoClient.db()
|
||||
|
||||
const db = {
|
||||
notifications: mongoDb.collection('notifications'),
|
||||
}
|
||||
|
||||
Metrics.mongodb.monitor(mongoClient)
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
mongoClient,
|
||||
ObjectId,
|
||||
}
|
Reference in New Issue
Block a user