Convert to async/await

GitOrigin-RevId: 45c139a64c848650f1f51e64e8ebd233211241fb
This commit is contained in:
Andrew Rumble
2025-09-12 08:07:30 +00:00
committed by Copybot
parent 0a845b3f06
commit f0bd9b57b8
8 changed files with 334 additions and 349 deletions
+2 -3
View File
@@ -44163,6 +44163,7 @@
"name": "@overleaf/notifications", "name": "@overleaf/notifications",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@overleaf/fetch-utils": "*",
"@overleaf/logger": "*", "@overleaf/logger": "*",
"@overleaf/metrics": "*", "@overleaf/metrics": "*",
"@overleaf/mongo-utils": "*", "@overleaf/mongo-utils": "*",
@@ -44173,11 +44174,9 @@
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"express": "^4.21.2", "express": "^4.21.2",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"mongodb-legacy": "6.1.3", "mongodb-legacy": "6.1.3"
"request": "^2.88.2"
}, },
"devDependencies": { "devDependencies": {
"@types/request": "^2.48.12",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
+1 -10
View File
@@ -38,16 +38,7 @@ app.delete(
app.get('/status', (req, res) => res.send('notifications is up')) app.get('/status', (req, res) => res.send('notifications is up'))
app.get('/health_check', (req, res) => app.get('/health_check', HealthCheckController.check)
HealthCheckController.check(function (err) {
if (err) {
logger.err({ err }, 'error performing health check')
res.sendStatus(500)
} else {
res.sendStatus(200)
}
})
)
app.get('*', (req, res) => res.sendStatus(404)) app.get('*', (req, res) => res.sendStatus(404))
@@ -1,110 +1,130 @@
// 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
*/
import { db, ObjectId } from './mongodb.js' import { db, ObjectId } from './mongodb.js'
import request from 'request'
import async from 'async'
import settings from '@overleaf/settings' import settings from '@overleaf/settings'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import {
fetchJson,
fetchNothing,
RequestFailedError,
} from '@overleaf/fetch-utils'
import { expressify } from '@overleaf/promise-utils'
const { port } = settings.internal.notifications const { port } = settings.internal.notifications
export default { function makeUrl(userId, endPath = '') {
check(callback) { return new URL(`/user/${userId}/${endPath}`, `http://127.0.0.1:${port}`)
const userId = new ObjectId() }
const cleanupNotifications = callback =>
db.notifications.deleteOne({ user_id: userId }, callback) async function makeNotification(notificationKey, userId) {
const postOpts = {
let notificationKey = `smoke-test-notification-${new ObjectId()}` method: 'POST',
const getOpts = endPath => ({ json: {
url: `http://127.0.0.1:${port}/user/${userId}${endPath}`, key: notificationKey,
timeout: 5000, messageOpts: '',
}) templateKey: 'f4g5',
logger.debug( user_id: userId,
{ opts: getOpts(), key: notificationKey, userId }, },
'Health Check: running' signal: AbortSignal.timeout(5000),
) }
const jobs = [ const url = makeUrl(userId)
function (cb) { await fetchNothing(url, postOpts)
const opts = getOpts('/') }
opts.json = {
key: notificationKey, async function getUsersNotifications(userId) {
messageOpts: '', const url = makeUrl(userId)
templateKey: 'f4g5', try {
user_id: userId, return await fetchJson(url, {
} signal: AbortSignal.timeout(5000),
return request.post(opts, cb) })
}, } catch (err) {
function (cb) { if (err instanceof RequestFailedError) {
const opts = getOpts('/') logger.err({ err }, 'Non-2xx status code received')
opts.json = true throw err
return request.get(opts, function (err, res, body) { }
if (err != null) { logger.err({ err }, 'Health Check: error getting notification')
logger.err({ err }, 'Health Check: error getting notification') throw err
return callback(err) }
} else if (res.statusCode !== 200) { }
const e = `status code not 200 ${res.statusCode}`
logger.err({ err }, e) async function userHasNotification(userId, notificationKey) {
return cb(e) const body = await getUsersNotifications(userId)
} const hasNotification = body.some(
const hasNotification = body.some( notification =>
notification => notification.key === notificationKey && notification.user_id === userId
notification.key === notificationKey && )
notification.user_id === userId.toString() if (hasNotification) {
) return body
if (hasNotification) { } else {
return cb(null, body) logger.err(
} else { { body, notificationKey },
logger.err( 'Health Check: notification not in response'
{ body, notificationKey }, )
'Health Check: notification not in response' throw new Error('notification not found in response')
) }
return cb(new Error('notification not found in response')) }
}
}) async function cleanupNotifications(userId) {
}, await db.notifications.deleteOne({ user_id: userId })
] }
return async.series(jobs, function (err, body) {
if (err != null) { async function deleteNotification(userId, notificationId, notificationKey) {
logger.err({ err }, 'Health Check: error running health check') const deleteByIdUrl = makeUrl(userId, `notification/${notificationId}`)
return cleanupNotifications(() => callback(err)) try {
} else { await fetchNothing(deleteByIdUrl, {
const notificationId = body[1][0]._id signal: AbortSignal.timeout(5000),
notificationKey = body[1][0].key method: 'DELETE',
let opts = getOpts(`/notification/${notificationId}`) })
logger.debug( } catch (err) {
{ notificationId, notificationKey }, logger.err(
'Health Check: doing cleanup' { err, url: deleteByIdUrl },
) 'Health Check: error cleaning up notification'
return request.del(opts, function (err, res, body) { )
if (err != null) { throw err
logger.err( }
err,
opts, const deleteByKeyUrl = makeUrl(userId)
'Health Check: error cleaning up notification'
) try {
return callback(err) await fetchNothing(deleteByKeyUrl, {
} signal: AbortSignal.timeout(5000),
opts = getOpts('') method: 'DELETE',
opts.json = { key: notificationKey } body: {
return request.del(opts, function (err, res, body) { key: notificationKey,
if (err != null) { },
logger.err( })
err, } catch (err) {
opts, logger.err(
'Health Check: error cleaning up notification' { err, url: deleteByKeyUrl },
) 'Health Check: error cleaning up notification'
return callback(err) )
} throw err
return cleanupNotifications(callback) }
}) }
})
} async function check(req, res) {
}) const userId = new ObjectId().toString()
}, let notificationKey = `smoke-test-notification-${new ObjectId()}`
logger.debug({ userId, key: notificationKey }, 'Health Check: running')
await makeNotification(notificationKey, userId)
try {
const body = await userHasNotification(userId, notificationKey)
const notificationId = body[0]._id
notificationKey = body[0].key
logger.debug(
{ notificationId, notificationKey },
'Health Check: doing cleanup'
)
await deleteNotification(userId, notificationId, notificationKey)
res.sendStatus(200)
} catch (err) {
logger.err({ err }, 'Health Check: error running health check')
res.sendStatus(500)
} finally {
await cleanupNotifications(userId)
}
}
export default {
check: expressify(check),
} }
+105 -117
View File
@@ -1,121 +1,109 @@
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import { db, ObjectId } from './mongodb.js' import { db, ObjectId } from './mongodb.js'
export default { async function getUserNotifications(userId) {
getUserNotifications(userId, callback) { const query = {
if (callback == null) { user_id: new ObjectId(userId),
callback = function () {} templateKey: { $exists: true },
} }
const query = { return await db.notifications.find(query).toArray()
user_id: new ObjectId(userId), }
templateKey: { $exists: true },
} async function _countExistingNotifications(userId, notification) {
db.notifications.find(query).toArray(callback) const query = {
}, user_id: new ObjectId(userId),
key: notification.key,
_countExistingNotifications(userId, notification, callback) { }
if (callback == null) { return await db.notifications.count(query)
callback = function () {} }
}
const query = { async function addNotification(userId, notification, callback) {
user_id: new ObjectId(userId), const count = await _countExistingNotifications(userId, notification)
key: notification.key, if (count !== 0 && !notification.forceCreate) {
} return
return db.notifications.count(query, function (err, count) { }
if (err != null) { const doc = {
return callback(err) user_id: new ObjectId(userId),
} key: notification.key,
return callback(null, count) messageOpts: notification.messageOpts,
}) templateKey: notification.templateKey,
}, }
// TTL index on the optional `expires` field, which should arrive as an iso date-string, corresponding to
addNotification(userId, notification, callback) { // a datetime in the future when the document should be automatically removed.
return this._countExistingNotifications( // in Mongo, TTL indexes only work on date fields, and ignore the document when that field is missing
userId, // see `README.md` for instruction on creating TTL index
notification, if (notification.expires != null) {
function (err, count) { try {
if (err != null) { doc.expires = new Date(notification.expires)
return callback(err) // _testValue assignment will throw if `expires` is not a valid date
} // eslint-disable-next-line no-unused-vars
if (count !== 0 && !notification.forceCreate) { const _testValue = doc.expires.toISOString()
return callback() } catch (error) {
} logger.error(
const doc = { { userId, expires: notification.expires },
user_id: new ObjectId(userId), 'error converting `expires` field to Date'
key: notification.key, )
messageOpts: notification.messageOpts, throw error
templateKey: notification.templateKey, }
} }
// TTL index on the optional `expires` field, which should arrive as an iso date-string, corresponding to return await db.notifications.updateOne(
// a datetime in the future when the document should be automatically removed. { user_id: doc.user_id, key: notification.key },
// in Mongo, TTL indexes only work on date fields, and ignore the document when that field is missing { $set: doc },
// see `README.md` for instruction on creating TTL index { upsert: true }
if (notification.expires != null) { )
try { }
doc.expires = new Date(notification.expires)
const _testValue = doc.expires.toISOString() async function removeNotificationId(userId, notificationId) {
} catch (error) { const searchOps = {
err = error user_id: new ObjectId(userId),
logger.error( _id: new ObjectId(notificationId),
{ userId, expires: notification.expires }, }
'error converting `expires` field to Date' const updateOperation = { $unset: { templateKey: true, messageOpts: true } }
) return await db.notifications.updateOne(searchOps, updateOperation)
return callback(err) }
}
} async function removeNotificationKey(userId, notificationKey) {
db.notifications.updateOne( const searchOps = {
{ user_id: doc.user_id, key: notification.key }, user_id: new ObjectId(userId),
{ $set: doc }, key: notificationKey,
{ upsert: true }, }
callback const updateOperation = { $unset: { templateKey: true } }
) return await db.notifications.updateOne(searchOps, updateOperation)
} }
)
}, async function removeNotificationByKeyOnly(notificationKey) {
const searchOps = { key: notificationKey }
removeNotificationId(userId, notificationId, callback) { const updateOperation = { $unset: { templateKey: true } }
const searchOps = { return await db.notifications.updateOne(searchOps, updateOperation)
user_id: new ObjectId(userId), }
_id: new ObjectId(notificationId),
} async function countNotificationsByKeyOnly(notificationKey) {
const updateOperation = { $unset: { templateKey: true, messageOpts: true } } const searchOps = { key: notificationKey, templateKey: { $exists: true } }
db.notifications.updateOne(searchOps, updateOperation, callback) return await db.notifications.countDocuments(searchOps)
}, }
removeNotificationKey(userId, notificationKey, callback) { async function deleteUnreadNotificationsByKeyOnlyBulk(notificationKey) {
const searchOps = { if (typeof notificationKey !== 'string') {
user_id: new ObjectId(userId), throw new Error('refusing to bulk delete arbitrary notifications')
key: notificationKey, }
} const searchOps = { key: notificationKey, templateKey: { $exists: true } }
const updateOperation = { $unset: { templateKey: true } } const result = await db.notifications.deleteMany(searchOps)
db.notifications.updateOne(searchOps, updateOperation, callback) return result.deletedCount
}, }
removeNotificationByKeyOnly(notificationKey, callback) { // hard delete of doc, rather than removing the templateKey
const searchOps = { key: notificationKey } async function deleteNotificationByKeyOnly(notificationKey) {
const updateOperation = { $unset: { templateKey: true } } const searchOps = { key: notificationKey }
db.notifications.updateOne(searchOps, updateOperation, callback) return await db.notifications.deleteOne(searchOps)
}, }
countNotificationsByKeyOnly(notificationKey, callback) { export default {
const searchOps = { key: notificationKey, templateKey: { $exists: true } } addNotification,
db.notifications.count(searchOps, callback) getUserNotifications,
}, removeNotificationId,
removeNotificationKey,
deleteUnreadNotificationsByKeyOnlyBulk(notificationKey, callback) { removeNotificationByKeyOnly,
if (typeof notificationKey !== 'string') { countNotificationsByKeyOnly,
throw new Error('refusing to bulk delete arbitrary notifications') deleteUnreadNotificationsByKeyOnlyBulk,
} deleteNotificationByKeyOnly,
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)
},
} }
@@ -1,117 +1,103 @@
// 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
*/
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics' import metrics from '@overleaf/metrics'
import Notifications from './Notifications.js' import Notifications from './Notifications.js'
import { expressify } from '@overleaf/promise-utils'
async function getUserNotifications(req, res, next) {
logger.debug(
{ userId: req.params.user_id },
'getting user unread notifications'
)
metrics.inc('getUserNotifications')
const notifications = await Notifications.getUserNotifications(
req.params.user_id
)
res.json(notifications)
}
async function addNotification(req, res) {
logger.debug(
{ userId: req.params.user_id, notification: req.body },
'adding notification'
)
metrics.inc('addNotification')
try {
await Notifications.addNotification(req.params.user_id, req.body)
res.sendStatus(200)
} catch (err) {
res.sendStatus(500)
}
}
async function removeNotificationId(req, res) {
logger.debug(
{
userId: req.params.user_id,
notificationId: req.params.notification_id,
},
'mark id notification as read'
)
metrics.inc('removeNotificationId')
await Notifications.removeNotificationId(
req.params.user_id,
req.params.notification_id
)
res.sendStatus(200)
}
async function removeNotificationKey(req, res) {
logger.debug(
{ userId: req.params.user_id, notificationKey: req.body.key },
'mark key notification as read'
)
metrics.inc('removeNotificationKey')
await Notifications.removeNotificationKey(req.params.user_id, req.body.key)
res.sendStatus(200)
}
async function removeNotificationByKeyOnly(req, res) {
const notificationKey = req.params.key
logger.debug({ notificationKey }, 'mark notification as read by key only')
metrics.inc('removeNotificationKey')
await Notifications.removeNotificationByKeyOnly(notificationKey)
res.sendStatus(200)
}
async function countNotificationsByKeyOnly(req, res) {
const notificationKey = req.params.key
try {
const count =
await Notifications.countNotificationsByKeyOnly(notificationKey)
res.json({ count })
} catch (err) {
logger.err({ err, notificationKey }, 'cannot count by key')
res.sendStatus(500)
}
}
async function deleteUnreadNotificationsByKeyOnlyBulk(req, res) {
const notificationKey = req.params.key
try {
const count =
await Notifications.deleteUnreadNotificationsByKeyOnlyBulk(
notificationKey
)
res.json({ count })
} catch (err) {
logger.err({ err, notificationKey }, 'cannot bulk remove by key')
res.sendStatus(500)
}
}
export default { export default {
getUserNotifications(req, res, next) { getUserNotifications: expressify(getUserNotifications),
logger.debug( addNotification: expressify(addNotification),
{ userId: req.params.user_id }, deleteUnreadNotificationsByKeyOnlyBulk: expressify(
'getting user unread notifications' deleteUnreadNotificationsByKeyOnlyBulk
) ),
metrics.inc('getUserNotifications') removeNotificationByKeyOnly: expressify(removeNotificationByKeyOnly),
return Notifications.getUserNotifications( removeNotificationId: expressify(removeNotificationId),
req.params.user_id, removeNotificationKey: expressify(removeNotificationKey),
(err, notifications) => { countNotificationsByKeyOnly: expressify(countNotificationsByKeyOnly),
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 })
}
)
},
} }
+2 -3
View File
@@ -19,6 +19,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@overleaf/fetch-utils": "*",
"@overleaf/logger": "*", "@overleaf/logger": "*",
"@overleaf/metrics": "*", "@overleaf/metrics": "*",
"@overleaf/mongo-utils": "*", "@overleaf/mongo-utils": "*",
@@ -29,11 +30,9 @@
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"express": "^4.21.2", "express": "^4.21.2",
"method-override": "^3.0.0", "method-override": "^3.0.0",
"mongodb-legacy": "6.1.3", "mongodb-legacy": "6.1.3"
"request": "^2.88.2"
}, },
"devDependencies": { "devDependencies": {
"@types/request": "^2.48.12",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "^3.2.4" "vitest": "^3.2.4"
@@ -38,7 +38,7 @@ describe('Notifications Tests', () => {
ObjectId, ObjectId,
})) }))
notifications = await import(modulePath) notifications = (await import(modulePath)).default
stubbedNotification = { stubbedNotification = {
user_id: new ObjectId(userId), user_id: new ObjectId(userId),
@@ -23,7 +23,9 @@ describe('Notifications Controller', () => {
removeNotificationKey: vi.fn(), removeNotificationKey: vi.fn(),
} }
vi.doMock('../../../app/js/Notifications', () => notifications) vi.doMock('../../../app/js/Notifications', () => ({
default: notifications,
}))
vi.doMock('@overleaf/metrics', () => ({ vi.doMock('@overleaf/metrics', () => ({
default: { default: {