update notif , now it is cursor based

This commit is contained in:
2025-12-20 20:24:39 +03:30
parent 6318c5b8ab
commit fae38c427d
3 changed files with 56 additions and 23 deletions
@@ -8,6 +8,7 @@ import { AdminAuthGuard } from '../../auth/guards/adminAuth.guard';
import { RestId } from '../../../common/decorators/rest-id.decorator'; import { RestId } from '../../../common/decorators/rest-id.decorator';
import { UpdatePreferenceDto } from '../dto/update-preference.dto'; import { UpdatePreferenceDto } from '../dto/update-preference.dto';
import { AdminId } from 'src/common/decorators/admin-id.decorator'; import { AdminId } from 'src/common/decorators/admin-id.decorator';
import { API_HEADER_SLUG } from 'src/common/constants/index';
@ApiTags('notifications') @ApiTags('notifications')
@Controller() @Controller()
@@ -21,20 +22,23 @@ export class NotificationsController {
@ApiBearerAuth() @ApiBearerAuth()
@Get('public/notifications') @Get('public/notifications')
@ApiOperation({ summary: 'Get user restaurant notifications' }) @ApiOperation({ summary: 'Get user restaurant notifications' })
@ApiHeader({ @ApiHeader(API_HEADER_SLUG)
name: 'X-Slug', @ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items to return (default: 50)' })
required: true, @ApiQuery({ name: 'cursor', required: false, type: String, description: 'Cursor for pagination (ID of the last item from previous page)' })
schema: { @ApiQuery({ name: 'status', required: false, enum: ['seen', 'unseen'], description: 'Filter by notification status' })
type: 'string', async getUserNotifications(
default: 'zhivan', @UserId() userId: string,
}, @RestId() restaurantId: string,
}) @Query('limit') limit?: number,
@ApiQuery({ name: 'limit', required: false, type: Number }) @Query('cursor') cursor?: string,
async getUserNotifications(@UserId() userId: string, @RestId() restaurantId: string, @Query('limit') limit?: number) { @Query('status') status?: 'seen' | 'unseen',
) {
return await this.notificationService.findByUserAndRestaurant( return await this.notificationService.findByUserAndRestaurant(
userId, userId,
restaurantId, restaurantId,
limit ? parseInt(limit.toString(), 10) : 50, limit ? parseInt(limit.toString(), 10) : 50,
cursor,
status,
); );
} }
@@ -23,5 +23,5 @@ export class Notification extends BaseEntity {
content!: string; content!: string;
@Property({ nullable: true }) @Property({ nullable: true })
sentAt?: Date; seenAt?: Date;
} }
@@ -1,5 +1,5 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 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 { Notification } from '../entities/notification.entity';
import { NotificationPreferenceService } from './notification-preference.service'; import { NotificationPreferenceService } from './notification-preference.service';
import { NotificationQueueService } from './notification-queue.service'; import { NotificationQueueService } from './notification-queue.service';
@@ -108,15 +108,44 @@ export class NotificationService {
); );
} }
async findByUserAndRestaurant(userId: string, restaurantId: string, limit = 50): Promise<Notification[]> { async findByUserAndRestaurant(
return this.em.find( userId: string,
Notification, restaurantId: string,
{ user: { id: userId }, restaurant: { id: restaurantId } }, limit = 50,
{ cursor?: string,
orderBy: { createdAt: 'DESC' }, status?: 'seen' | 'unseen',
limit, ): Promise<{ data: Notification[]; nextCursor: string | null }> {
}, const where: FilterQuery<Notification> = {
); 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<void> { async readNotificationAdmin(id: string, adminId: string, restaurantId: string): Promise<void> {
@@ -128,7 +157,7 @@ export class NotificationService {
if (!notification) { if (!notification) {
throw new NotFoundException('Notification not found'); throw new NotFoundException('Notification not found');
} }
notification.sentAt = new Date(); notification.seenAt = new Date();
await this.em.persistAndFlush(notification); await this.em.persistAndFlush(notification);
} }
async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise<void> { async readNotificationAsUser(id: string, userId: string, restaurantId: string): Promise<void> {
@@ -140,7 +169,7 @@ export class NotificationService {
if (!notification) { if (!notification) {
throw new NotFoundException('Notification not found'); throw new NotFoundException('Notification not found');
} }
notification.sentAt = new Date(); notification.seenAt = new Date();
await this.em.persistAndFlush(notification); await this.em.persistAndFlush(notification);
} }