Skip to content

Commit

Permalink
Merge pull request #3035 from Northeastern-Electric-Racing/#2997-Caio…
Browse files Browse the repository at this point in the history
…-SendNotifications

#2997-Send Notifications
  • Loading branch information
Peyton-McKee authored Dec 11, 2024
2 parents d68b106 + d53dc2e commit 46c34ea
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 32 deletions.
11 changes: 11 additions & 0 deletions src/backend/src/prisma-query-args/notifications.query-args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Prisma } from '@prisma/client';
import { getUserQueryArgs } from './user.query-args';

export type NotificationQueryArgs = ReturnType<typeof getNotificationQueryArgs>;

export const getNotificationQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.NotificationDefaultArgs>()({
include: {
users: getUserQueryArgs(organizationId)
}
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "Announcement" (
"announcementId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"dateCrated" TIMESTAMP(3) NOT NULL,
"userCreatedId" TEXT NOT NULL,

CONSTRAINT "Announcement_pkey" PRIMARY KEY ("announcementId")
);

-- CreateTable
CREATE TABLE "Notification" (
"notificationId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"iconName" TEXT NOT NULL,

CONSTRAINT "Notification_pkey" PRIMARY KEY ("notificationId")
);

-- CreateTable
CREATE TABLE "_receivedAnnouncements" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateTable
CREATE TABLE "_userNotifications" (
"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 "_userNotifications_AB_unique" ON "_userNotifications"("A", "B");

-- CreateIndex
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;

-- AddForeignKey
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;

-- AddForeignKey
ALTER TABLE "_userNotifications" ADD CONSTRAINT "_userNotifications_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
12 changes: 6 additions & 6 deletions src/backend/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
receivedAnnouncements Announcement[] @relation(name: "receivedAnnouncements")
createdAnnouncements Announcement[] @relation(name: "createdAnnouncements")
unreadNotifications Notification[] @relation(name: "userNotifications")
}

model Role {
Expand Down Expand Up @@ -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[]
users User[] @relation("userNotifications")
}
44 changes: 43 additions & 1 deletion src/backend/src/services/notifications.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}
}
13 changes: 13 additions & 0 deletions src/backend/src/transformers/notifications.transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Prisma } from '@prisma/client';
import { NotificationQueryArgs } from '../prisma-query-args/notifications.query-args';
import { Notification } from 'shared';

const notificationTransformer = (notification: Prisma.NotificationGetPayload<NotificationQueryArgs>): Notification => {
return {
notificationId: notification.notificationId,
text: notification.text,
iconName: notification.iconName
};
};

export default notificationTransformer;
59 changes: 59 additions & 0 deletions src/backend/tests/unmocked/notifications.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
2 changes: 1 addition & 1 deletion src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export * from './src/types/team-types';
export * from './src/types/task-types';
export * from './src/types/reimbursement-requests-types';
export * from './src/types/design-review-types';

export * from './src/types/notifications.types';
export * from './src/validate-wbs';
export * from './src/date-utils';

Expand Down
5 changes: 5 additions & 0 deletions src/shared/src/types/notifications.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Notification {
notificationId: string;
text: string;
iconName: string;
}

0 comments on commit 46c34ea

Please sign in to comment.