diff --git a/src/backend/src/controllers/users.controllers.ts b/src/backend/src/controllers/users.controllers.ts index 39b2b77a4a..896648bd4c 100644 --- a/src/backend/src/controllers/users.controllers.ts +++ b/src/backend/src/controllers/users.controllers.ts @@ -203,16 +203,4 @@ export default class UsersController { return next(error); } } - - static async sendNotitifcation(req: Request, res: Response, next: NextFunction) { - try { - const { userId } = req.params; - const { text, iconName } = req.body; - - const updatedUser = await UsersService.sendNotification(userId, text, iconName); - return res.status(200).json(updatedUser); - } catch (error: unknown) { - return next(error); - } - } } diff --git a/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql new file mode 100644 index 0000000000..192243b385 --- /dev/null +++ b/src/backend/src/prisma/migrations/20241122155216_updated_notifications/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Notification` table. All the data in the column will be lost. + - Added the required column `text` to the `Announcement` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Notification" DROP CONSTRAINT "Notification_userId_fkey"; + +-- AlterTable +ALTER TABLE "Announcement" ADD COLUMN "text" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Notification" DROP COLUMN "userId"; + +-- CreateTable +CREATE TABLE "_ReceivedAnnouncements" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_NotificationToUser" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_ReceivedAnnouncements_AB_unique" ON "_ReceivedAnnouncements"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ReceivedAnnouncements_B_index" ON "_ReceivedAnnouncements"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_NotificationToUser_AB_unique" ON "_NotificationToUser"("A", "B"); + +-- CreateIndex +CREATE INDEX "_NotificationToUser_B_index" ON "_NotificationToUser"("B"); + +-- AddForeignKey +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; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Notification"("notificationId") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_NotificationToUser" ADD CONSTRAINT "_NotificationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/backend/src/routes/users.routes.ts b/src/backend/src/routes/users.routes.ts index 858e514179..34ae1a0136 100644 --- a/src/backend/src/routes/users.routes.ts +++ b/src/backend/src/routes/users.routes.ts @@ -55,11 +55,5 @@ userRouter.post( UsersController.getManyUserTasks ); userRouter.get('/:userId/notifications', UsersController.getUserUnreadNotifications); -userRouter.post( - `/:userId/notifications/send`, - nonEmptyString(body('text')), - nonEmptyString(body('iconName')), - UsersController.sendNotitifcation -); export default userRouter; diff --git a/src/backend/src/services/users.services.ts b/src/backend/src/services/users.services.ts index fa63a09625..0b8e06b096 100644 --- a/src/backend/src/services/users.services.ts +++ b/src/backend/src/services/users.services.ts @@ -583,27 +583,4 @@ export default class UsersService { return requestedUser.unreadNotifications.map(notificationTransformer); } - - static async sendNotification(userId: string, text: string, iconName: string) { - const requestedUser = await prisma.user.findUnique({ - where: { userId } - }); - - if (!requestedUser) throw new NotFoundException('User', userId); - - const createdNotification = await prisma.notification.create({ - data: { - text, - iconName - } - }); - - const udaptedUser = await prisma.user.update({ - where: { userId: requestedUser.userId }, - data: { unreadNotifications: { connect: createdNotification } }, - include: { unreadNotifications: true } - }); - - return udaptedUser; - } } diff --git a/src/backend/src/transformers/notification.transformer.ts b/src/backend/src/transformers/notification.transformer.ts index 24e4347b20..32666b151a 100644 --- a/src/backend/src/transformers/notification.transformer.ts +++ b/src/backend/src/transformers/notification.transformer.ts @@ -4,6 +4,7 @@ import { Notification } from 'shared'; const notificationTransformer = (notification: Prisma.NotificationGetPayload): Notification => { return { + notificationId: notification.notificationId, text: notification.text, iconName: notification.iconName }; diff --git a/src/backend/src/utils/homepage-notifications.utils.ts b/src/backend/src/utils/homepage-notifications.utils.ts new file mode 100644 index 0000000000..f795064c28 --- /dev/null +++ b/src/backend/src/utils/homepage-notifications.utils.ts @@ -0,0 +1,37 @@ +import { getNotificationQueryArgs } from '../prisma-query-args/notifications.query-args'; +import prisma from '../prisma/prisma'; +import notificationTransformer from '../transformers/notification.transformer'; +import { NotFoundException } from './errors.utils'; + +const sendNotificationToUser = async (userId: string, notificationId: string, organizationId: string) => { + const requestedUser = await prisma.user.findUnique({ + where: { userId } + }); + + if (!requestedUser) throw new NotFoundException('User', userId); + + const updatedUser = await prisma.user.update({ + where: { userId: requestedUser.userId }, + data: { unreadNotifications: { connect: { notificationId } } }, + include: { unreadNotifications: getNotificationQueryArgs(organizationId) } + }); + + return updatedUser.unreadNotifications.map(notificationTransformer); +}; + +export const sendNotificationToUsers = async (userIds: string[], text: string, iconName: string, organizationId: string) => { + const createdNotification = await prisma.notification.create({ + data: { + text, + iconName + }, + ...getNotificationQueryArgs(organizationId) + }); + + const notificationPromises = userIds.map(async (userId) => { + return sendNotificationToUser(userId, createdNotification.notificationId, organizationId); + }); + + await Promise.all(notificationPromises); + 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..eea07b47e4 --- /dev/null +++ b/src/backend/tests/unmocked/notifications.test.ts @@ -0,0 +1,49 @@ +import { Organization } from '@prisma/client'; +import { createTestOrganization, createTestUser, resetUsers } from '../test-utils'; +import { batmanAppAdmin, wonderwomanGuest } from '../test-data/users.test-data'; +import { NotFoundException } from '../../src/utils/errors.utils'; +import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; +import prisma from '../../src/prisma/prisma'; + +describe('Notification 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 sendNotificationToUsers(['1'], 'test', 'test', orgId)).rejects.toThrow( + new NotFoundException('User', '1') + ); + }); + + it('Succeeds and sends notification to user', async () => { + const testBatman = await createTestUser(batmanAppAdmin, orgId); + const testWonderWoman = await createTestUser(wonderwomanGuest, orgId); + + const notification = await sendNotificationToUsers([testBatman.userId, testWonderWoman.userId], 'test', 'icon', orgId); + + const batmanWithNotifications = await prisma.user.findUnique({ + where: { userId: testBatman.userId }, + include: { unreadNotifications: true } + }); + const wonderWomanWithNotifications = await prisma.user.findUnique({ + where: { userId: testWonderWoman.userId }, + include: { unreadNotifications: true } + }); + + expect(batmanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(batmanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + + expect(wonderWomanWithNotifications?.unreadNotifications).toHaveLength(1); + expect(wonderWomanWithNotifications?.unreadNotifications[0]).toStrictEqual(notification); + }); + }); +}); diff --git a/src/backend/tests/unmocked/users.test.ts b/src/backend/tests/unmocked/users.test.ts index 80e5c13b2d..fa43dda9db 100644 --- a/src/backend/tests/unmocked/users.test.ts +++ b/src/backend/tests/unmocked/users.test.ts @@ -3,7 +3,7 @@ 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 prisma from '../../src/prisma/prisma'; +import { sendNotificationToUsers } from '../../src/utils/homepage-notifications.utils'; describe('User Tests', () => { let orgId: string; @@ -50,29 +50,6 @@ describe('User Tests', () => { }); }); - describe('Send Notification', () => { - it('fails on invalid user id', async () => { - await expect(async () => await UsersService.sendNotification('1', 'test', 'test')).rejects.toThrow( - new NotFoundException('User', '1') - ); - }); - - it('Succeeds and sends notification to user', async () => { - const testBatman = await createTestUser(batmanAppAdmin, orgId); - await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); - await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); - - const batmanWithNotifications = await prisma.user.findUnique({ - where: { userId: testBatman.userId }, - include: { unreadNotifications: true } - }); - - expect(batmanWithNotifications?.unreadNotifications).toHaveLength(2); - expect(batmanWithNotifications?.unreadNotifications[0].text).toBe('test1'); - expect(batmanWithNotifications?.unreadNotifications[1].text).toBe('test2'); - }); - }); - describe('Get Notifications', () => { it('fails on invalid user id', async () => { await expect(async () => await UsersService.getUserUnreadNotifications('1', organization)).rejects.toThrow( @@ -82,8 +59,8 @@ describe('User Tests', () => { it('Succeeds and gets user notifications', async () => { const testBatman = await createTestUser(batmanAppAdmin, orgId); - await UsersService.sendNotification(testBatman.userId, 'test1', 'test1'); - await UsersService.sendNotification(testBatman.userId, 'test2', 'test2'); + await sendNotificationToUsers([testBatman.userId], 'test1', 'test1', orgId); + await sendNotificationToUsers([testBatman.userId], 'test2', 'test2', orgId); const notifications = await UsersService.getUserUnreadNotifications(testBatman.userId, organization); diff --git a/src/shared/src/types/notification.types.ts b/src/shared/src/types/notification.types.ts index 2d7260d35f..e4419ef2ed 100644 --- a/src/shared/src/types/notification.types.ts +++ b/src/shared/src/types/notification.types.ts @@ -1,4 +1,5 @@ export interface Notification { - text: String; - iconName: String; + notificationId: string; + text: string; + iconName: string; }