Conversation
There was a problem hiding this comment.
Pull request overview
Implements a backend notifications system backed by Prisma (persisted notifications) and a WebSocket channel to push notifications to connected clients, wired into the NestJS application.
Changes:
- Added
NotificationPrisma model + migration and linked it toUser. - Added
NotificationsService+ module + controller endpoints to create/test and mark notifications as read. - Added a dedicated WebSocket server (
/socket/notifications) that authenticates via JWT and emits init + realtime notification messages.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/notifications/notifications.types.ts | Adds shared types for notification payloads and creation input. |
| src/notifications/notifications.socket.ts | Implements notification WebSocket server, auth, connection tracking, and emit helper. |
| src/notifications/notifications.service.ts | Adds Prisma-backed CRUD-like operations (list, create+prune, mark read). |
| src/notifications/notifications.module.ts | Registers controller/service and exports the service. |
| src/notifications/notifications.controller.ts | Adds REST endpoints for test notification + mark-as-read operations. |
| src/notifications/dto/notification-test.dto.ts | Adds DTO validation + Swagger metadata for test notification creation. |
| src/main.ts | Wires notification WebSocket setup into server bootstrap. |
| src/collab/signaling/signal.ts | Scopes WebRTC upgrade handling to avoid clashing with other WS endpoints. |
| src/app.module.ts | Registers the new NotificationsModule in the app. |
| prisma/models/user.prisma | Adds User ↔ Notification relation. |
| prisma/models/notification.prisma | Defines Notification model schema and indexes. |
| prisma/migrations/20260222071209_add_notifications/migration.sql | Creates Notification table, indexes, and FK. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let notificationWss: WebSocketServer | undefined; | ||
| const userSockets = new Map<number, Set<WebSocket>>(); | ||
|
|
There was a problem hiding this comment.
This WebSocket implementation stores active user connections in an in-memory Map. In a multi-instance/auto-scaling deployment, notifications will only reach clients connected to the same node that emitted them, leading to inconsistent delivery. If horizontal scaling is expected, consider a shared pub/sub (e.g., Redis) and/or a dedicated gateway layer.
| async createNotification(input: CreateNotificationInput): Promise<NotificationPayload> { | ||
| const created = await this.prisma.$transaction(async (tx) => { | ||
| const notification = await tx.notification.create({ | ||
| data: { | ||
| userId: input.userId, | ||
| title: input.title, | ||
| message: input.message, | ||
| type: input.type, | ||
| }, | ||
| }); | ||
|
|
||
| const extraNotifications = await tx.notification.findMany({ | ||
| where: { userId: input.userId }, | ||
| orderBy: [ | ||
| { createdAt: "desc" }, | ||
| { id: "desc" }, | ||
| ], | ||
| skip: MAX_NOTIFICATIONS_PER_USER, | ||
| select: { id: true }, | ||
| }); | ||
|
|
||
| if (extraNotifications.length > 0) { | ||
| await tx.notification.deleteMany({ | ||
| where: { | ||
| id: { in: extraNotifications.map((entry) => entry.id) }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return notification; | ||
| }); | ||
|
|
||
| const payload = this.toPayload(created); | ||
| emitNotificationToUser(input.userId, payload); | ||
| return payload; | ||
| } |
There was a problem hiding this comment.
NotificationsService introduces non-trivial behavior (transactional create + pruning, markAsRead ownership checks) but there are no unit tests validating these scenarios. Given the repo already uses Jest for services/controllers, please add tests covering creation/pruning limits and markAsRead/markAllAsRead behavior (including invalid ids and not-found cases).
There was a problem hiding this comment.
tests dont work for now
There was a problem hiding this comment.
They do btw, even if they break every now and then because of changes, you can create test and then run npm run test to see the tests.
There was a problem hiding this comment.
@Notgoyome I fixed them all in #14, just have to make sure that I didn't break stuff with the front and I'll do the merge once this is integrated
|
|
||
| server.on("upgrade", (request, socket, head) => { | ||
| const requestUrl = new URL(request.url ?? "/", "http://localhost"); | ||
| if (!requestUrl.pathname.startsWith(NOTIFICATION_SOCKET_PATH)) { |
There was a problem hiding this comment.
This upgrade handler returns early when the path doesn’t match, but it does not close/destroy the underlying TCP socket. If an Upgrade request comes in for an unexpected path (or if no other upgrade handler handles it), the connection can hang and leak resources. Consider routing upgrades centrally and calling socket.destroy() (or writing a 400/404 response) when no handler matches.
| if (!requestUrl.pathname.startsWith(NOTIFICATION_SOCKET_PATH)) { | |
| if (!requestUrl.pathname.startsWith(NOTIFICATION_SOCKET_PATH)) { | |
| socket.destroy(); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
a6cf0cd to
4f4d5de
Compare
8bc7715 to
fe1db69
Compare
fe1db69 to
d72ca5c
Compare
prisma/models/notification.prisma
Outdated
| userId Int | ||
| title String | ||
| message String | ||
| type String |
There was a problem hiding this comment.
Wouldn't it be better to have an enum / something different for the notification type ? If we keep it as a string, it might create to many very similar notification types while we could easily control what types we want for notifications
There was a problem hiding this comment.
I agree, this needs to be enum -- otherwise clients could send unknown notification types that just overload the system -- notification types should be checked when received
| createdAt: Date; | ||
| }): NotificationPayload { | ||
| return { | ||
| id: String(notification.id), |
There was a problem hiding this comment.
Kinda pedantic but notification.id.toString() would be better in the context that toString() is used everywhere else, but it's not that important
There was a problem hiding this comment.
I think that doing String(...) over toString is clearer imho
There was a problem hiding this comment.
That's also fair but then the change needs to be done in other places to keep it logical
| } | ||
| }; | ||
|
|
||
| const authenticateToken = (token: string, jwtSecret: string): number | null => { |
There was a problem hiding this comment.
This is kinda the case for this whole file, why are you recreating the whole JWT token verification ?
Use the JwtAuthGuards in the auth directory. This is a redo of an already existing middleware and could be a problem
| export class NotificationsService { | ||
| constructor(private readonly prisma: PrismaService) {} | ||
|
|
||
| async getUserNotifications(userId: number): Promise<NotificationPayload[]> { |
There was a problem hiding this comment.
Idk for what use the Notifications are going to be used, but wouldn't it be better, instead of using a notification cap, to implement pagination, as to be able to get older notifications and implementing the limit in the getUserNotifications method ? If it's not useful it's ok to do it like that
There was a problem hiding this comment.
Since there is a MAX_NOTIFICATIONS_PER_USER, this is ok-ish if the notifications aren't that big in size
|
Needed by #14 |
src/main.ts
Outdated
| const notificationsService = app.get(NotificationsService); | ||
| const jwtSecret = configService.getOrThrow<string>("JWT_SECRET"); | ||
|
|
||
| setupWebSocketServer(server); |
There was a problem hiding this comment.
Should be refactored to something clearer like setupCollaborationSocket
| } | ||
| }; | ||
|
|
||
| const authenticateToken = (token: string, jwtSecret: string): number | null => { |
| async createNotification(input: CreateNotificationInput): Promise<NotificationPayload> { | ||
| const created = await this.prisma.$transaction(async (tx) => { | ||
| const notification = await tx.notification.create({ | ||
| data: { | ||
| userId: input.userId, | ||
| title: input.title, | ||
| message: input.message, | ||
| type: input.type, | ||
| }, | ||
| }); | ||
|
|
||
| const extraNotifications = await tx.notification.findMany({ | ||
| where: { userId: input.userId }, | ||
| orderBy: [ | ||
| { createdAt: "desc" }, | ||
| { id: "desc" }, | ||
| ], | ||
| skip: MAX_NOTIFICATIONS_PER_USER, | ||
| select: { id: true }, | ||
| }); | ||
|
|
||
| if (extraNotifications.length > 0) { | ||
| await tx.notification.deleteMany({ | ||
| where: { | ||
| id: { in: extraNotifications.map((entry) => entry.id) }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return notification; | ||
| }); | ||
|
|
||
| const payload = this.toPayload(created); | ||
| emitNotificationToUser(input.userId, payload); | ||
| return payload; | ||
| } |
There was a problem hiding this comment.
@Notgoyome I fixed them all in #14, just have to make sure that I didn't break stuff with the front and I'll do the merge once this is integrated
prisma/models/notification.prisma
Outdated
| userId Int | ||
| title String | ||
| message String | ||
| type String |
There was a problem hiding this comment.
I agree, this needs to be enum -- otherwise clients could send unknown notification types that just overload the system -- notification types should be checked when received
| export class NotificationsService { | ||
| constructor(private readonly prisma: PrismaService) {} | ||
|
|
||
| async getUserNotifications(userId: number): Promise<NotificationPayload[]> { |
There was a problem hiding this comment.
Since there is a MAX_NOTIFICATIONS_PER_USER, this is ok-ish if the notifications aren't that big in size
| export class NotificationsController { | ||
| constructor(private readonly notificationsService: NotificationsService) {} | ||
|
|
||
| // TODO: Remove this endpoint after testing, or restrict it to admin users |
| createdAt: Date; | ||
| }): NotificationPayload { | ||
| return { | ||
| id: String(notification.id), |
There was a problem hiding this comment.
I think that doing String(...) over toString is clearer imho
Jira ticket
https://naucto.atlassian.net/jira/software/projects/NCTO/boards/2?selectedIssue=NCTO-155
What does your MR do ?
Implementing notifications system via websocket and storing notifications in backend
How to test it
swagger
Screenshot
Notes