mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
* [notifications] adopt strict zod types and extend test coverage * [notifications] validate expires date via schema GitOrigin-RevId: 25dee0578ec443447e4c38a50a7e6c8b44831fe7
242 lines
7.5 KiB
TypeScript
242 lines
7.5 KiB
TypeScript
import logger from '@overleaf/logger'
|
|
import metrics from '@overleaf/metrics'
|
|
import Notifications from './Notifications.js'
|
|
import { expressify } from '@overleaf/promise-utils'
|
|
import { parseReq, z, zz } from '@overleaf/validation-tools'
|
|
import type { Request, Response } from 'express'
|
|
|
|
const getUserNotificationsSchema = z.object({
|
|
params: z.strictObject({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const getUserNotificationsFallbackSchema = z.object({
|
|
params: z.object({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
})
|
|
|
|
async function getUserNotifications(req: Request, res: Response) {
|
|
const { params } = parseReq(req, getUserNotificationsSchema, {
|
|
fallbackSchema: getUserNotificationsFallbackSchema,
|
|
})
|
|
logger.debug({ userId: params.user_id }, 'getting user unread notifications')
|
|
metrics.inc('getUserNotifications')
|
|
const notifications = await Notifications.getUserNotifications(params.user_id)
|
|
res.json(notifications)
|
|
}
|
|
|
|
const addNotificationSchema = z.object({
|
|
params: z.strictObject({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
body: z.strictObject({
|
|
key: z.string(),
|
|
templateKey: z.string(),
|
|
// Arbitrary per-template render params: different notification types
|
|
// populate completely different keys here (see web's
|
|
// NotificationsBuilder.mjs, the only real production caller), so this is
|
|
// a genuinely open map rather than an approximated shape.
|
|
messageOpts: z.record(z.string(), z.unknown()).optional(),
|
|
// web's NotificationsHandler.createNotification always sends this (it
|
|
// defaults to `true` client-side before serializing), but
|
|
// Notifications.js already tolerates a missing value as falsy (skip
|
|
// overwriting an existing notification), so keep it optional here too.
|
|
forceCreate: z.boolean().optional(),
|
|
expires: z.iso.datetime().optional(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const addNotificationFallbackSchema = z.object({
|
|
params: z.object({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
body: z.looseObject({}),
|
|
})
|
|
|
|
async function addNotification(req: Request, res: Response) {
|
|
const { params, body } = parseReq(req, addNotificationSchema, {
|
|
fallbackSchema: addNotificationFallbackSchema,
|
|
})
|
|
logger.debug(
|
|
{ userId: params.user_id, notification: body },
|
|
'adding notification'
|
|
)
|
|
metrics.inc('addNotification')
|
|
try {
|
|
await Notifications.addNotification(params.user_id, body)
|
|
res.sendStatus(200)
|
|
} catch (err) {
|
|
res.sendStatus(500)
|
|
}
|
|
}
|
|
|
|
const removeNotificationIdSchema = z.object({
|
|
params: z.strictObject({
|
|
user_id: zz.objectId(),
|
|
notification_id: zz.objectId(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const removeNotificationIdFallbackSchema = z.object({
|
|
params: z.object({
|
|
user_id: zz.objectId(),
|
|
notification_id: zz.objectId(),
|
|
}),
|
|
})
|
|
|
|
async function removeNotificationId(req: Request, res: Response) {
|
|
const { params } = parseReq(req, removeNotificationIdSchema, {
|
|
fallbackSchema: removeNotificationIdFallbackSchema,
|
|
})
|
|
logger.debug(
|
|
{
|
|
userId: params.user_id,
|
|
notificationId: params.notification_id,
|
|
},
|
|
'mark id notification as read'
|
|
)
|
|
metrics.inc('removeNotificationId')
|
|
await Notifications.removeNotificationId(
|
|
params.user_id,
|
|
params.notification_id
|
|
)
|
|
res.sendStatus(200)
|
|
}
|
|
|
|
const removeNotificationKeySchema = z.object({
|
|
params: z.strictObject({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
body: z.strictObject({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const removeNotificationKeyFallbackSchema = z.object({
|
|
params: z.object({
|
|
user_id: zz.objectId(),
|
|
}),
|
|
body: z.object({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
|
|
async function removeNotificationKey(req: Request, res: Response) {
|
|
const { params, body } = parseReq(req, removeNotificationKeySchema, {
|
|
fallbackSchema: removeNotificationKeyFallbackSchema,
|
|
})
|
|
logger.debug(
|
|
{ userId: params.user_id, notificationKey: body.key },
|
|
'mark key notification as read'
|
|
)
|
|
metrics.inc('removeNotificationKey')
|
|
await Notifications.removeNotificationKey(params.user_id, body.key)
|
|
|
|
res.sendStatus(200)
|
|
}
|
|
|
|
const removeNotificationByKeyOnlySchema = z.object({
|
|
params: z.strictObject({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const removeNotificationByKeyOnlyFallbackSchema = z.object({
|
|
params: z.object({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
|
|
async function removeNotificationByKeyOnly(req: Request, res: Response) {
|
|
const { params } = parseReq(req, removeNotificationByKeyOnlySchema, {
|
|
fallbackSchema: removeNotificationByKeyOnlyFallbackSchema,
|
|
})
|
|
const notificationKey = params.key
|
|
logger.debug({ notificationKey }, 'mark notification as read by key only')
|
|
metrics.inc('removeNotificationKey')
|
|
await Notifications.removeNotificationByKeyOnly(notificationKey)
|
|
res.sendStatus(200)
|
|
}
|
|
|
|
const countNotificationsByKeyOnlySchema = z.object({
|
|
params: z.strictObject({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const countNotificationsByKeyOnlyFallbackSchema = z.object({
|
|
params: z.object({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
|
|
async function countNotificationsByKeyOnly(req: Request, res: Response) {
|
|
const { params } = parseReq(req, countNotificationsByKeyOnlySchema, {
|
|
fallbackSchema: countNotificationsByKeyOnlyFallbackSchema,
|
|
})
|
|
const notificationKey = 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)
|
|
}
|
|
}
|
|
|
|
const deleteUnreadNotificationsByKeyOnlyBulkSchema = z.object({
|
|
params: z.strictObject({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
// Rollout-temporary fallback (pre-refinement schema from main); delete
|
|
// when this route's REQ_VALIDATION_MODE instrumentation is removed.
|
|
const deleteUnreadNotificationsByKeyOnlyBulkFallbackSchema = z.object({
|
|
params: z.object({
|
|
key: z.string(),
|
|
}),
|
|
})
|
|
|
|
async function deleteUnreadNotificationsByKeyOnlyBulk(
|
|
req: Request,
|
|
res: Response
|
|
) {
|
|
const { params } = parseReq(
|
|
req,
|
|
deleteUnreadNotificationsByKeyOnlyBulkSchema,
|
|
{ fallbackSchema: deleteUnreadNotificationsByKeyOnlyBulkFallbackSchema }
|
|
)
|
|
const notificationKey = 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 {
|
|
getUserNotifications: expressify(getUserNotifications),
|
|
addNotification: expressify(addNotification),
|
|
deleteUnreadNotificationsByKeyOnlyBulk: expressify(
|
|
deleteUnreadNotificationsByKeyOnlyBulk
|
|
),
|
|
removeNotificationByKeyOnly: expressify(removeNotificationByKeyOnly),
|
|
removeNotificationId: expressify(removeNotificationId),
|
|
removeNotificationKey: expressify(removeNotificationKey),
|
|
countNotificationsByKeyOnly: expressify(countNotificationsByKeyOnly),
|
|
}
|