From fae38c427da45808bd538de5abc78e774d4ae7ac Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 20 Dec 2025 20:24:39 +0330 Subject: [PATCH] update notif , now it is cursor based --- .../controllers/notifications.controller.ts | 24 +++++---- .../entities/notification.entity.ts | 2 +- .../services/notification.service.ts | 53 ++++++++++++++----- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/modules/notifications/controllers/notifications.controller.ts b/src/modules/notifications/controllers/notifications.controller.ts index e7a9ca5..bbf6de2 100644 --- a/src/modules/notifications/controllers/notifications.controller.ts +++ b/src/modules/notifications/controllers/notifications.controller.ts @@ -8,6 +8,7 @@ import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard'; import { RestId } from '../../../common/decorators/rest-id.decorator'; import { UpdatePreferenceDto } from '../dto/update-preference.dto'; import { AdminId } from 'src/common/decorators/admin-id.decorator'; +import { API_HEADER_SLUG } from 'src/common/constants/index'; @ApiTags('notifications') @Controller() @@ -21,20 +22,23 @@ export class NotificationsController { @ApiBearerAuth() @Get('public/notifications') @ApiOperation({ summary: 'Get user restaurant notifications' }) - @ApiHeader({ - name: 'X-Slug', - required: true, - schema: { - type: 'string', - default: 'zhivan', - }, - }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - async getUserNotifications(@UserId() userId: string, @RestId() restaurantId: string, @Query('limit') limit?: number) { + @ApiHeader(API_HEADER_SLUG) + @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' }) + @ApiQuery({ name: 'cursor', required: false, type: String, description: 'Cursor for pagination (ID of the last item from previous page)' }) + @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' }) + async getUserNotifications( + @UserId() userId: string, + @RestId() restaurantId: string, + @Query('limit') limit?: number, + @Query('cursor') cursor?: string, + @Query('status') status?: 'seen' | 'unseen', + ) { return await this.notificationService.findByUserAndRestaurant( userId, restaurantId, limit ? parseInt(limit.toString(), 10) : 50, + cursor, + status, ); } diff --git a/src/modules/notifications/entities/notification.entity.ts b/src/modules/notifications/entities/notification.entity.ts index 0ff53ed..0f2d544 100644 --- a/src/modules/notifications/entities/notification.entity.ts +++ b/src/modules/notifications/entities/notification.entity.ts @@ -23,5 +23,5 @@ export class Notification extends BaseEntity { content!: string; @Property({ nullable: true }) - sentAt?: Date; + seenAt?: Date; } diff --git a/src/modules/notifications/services/notification.service.ts b/src/modules/notifications/services/notification.service.ts index 653589d..034afee 100644 --- a/src/modules/notifications/services/notification.service.ts +++ b/src/modules/notifications/services/notification.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { EntityManager } from '@mikro-orm/postgresql'; +import { EntityManager, FilterQuery } from '@mikro-orm/postgresql'; import { Notification } from '../entities/notification.entity'; import { NotificationPreferenceService } from './notification-preference.service'; import { NotificationQueueService } from './notification-queue.service'; @@ -108,15 +108,44 @@ export class NotificationService { ); } - async findByUserAndRestaurant(userId: string, restaurantId: string, limit = 50): Promise { - return this.em.find( - Notification, - { user: { id: userId }, restaurant: { id: restaurantId } }, - { - orderBy: { createdAt: 'DESC' }, - limit, - }, - ); + async findByUserAndRestaurant( + userId: string, + restaurantId: string, + limit = 50, + cursor?: string, + status?: 'seen' | 'unseen', + ): Promise<{ data: Notification[]; nextCursor: string | null }> { + const where: FilterQuery = { + user: { id: userId }, + restaurant: { id: restaurantId }, + }; + + // Filter by status (seen/unseen) + if (status === 'seen') { + where.seenAt = { $ne: null }; + } else if (status === 'unseen') { + where.seenAt = null; + } + + // Cursor-based pagination: if cursor is provided, get items with id < cursor (since ULIDs are time-ordered) + if (cursor) { + where.id = { $lt: cursor }; + } + + const notifications = await this.em.find(Notification, where, { + orderBy: { createdAt: 'DESC', id: 'DESC' }, + limit: limit + 1, // Fetch one extra to determine if there's a next page + }); + + // Check if there's a next page + const hasNextPage = notifications.length > limit; + const data = hasNextPage ? notifications.slice(0, limit) : notifications; + const nextCursor = hasNextPage && data.length > 0 ? data[data.length - 1].id : null; + + return { + data, + nextCursor, + }; } async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise { @@ -128,7 +157,7 @@ export class NotificationService { if (!notification) { throw new NotFoundException('Notification not found'); } - notification.sentAt = new Date(); + notification.seenAt = new Date(); await this.em.persistAndFlush(notification); } async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise { @@ -140,7 +169,7 @@ export class NotificationService { if (!notification) { throw new NotFoundException('Notification not found'); } - notification.sentAt = new Date(); + notification.seenAt = new Date(); await this.em.persistAndFlush(notification); }