Skip to content

Commit

Permalink
#3074-refactored notifications to separate router
Browse files Browse the repository at this point in the history
  • Loading branch information
caiodasilva2005 committed Dec 21, 2024
1 parent fd4f299 commit 0673cdf
Show file tree
Hide file tree
Showing 16 changed files with 197 additions and 182 deletions.
30 changes: 30 additions & 0 deletions src/backend/src/controllers/notifications.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,34 @@ export default class NotificationsController {
next(error);
}
}

static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadNotifications = await NotificationsService.getUserUnreadNotifications(
currentUser.userId,
organization.organizationId
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async removeUserNotification(req: Request, res: Response, next: NextFunction) {
try {
const { notificationId } = req.body;
const { organization, currentUser } = req;

const unreadNotifications = await NotificationsService.removeUserNotification(
currentUser.userId,
notificationId,
organization.organizationId
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}
}
27 changes: 0 additions & 27 deletions src/backend/src/controllers/users.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,33 +192,6 @@ export default class UsersController {
}
}

static async getUserUnreadNotifications(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;

const unreadNotifications = await UsersService.getUserUnreadNotifications(currentUser.userId, organization);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async removeUserNotification(req: Request, res: Response, next: NextFunction) {
try {
const { notificationId } = req.body;
const { organization, currentUser } = req;

const unreadNotifications = await UsersService.removeUserNotification(
currentUser.userId,
notificationId,
organization
);
res.status(200).json(unreadNotifications);
} catch (error: unknown) {
next(error);
}
}

static async getUserUnreadAnnouncements(req: Request, res: Response, next: NextFunction) {
try {
const { organization, currentUser } = req;
Expand Down
8 changes: 8 additions & 0 deletions src/backend/src/routes/notifications.routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import express from 'express';
import NotificationsController from '../controllers/notifications.controllers';
import { nonEmptyString } from '../utils/validation.utils';
import { body } from 'express-validator';

const notificationsRouter = express.Router();

notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications);
notificationsRouter.get('/current-user', NotificationsController.getUserUnreadNotifications);
notificationsRouter.post(
'/curent-user/remove',
nonEmptyString(body('notificationId')),
NotificationsController.removeUserNotification
);

export default notificationsRouter;
2 changes: 0 additions & 2 deletions src/backend/src/routes/users.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ userRouter.post(
validateInputs,
UsersController.getManyUserTasks
);
userRouter.get('/notifications/current-user', UsersController.getUserUnreadNotifications);
userRouter.get('/announcements/current-user', UsersController.getUserUnreadAnnouncements);
userRouter.post('/notifications/remove', nonEmptyString(body('notificationId')), UsersController.removeUserNotification);

export default userRouter;
46 changes: 46 additions & 0 deletions src/backend/src/services/notifications.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,52 @@ export default class NotificationsService {
await Promise.all(promises);
}

/**
* Gets all of a user's unread notifications
* @param userId id of user to get unread notifications from
* @param organization the user's orgainzation
* @returns the unread notifications of the user
*/
static async getUserUnreadNotifications(userId: string, organizationId: string) {
const unreadNotifications = await prisma.notification.findMany({
where: {
users: {
some: { userId }
}
},
...getNotificationQueryArgs(organizationId)
});

if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found');

return unreadNotifications.map(notificationTransformer);
}

/**
* Removes a notification from the user's unread notifications
* @param userId id of the current user
* @param notificationId id of the notification to remove
* @param organization the user's organization
* @returns the user's updated unread notifications
*/
static async removeUserNotification(userId: string, notificationId: string, organizationId: string) {
const updatedUser = await prisma.user.update({
where: { userId },
data: {
unreadNotifications: {
disconnect: {
notificationId
}
}
},
include: { unreadNotifications: getNotificationQueryArgs(organizationId) }
});

if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`);

return updatedUser.unreadNotifications.map(notificationTransformer);
}

/**
* Creates and sends a notification to all users with the given userIds
* @param text writing in the notification
Expand Down
48 changes: 0 additions & 48 deletions src/backend/src/services/users.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ import { getAuthUserQueryArgs } from '../prisma-query-args/auth-user.query-args'
import authenticatedUserTransformer from '../transformers/auth-user.transformer';
import { getTaskQueryArgs } from '../prisma-query-args/tasks.query-args';
import taskTransformer from '../transformers/tasks.transformer';
import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args';
import notificationTransformer from '../transformers/notifications.transformer';
import { getAnnouncementQueryArgs } from '../prisma-query-args/announcements.query.args';
import announcementTransformer from '../transformers/announcements.transformer';

Expand Down Expand Up @@ -571,27 +569,6 @@ export default class UsersService {
return resolvedTasks.flat();
}

/**
* Gets all of a user's unread notifications
* @param userId id of user to get unread notifications from
* @param organization the user's orgainzation
* @returns the unread notifications of the user
*/
static async getUserUnreadNotifications(userId: string, organization: Organization) {
const unreadNotifications = await prisma.notification.findMany({
where: {
users: {
some: { userId }
}
},
...getNotificationQueryArgs(organization.organizationId)
});

if (!unreadNotifications) throw new HttpException(404, 'User Unread Notifications Not Found');

return unreadNotifications.map(notificationTransformer);
}

/**
* Gets all of a user's unread announcements
* @param userId id of the current user
Expand All @@ -612,29 +589,4 @@ export default class UsersService {

return unreadAnnouncements.map(announcementTransformer);
}

/**
* Removes a notification from the user's unread notifications
* @param userId id of the current user
* @param notificationId id of the notification to remove
* @param organization the user's organization
* @returns the user's updated unread notifications
*/
static async removeUserNotification(userId: string, notificationId: string, organization: Organization) {
const updatedUser = await prisma.user.update({
where: { userId },
data: {
unreadNotifications: {
disconnect: {
notificationId
}
}
},
include: { unreadNotifications: getNotificationQueryArgs(organization.organizationId) }
});

if (!updatedUser) throw new HttpException(404, `Failed to remove notication: ${notificationId}`);

return updatedUser.unreadNotifications.map(notificationTransformer);
}
}
2 changes: 1 addition & 1 deletion src/backend/src/utils/auth.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const requireJwtDev = (req: Request, res: Response, next: NextFunction) =
) {
next();
} else if (
req.path.startsWith('/notifications') // task deadline notification endpoint
req.path.startsWith('/notifications/taskdeadlines') // task deadline notification endpoint
) {
notificationEndpointAuth(req, res, next);
} else {
Expand Down
6 changes: 1 addition & 5 deletions src/backend/src/utils/notifications.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,7 @@ export const sendHomeCrReviewedNotification = async (
accepted: boolean,
organizationId: string
) => {
const isProd = process.env.NODE_ENV === 'production';

const changeRequestLink = isProd
? `https://finishlinebyner.com/change-requests/${changeRequest.crId}`
: `http://localhost:3000/change-requests/${changeRequest.crId}`;
const changeRequestLink = `/change-requests/${changeRequest.crId}`;
await NotificationsService.sendNotifcationToUsers(
`CR #${changeRequest.identifier} has been ${accepted ? 'approved!' : 'denied.'}`,
accepted ? 'check_circle' : 'cancel',
Expand Down
43 changes: 43 additions & 0 deletions src/backend/tests/unmocked/notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,47 @@ describe('Notifications Tests', () => {
expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification');
});
});

describe('Get Notifications', () => {
it('Succeeds and gets user notifications', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId);

const notifications = await NotificationService.getUserUnreadNotifications(
testBatman.userId,
organization.organizationId
);

expect(notifications).toHaveLength(2);
expect(notifications[0].text).toBe('test1');
expect(notifications[1].text).toBe('test2');
});
});

describe('Remove Notifications', () => {
it('Succeeds and removes user notification', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
await NotificationService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId);

const notifications = await NotificationService.getUserUnreadNotifications(
testBatman.userId,
organization.organizationId
);

expect(notifications).toHaveLength(2);
expect(notifications[0].text).toBe('test1');
expect(notifications[1].text).toBe('test2');

const updatedNotifications = await NotificationService.removeUserNotification(
testBatman.userId,
notifications[0].notificationId,
organization.organizationId
);

expect(updatedNotifications).toHaveLength(1);
expect(updatedNotifications[0].text).toBe('test2');
});
});
});
38 changes: 0 additions & 38 deletions src/backend/tests/unmocked/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { createTestOrganization, createTestTask, createTestUser, resetUsers } fr
import { batmanAppAdmin } from '../test-data/users.test-data';
import UsersService from '../../src/services/users.services';
import { NotFoundException } from '../../src/utils/errors.utils';
import NotificationsService from '../../src/services/notifications.services';
import AnnouncementService from '../../src/services/announcement.service';

describe('User Tests', () => {
Expand Down Expand Up @@ -51,43 +50,6 @@ describe('User Tests', () => {
});
});

describe('Get Notifications', () => {
it('Succeeds and gets user notifications', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId);

const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization);

expect(notifications).toHaveLength(2);
expect(notifications[0].text).toBe('test1');
expect(notifications[1].text).toBe('test2');
});
});

describe('Remove Notifications', () => {
it('Succeeds and removes user notification', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await NotificationsService.sendNotifcationToUsers('test1', 'test1', [testBatman.userId], orgId);
await NotificationsService.sendNotifcationToUsers('test2', 'test2', [testBatman.userId], orgId);

const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization);

expect(notifications).toHaveLength(2);
expect(notifications[0].text).toBe('test1');
expect(notifications[1].text).toBe('test2');

const updatedNotifications = await UsersService.removeUserNotification(
testBatman.userId,
notifications[0].notificationId,
organization
);

expect(updatedNotifications).toHaveLength(1);
expect(updatedNotifications[0].text).toBe('test2');
});
});

describe('Get Announcements', () => {
it('Succeeds and gets user announcements', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
Expand Down
19 changes: 19 additions & 0 deletions src/frontend/src/apis/notifications.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import axios from 'axios';
import { apiUrls } from '../utils/urls';
import { Notification } from 'shared';

/*
* Gets all unread notifications of the user with the given id
*/
export const getNotifications = () => {
return axios.get<Notification[]>(apiUrls.notificationsCurrentUser(), {
transformResponse: (data) => JSON.parse(data)
});
};

/*
* Removes a notification from the user with the given id
*/
export const removeNotification = (notificationId: string) => {
return axios.post<Notification[]>(apiUrls.notificationsRemoveCurrentUser(), { notificationId });
};
17 changes: 0 additions & 17 deletions src/frontend/src/apis/users.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import axios from '../utils/axios';
import {
Notification,
Project,
SetUserScheduleSettingsPayload,
Task,
Expand Down Expand Up @@ -160,19 +159,3 @@ export const getManyUserTasks = (userIds: string[]) => {
}
);
};

/*
* Gets all unread notifications of the user with the given id
*/
export const getNotifications = () => {
return axios.get<Notification[]>(apiUrls.userNotifications(), {
transformResponse: (data) => JSON.parse(data)
});
};

/*
* Removes a notification from the user with the given id
*/
export const removeNotification = (notificationId: string) => {
return axios.post<Notification[]>(apiUrls.userRemoveNotifications(), { notificationId });
};
4 changes: 2 additions & 2 deletions src/frontend/src/components/NotificationAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Box } from '@mui/material';
import React, { useEffect, useState } from 'react';
import { Notification } from 'shared';
import NotificationCard from './NotificationCard';
import { useRemoveUserNotification, useUserNotifications } from '../hooks/users.hooks';
import { useHistory } from 'react-router-dom';
import { useCurrentUserNotifications, useRemoveUserNotification } from '../hooks/notifications.hooks';

const NotificationAlert: React.FC = () => {
const { data: notifications, isLoading: notificationsIsLoading } = useUserNotifications();
const { data: notifications, isLoading: notificationsIsLoading } = useCurrentUserNotifications();
const { mutateAsync: removeNotification, isLoading: removeIsLoading } = useRemoveUserNotification();
const [currentNotification, setCurrentNotification] = useState<Notification>();
const history = useHistory();
Expand Down
Loading

0 comments on commit 0673cdf

Please sign in to comment.