diff --git a/src/routes/v2/authenticated/review.ts b/src/routes/v2/authenticated/review.ts index 37415f269..b0cb82e15 100644 --- a/src/routes/v2/authenticated/review.ts +++ b/src/routes/v2/authenticated/review.ts @@ -295,6 +295,70 @@ export class ReviewsRouter { return res.status(200).json() } + markReviewRequestAsViewed: RequestHandler< + { siteName: string; requestId: number }, + string | ResponseErrorBody, + never, + unknown, + { userWithSiteSessionData: UserWithSiteSessionData } + > = async (req, res) => { + // Step 1: Check that the site exists + const { siteName, requestId: prNumber } = req.params + const site = await this.sitesService.getBySiteName(siteName) + + if (!site) { + return res.status(404).send({ + message: "Please ensure that the site exists!", + }) + } + + // Step 2: Check that user exists. + // Having session data is proof that this user exists + // as otherwise, they would be rejected by our middleware + const { userWithSiteSessionData } = res.locals + + // Check if they are a collaborator + const role = await this.collaboratorsService.getRole( + siteName, + userWithSiteSessionData.isomerUserId + ) + + if (!role) { + return res.status(400).send({ + message: "User is not a collaborator of this site!", + }) + } + + // Step 3: Retrieve review request + const possibleReviewRequest = await this.reviewRequestService.getReviewRequest( + site, + prNumber + ) + + if (isIsomerError(possibleReviewRequest)) { + logger.error({ + message: "Invalid review request requested", + method: "markReviewRequestAsViewed", + meta: { + userId: userWithSiteSessionData.isomerUserId, + email: userWithSiteSessionData.email, + siteName, + prNumber, + }, + }) + return res.status(404).json({ message: possibleReviewRequest.message }) + } + + // Step 4: Mark review request as viewed + await this.reviewRequestService.markReviewRequestAsViewed( + userWithSiteSessionData, + site, + possibleReviewRequest.id + ) + + return res.status(200).json() + } + getReviewRequest: RequestHandler< { siteName: string; requestId: number }, { reviewRequest: ReviewRequestDto } | ResponseErrorBody, @@ -786,6 +850,10 @@ export class ReviewsRouter { "/:requestId", attachReadRouteHandlerWrapper(this.getReviewRequest) ) + router.post( + "/:requestId/viewed", + attachWriteRouteHandlerWrapper(this.markReviewRequestAsViewed) + ) router.post( "/:requestId/merge", attachWriteRouteHandlerWrapper(this.mergeReviewRequest) diff --git a/src/services/review/ReviewRequestService.ts b/src/services/review/ReviewRequestService.ts index e4fa863c7..78a767e07 100644 --- a/src/services/review/ReviewRequestService.ts +++ b/src/services/review/ReviewRequestService.ts @@ -285,18 +285,41 @@ export default class ReviewRequestService { const { isomerUserId: userId } = sessionData const { id: reviewRequestId } = reviewRequest - await this.reviewRequestView.update( - { - lastViewedAt: new Date(), + await this.reviewRequestView.upsert({ + reviewRequestId, + siteId: site.id, + userId, + lastViewedAt: new Date(), + }) + } + + markReviewRequestAsViewed = async ( + sessionData: UserWithSiteSessionData, + site: Site, + requestId: number + ): Promise => { + const { isomerUserId: userId } = sessionData + + const reviewRequestView = await this.reviewRequestView.findOne({ + where: { + siteId: site.id, + userId, + reviewRequestId: requestId, }, - { - where: { - reviewRequestId, - siteId: site.id, - userId, - }, - } - ) + }) + + // We only want to create the entry if it does not exist + // (i.e. the review request has never been viewed before) + if (!reviewRequestView) { + await this.reviewRequestView.create({ + reviewRequestId: requestId, + siteId: site.id, + userId, + // This field represents the user opening the review request + // itself, which the user has not done so yet at this stage. + lastViewedAt: null, + }) + } } deleteAllReviewRequestViews = async (