Skip to content

NCTO-155-make-notifications-system#17

Open
Notgoyome wants to merge 12 commits intomainfrom
NCTO-155-make-notifications-system
Open

NCTO-155-make-notifications-system#17
Notgoyome wants to merge 12 commits intomainfrom
NCTO-155-make-notifications-system

Conversation

@Notgoyome
Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Notification Prisma model + migration and linked it to User.
  • 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.

Comment on lines +25 to +27
let notificationWss: WebSocketServer | undefined;
const userSockets = new Map<number, Set<WebSocket>>();

Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +56
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;
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests dont work for now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)) {
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
if (!requestUrl.pathname.startsWith(NOTIFICATION_SOCKET_PATH)) {
if (!requestUrl.pathname.startsWith(NOTIFICATION_SOCKET_PATH)) {
socket.destroy();

Copilot uses AI. Check for mistakes.
@Notgoyome Notgoyome requested a review from Copilot March 1, 2026 02:42
@Naucto Naucto deleted a comment from Copilot AI Mar 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Notgoyome Notgoyome force-pushed the NCTO-155-make-notifications-system branch from a6cf0cd to 4f4d5de Compare March 1, 2026 03:00
@Naucto Naucto deleted a comment from Copilot AI Mar 1, 2026
@Naucto Naucto deleted a comment from Copilot AI Mar 1, 2026
@Notgoyome Notgoyome force-pushed the NCTO-155-make-notifications-system branch from 8bc7715 to fe1db69 Compare March 1, 2026 05:35
@Notgoyome Notgoyome force-pushed the NCTO-155-make-notifications-system branch from fe1db69 to d72ca5c Compare March 1, 2026 05:41
userId Int
title String
message String
type String
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda pedantic but notification.id.toString() would be better in the context that toString() is used everywhere else, but it's not that important

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that doing String(...) over toString is clearer imho

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

export class NotificationsService {
constructor(private readonly prisma: PrismaService) {}

async getUserNotifications(userId: number): Promise<NotificationPayload[]> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is a MAX_NOTIFICATIONS_PER_USER, this is ok-ish if the notifications aren't that big in size

@alexis-belmonte
Copy link
Copy Markdown
Contributor

Needed by #14

src/main.ts Outdated
const notificationsService = app.get(NotificationsService);
const jwtSecret = configService.getOrThrow<string>("JWT_SECRET");

setupWebSocketServer(server);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be refactored to something clearer like setupCollaborationSocket

}
};

const authenticateToken = (token: string, jwtSecret: string): number | null => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +21 to +56
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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

userId Int
title String
message String
type String
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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[]> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmh

createdAt: Date;
}): NotificationPayload {
return {
id: String(notification.id),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that doing String(...) over toString is clearer imho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants