diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 1411e440a8..8076e225d7 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -191,15 +191,4 @@ export default class UsersController { next(error); } } - - static async sendNotificationToUsers(req: Request, res: Response, next: NextFunction) { - try { - const { text, iconName, userIds } = req.body; - - const createdNotification = await UsersService.sendNotifcationToUsers(text, iconName, userIds, req.organization); - res.status(200).json(createdNotification); - } catch (error: unknown) { - next(error); - } - } } diff --git a/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql b/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql similarity index 70% rename from src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql rename to src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql index 30b887407d..c57afabd25 100644 --- a/src/backend/src/prisma/migrations/20241211185407_announcements_and_notifications/migration.sql +++ b/src/backend/src/prisma/migrations/20241211195435_announcements_and_notifications/migration.sql @@ -18,40 +18,40 @@ CREATE TABLE "Notification" ( ); -- CreateTable -CREATE TABLE "_ReceivedAnnouncements" ( +CREATE TABLE "_receivedAnnouncements" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateTable -CREATE TABLE "_UserNotifications" ( +CREATE TABLE "_userNotifications" ( "A" TEXT NOT NULL, "B" TEXT NOT NULL ); -- CreateIndex -CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); +CREATE UNIQUE INDEX "_receivedAnnouncements_AB_unique" ON "_receivedAnnouncements"("A", "B"); -- CreateIndex -CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); +CREATE INDEX "_receivedAnnouncements_B_index" ON "_receivedAnnouncements"("B"); -- CreateIndex -CREATE UNIQUE INDEX "_UserNotifications_AB_unique" ON "_UserNotifications"("A", "B"); +CREATE UNIQUE INDEX "_userNotifications_AB_unique" ON "_userNotifications"("A", "B"); -- CreateIndex -CREATE INDEX "_UserNotifications_B_index" ON "_UserNotifications"("B"); +CREATE INDEX "_userNotifications_B_index" ON "_userNotifications"("B"); -- AddForeignKey ALTER TABLE "Announcement" ADD CONSTRAINT "Announcement_userCreatedId_fkey" FOREIGN KEY ("userCreatedId") REFERENCES "User"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_A_fkey" FOREIGN KEY ("A") REFERENCES "Announcement"("announcementId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_ReceivedAnnouncements" ADD CONSTRAINT "_ReceivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_receivedAnnouncements" ADD CONSTRAINT "_receivedAnnouncements_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_UserNotifications" ADD CONSTRAINT "_UserNotifications_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "_UserNotifications" ADD CONSTRAINT "_UserNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index 0f010392ea..c06ca3d45f 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -180,9 +180,9 @@ model User { deletedFrequentlyAskedQuestions FrequentlyAskedQuestion[] @relation(name: "frequentlyAskedQuestionDeleter") createdMilestones Milestone[] @relation(name: "milestoneCreator") deletedMilestones Milestone[] @relation(name: "milestoneDeleter") - receivedAnnouncements Announcement[] @relation(name: "ReceivedAnnouncements") - createdAnnouncements Announcement[] @relation(name: "CreatedAnnouncements") - unreadNotifications Notification[] @relation(name: "UserNotifications") + receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements") + createdAnnouncements Announcement[] @relation(name: "createdAnnouncements") + unreadNotifications Notification[] @relation(name: "userNotifications") } model Role { @@ -934,15 +934,15 @@ model Milestone { model Announcement { announcementId String @id @default(uuid()) text String - usersReceived User[] @relation("ReceivedAnnouncements") + usersReceived User[] @relation("receivedAnnouncements") dateCrated DateTime userCreatedId String - userCreated User @relation("CreatedAnnouncements", fields: [userCreatedId], references: [userId]) + userCreated User @relation("createdAnnouncements", fields: [userCreatedId], references: [userId]) } model Notification { notificationId String @id @default(uuid()) text String iconName String - users User[] @relation("UserNotifications") + users User[] @relation("userNotifications") } diff --git a/src/backend/src/routes/notifications.routes.ts b/src/backend/src/routes/notifications.routes.ts index 4701b0f3ea..ff712d8b48 100644 --- a/src/backend/src/routes/notifications.routes.ts +++ b/src/backend/src/routes/notifications.routes.ts @@ -1,8 +1,19 @@ import express from 'express'; import NotificationsController from '../controllers/notifications.controllers'; +import { nonEmptyString, validateInputs } from '../utils/validation.utils'; +import { body } from 'express-validator'; const notificationsRouter = express.Router(); notificationsRouter.post('/task-deadlines', NotificationsController.sendDailySlackNotifications); +notificationsRouter.post( + '/send/users', + nonEmptyString(body('text')), + nonEmptyString(body('iconName')), + body('userIds').isArray(), + nonEmptyString(body('userIds.*')), + validateInputs, + NotificationsController.sendNotificationToUsers +); export default notificationsRouter; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 4d7fca68f2..2f95201f6f 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -55,14 +55,4 @@ userRouter.post( UsersController.getManyUserTasks ); -userRouter.post( - '/notifications/send/many', - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - body('userIds').isArray(), - nonEmptyString(body('userIds.*')), - validateInputs, - UsersController.sendNotificationToUsers -); - export default userRouter; diff --git a/src/backend/src/services/notifications.services.ts b/src/backend/src/services/notifications.services.ts index a443d93588..483e6ed9d6 100644 --- a/src/backend/src/services/notifications.services.ts +++ b/src/backend/src/services/notifications.services.ts @@ -11,8 +11,10 @@ import { daysBetween, startOfDay, wbsPipe } from 'shared'; import { buildDueString } from '../utils/slack.utils'; import WorkPackagesService from './work-packages.services'; import { addWeeksToDate } from 'shared'; -import { HttpException } from '../utils/errors.utils'; +import { HttpException, NotFoundException } from '../utils/errors.utils'; import { meetingStartTimePipe } from '../utils/design-reviews.utils'; +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import notificationTransformer from '../transformers/notifications.transformer'; export default class NotificationsService { static async sendDailySlackNotifications() { @@ -193,4 +195,44 @@ export default class NotificationsService { await Promise.all(promises); } + + /** + * Creates and sends a notification to all users with the given userIds + * @param text writing in the notification + * @param iconName icon that appears in the notification + * @param userIds ids of users to send the notification to + * @param organizationId + * @returns the created notification + */ + static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organizationId: string) { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + }, + ...getNotificationQueryArgs(organizationId) + }); + + if (!createdNotification) throw new HttpException(500, 'Failed to create notification'); + + const notificationsPromises = userIds.map(async (userId) => { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + return await prisma.user.update({ + where: { userId: requestedUser.userId }, + data: { + unreadNotifications: { + connect: { notificationId: createdNotification.notificationId } + } + } + }); + }); + + await Promise.all(notificationsPromises); + return notificationTransformer(createdNotification); + } } diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index b680811d1b..d786c04137 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -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 notificationTransformer from '../transformers/notifications.transformer'; -import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; export default class UsersService { /** @@ -568,44 +566,4 @@ export default class UsersService { const resolvedTasks = await Promise.all(tasksPromises); return resolvedTasks.flat(); } - - /** - * Creates and sends a notification to all users with the given userIds - * @param text writing in the notification - * @param iconName icon that appears in the notification - * @param userIds ids of users to send the notification to - * @param organization - * @returns the created notification - */ - static async sendNotifcationToUsers(text: string, iconName: string, userIds: string[], organization: Organization) { - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - }, - ...getNotificationQueryArgs(organization.organizationId) - }); - - if (!createdNotification) throw new HttpException(500, 'Failed to create notification'); - - const notificationsPromises = userIds.map(async (userId) => { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - return await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { - unreadNotifications: { - connect: { notificationId: createdNotification.notificationId } - } - } - }); - }); - - await Promise.all(notificationsPromises); - return notificationTransformer(createdNotification); - } } diff --git a/src/backend/tests/unmocked/notifications.test.ts b/src/backend/tests/unmocked/notifications.test.ts new file mode 100644 index 0000000000..d3cce68361 --- /dev/null +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -0,0 +1,59 @@ +import { Organization } from '@prisma/client'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; +import { NotFoundException } from '../../src/utils/errors.utils'; +import prisma from '../../src/prisma/prisma'; +import NotificationService from '../../src/services/notifications.services'; + +describe('Notifications Tests', () => { + let orgId: string; + let organization: Organization; + beforeEach(async () => { + organization = await createTestOrganization(); + orgId = organization.organizationId; + }); + + afterEach(async () => { + await resetUsers(); + }); + + describe('Send Notification', () => { + it('fails on invalid user id', async () => { + await expect( + async () => + await NotificationService.sendNotifcationToUsers( + 'test notification', + 'star', + ['1', '2'], + organization.organizationId + ) + ).rejects.toThrow(new NotFoundException('User', '1')); + }); + + it('Succeeds and sends notification to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + const testSuperman = await createTestUser(supermanAdmin, orgId); + await NotificationService.sendNotifcationToUsers( + 'test notification', + 'star', + [testBatman.userId, testSuperman.userId], + organization.organizationId + ); + + const batmanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + + const supermanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + + expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); + expect(supermanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 4d59d49dd1..c13a0c857f 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -1,9 +1,8 @@ import { Organization } from '@prisma/client'; import { createTestOrganization, createTestTask, createTestUser, resetUsers } from '../test-utils'; -import { batmanAppAdmin, supermanAdmin } from '../test-data/users.test-data'; +import { batmanAppAdmin } from '../test-data/users.test-data'; import UsersService from '../../src/services/users.services'; import { NotFoundException } from '../../src/utils/errors.utils'; -import prisma from '../../src/prisma/prisma'; describe('User Tests', () => { let orgId: string; @@ -49,38 +48,4 @@ describe('User Tests', () => { expect(userTasks).toStrictEqual([batmanTask, batmanTask]); }); }); - - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect( - async () => await UsersService.sendNotifcationToUsers('test notification', 'star', ['1', '2'], organization) - ).rejects.toThrow(new NotFoundException('User', '1')); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - const testSuperman = await createTestUser(supermanAdmin, orgId); - await UsersService.sendNotifcationToUsers( - 'test notification', - 'star', - [testBatman.userId, testSuperman.userId], - organization - ); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - const supermanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); - expect(supermanWithNotifications?.unreadNotifications).toHaveLength(1); - expect(supermanWithNotifications?.unreadNotifications[0].text).toBe('test notification'); - }); - }); });