notif cursor

This commit is contained in:
2025-12-21 12:34:45 +03:30
parent c36ff7ed85
commit ecba2773be
5 changed files with 72 additions and 155 deletions
@@ -16,7 +16,7 @@ export class NotificationsController {
constructor(
private readonly notificationService: NotificationService,
private readonly preferenceService: NotificationPreferenceService,
) {}
) { }
@UseGuards(AuthGuard)
@ApiBearerAuth()
@@ -24,7 +24,12 @@ export class NotificationsController {
@ApiOperation({ summary: 'Get user restaurant notifications' })
@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: '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,
@@ -52,13 +57,29 @@ export class NotificationsController {
return { message: 'Notification read successfully' };
}
/* ***************** Admin Endpoints ***************** */
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Get('admin/notifications')
@ApiOperation({ summary: 'Get Admin restaurant notifications' })
@ApiOperation({ summary: 'Get Admin notifications' })
@ApiQuery({ name: 'limit', required: false, type: Number })
async getAdminNotifications(@RestId() restaurantId: string, @Query('limit') limit?: number) {
return await this.notificationService.findByRestaurant(restaurantId, limit ? parseInt(limit.toString(), 30) : 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 getAdminNotifications(
@AdminId() adminId: string,
@RestId() restaurantId: string,
@Query('limit') limit?: number,
@Query('cursor') cursor?: string,
@Query('status') status?: 'seen' | 'unseen',
) {
const parsedLimit = limit ? parseInt(limit.toString(), 10) : 50;
return await this.notificationService.findByRestaurant(restaurantId, parsedLimit, cursor, status);
}
@UseGuards(AdminAuthGuard)
@@ -71,72 +92,6 @@ export class NotificationsController {
return { message: 'Notification read successfully' };
}
// @UseGuards(AuthGuard)
// @ApiBearerAuth()
// @Get('public/notifications/:id')
// @ApiOperation({ summary: 'Get a notification by ID' })
// @ApiParam({ name: 'id', description: 'Notification ID' })
// async getUserNotification(@Param('id') id: string) {
// return this.notificationService.findOne(id);
// }
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Post('admin/notifications/send')
// @ApiOperation({ summary: 'Send a notification (queued)' })
// async sendNotification(@Body() dto: SendNotificationDto) {
// return this.notificationService.sendNotification(dto);
// }
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notifications')
// @ApiOperation({ summary: 'Get notifications for a restaurant' })
// @ApiQuery({ name: 'limit', required: false, type: Number })
// @ApiQuery({ name: 'notificationType', required: false, type: String })
// async getRestaurantNotifications(
// @RestId() restaurantId: string,
// @Query('limit') limit?: number,
// @Query('notificationType') notificationType?: string,
// ) {
// if (notificationType) {
// return this.notificationService.findByRestaurantAndType(
// restaurantId,
// notificationType,
// limit ? parseInt(limit.toString(), 10) : 50,
// );
// }
// return this.notificationService.findByRestaurant(restaurantId, limit ? parseInt(limit.toString(), 10) : 50);
// }
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notifications/:id')
// @ApiOperation({ summary: 'Get a notification by ID' })
// @ApiParam({ name: 'id', description: 'Notification ID' })
// async getNotification(@Param('id') id: string) {
// return this.notificationService.findOne(id);
// }
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Post('admin/notifications/:id/retry')
// @ApiOperation({ summary: 'Retry a failed notification' })
// @ApiParam({ name: 'id', description: 'Notification ID' })
// async retryNotification(@Param('id') id: string) {
// await this.notificationService.retryFailedNotification(id);
// return { message: 'Notification queued for retry' };
// }
// Preference endpoints
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Post('admin/notification-preferences')
// @ApiOperation({ summary: 'Create notification preference' })
// createPreference(@RestId() restaurantId: string, @Body() dto: CreatePreferenceDto) {
// return this.preferenceService.createOrUpdate(restaurantId, dto);
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Get('admin/notification-preferences')
@@ -145,28 +100,6 @@ export class NotificationsController {
return this.preferenceService.findByRestaurant(restaurantId);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Get('admin/notification-preferences/:type')
// @ApiOperation({ summary: 'Get a specific notification preference' })
// @ApiParam({ name: 'type', description: 'Notification type' })
// async getPreference(@RestId() restaurantId: string, @Param('type') notificationType: string) {
// return this.preferenceService.findByRestaurantAndType(restaurantId, notificationType);
// }
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Patch('admin/notification-preferences/:type/enabled')
// @ApiOperation({ summary: 'Enable or disable a notification preference' })
// @ApiParam({ name: 'type', description: 'Notification type' })
// async updatePreferenceEnabled(
// @RestId() restaurantId: string,
// @Param('type') notificationType: string,
// @Body('enabled') enabled: boolean,
// ) {
// return this.preferenceService.updateEnabled(restaurantId, notificationType, enabled);
// }
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Patch('admin/notification-preferences/:id')
@@ -179,14 +112,4 @@ export class NotificationsController {
) {
return this.preferenceService.updatePreference(preferenceId, restaurantId, dto);
}
// @UseGuards(AdminAuthGuard)
// @ApiBearerAuth()
// @Delete('admin/notification-preferences/:id')
// @ApiOperation({ summary: 'Delete a notification preference' })
// @ApiParam({ name: 'id', description: 'Notification preference ID' })
// async deletePreference(@RestId() restaurantId: string, @Param('id') preferenceId: string) {
// await this.preferenceService.delete(restaurantId, preferenceId);
// return { message: 'Preference deleted successfully' };
// }
}
@@ -15,7 +15,7 @@ export class NotificationService {
private readonly preferenceService: NotificationPreferenceService,
private readonly queueService: NotificationQueueService,
private readonly notificationGateway: NotificationsGateway,
) {}
) { }
async sendNotification(params: NotifRequest): Promise<Notification[]> {
const { recipients, message, metadata, restaurantId } = params;
@@ -96,16 +96,42 @@ export class NotificationService {
return notification;
}
async findByRestaurant(restaurantId: string, limit = 50): Promise<Notification[]> {
return this.em.find(
Notification,
{ restaurant: { id: restaurantId } },
{
orderBy: { createdAt: 'DESC' },
limit,
populate: ['user'],
},
);
async findByRestaurant(
restaurantId: string,
limit = 50,
cursor?: string,
status?: 'seen' | 'unseen',
): Promise<{ data: Notification[]; nextCursor: string | null }> {
const where: FilterQuery<Notification> = {
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
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 next page
populate: ['user'],
});
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: nextCursor ?? null,
};
}
async findByUserAndRestaurant(
@@ -144,7 +170,7 @@ export class NotificationService {
return {
data,
nextCursor,
nextCursor: nextCursor ?? null,
};
}