feat: add notification category filters
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-06-07 01:20:49 +03:00
الأصل 1736cf143f
التزام d373d576e3
4 ملفات معدلة مع 132 إضافات و7 حذوفات

عرض الملف

@@ -5,6 +5,14 @@ import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
import { toBoolean } from '../../../common/utils/query-transform.util';
import { NOTIFICATION_TYPES, NotificationType } from '../schemas/notification.schema';
export const NOTIFICATION_CATEGORIES = [
'interactions',
'messages',
'follows',
'follow_requests',
] as const;
export type NotificationCategory = (typeof NOTIFICATION_CATEGORIES)[number];
export class NotificationQueryDto extends PaginationQueryDto {
@ApiPropertyOptional({ default: false })
@IsOptional()
@@ -21,4 +29,9 @@ export class NotificationQueryDto extends PaginationQueryDto {
@IsOptional()
@IsString()
resourceType?: string;
@ApiPropertyOptional({ enum: NOTIFICATION_CATEGORIES })
@IsOptional()
@IsEnum(NOTIFICATION_CATEGORIES)
category?: NotificationCategory;
}

عرض الملف

@@ -107,6 +107,80 @@ describe('NotificationsService', () => {
expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
});
it('filters notifications by interactions category', async () => {
const notificationsRepository = {
findMine: jest.fn().mockResolvedValue([]),
countMine: jest.fn().mockResolvedValue(0),
countUnread: jest.fn().mockResolvedValue(0),
};
const notificationsGateway = {
emitUnreadCount: jest.fn(),
};
const service = new NotificationsService(
notificationsRepository as any,
notificationsGateway as any,
);
await service.getMine('507f1f77bcf86cd799439011', {
page: 1,
limit: 20,
category: 'interactions',
});
expect(notificationsRepository.findMine).toHaveBeenCalledWith(
'507f1f77bcf86cd799439011',
{
type: {
$in: [
'like',
'comment',
'reply',
'mention',
'save',
'share',
'collaboration_request',
'system',
],
},
},
0,
20,
{ createdAt: -1 },
);
});
it('keeps type filter priority over category for backward compatibility', async () => {
const notificationsRepository = {
findMine: jest.fn().mockResolvedValue([]),
countMine: jest.fn().mockResolvedValue(0),
countUnread: jest.fn().mockResolvedValue(0),
};
const notificationsGateway = {
emitUnreadCount: jest.fn(),
};
const service = new NotificationsService(
notificationsRepository as any,
notificationsGateway as any,
);
await service.getMine('507f1f77bcf86cd799439011', {
page: 1,
limit: 20,
type: 'message',
category: 'interactions',
});
expect(notificationsRepository.findMine).toHaveBeenCalledWith(
'507f1f77bcf86cd799439011',
{ type: 'message' },
0,
20,
{ createdAt: -1 },
);
});
it('creates system notifications with system resource mapping when requested', async () => {
const notificationsRepository = {
create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),

عرض الملف

@@ -3,11 +3,27 @@ import { Types } from 'mongoose';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { CreateNotificationDto } from './dto/create-notification.dto';
import { NotificationQueryDto } from './dto/notification-query.dto';
import { NotificationCategory, NotificationQueryDto } from './dto/notification-query.dto';
import { NotificationsGateway } from './notifications.gateway';
import { NotificationsRepository } from './notifications.repository';
import { NotificationType } from './schemas/notification.schema';
const NOTIFICATION_CATEGORY_TYPES: Record<NotificationCategory, NotificationType[]> = {
interactions: [
'like',
'comment',
'reply',
'mention',
'save',
'share',
'collaboration_request',
'system',
],
messages: ['message'],
follows: ['follow'],
follow_requests: ['follow_request', 'follow_request_approved', 'follow_request_rejected'],
};
@Injectable()
export class NotificationsService {
constructor(
@@ -162,9 +178,7 @@ export class NotificationsService {
if (typeof query.read === 'boolean') {
filter.read = query.read;
}
if (query.type) {
filter.type = query.type;
}
this.applyTypeOrCategoryFilter(filter, query);
if (query.resourceType) {
filter.resourceType = query.resourceType.trim();
}
@@ -197,9 +211,7 @@ export class NotificationsService {
if (typeof query.read === 'boolean') {
filter.read = query.read;
}
if (query.type) {
filter.type = query.type;
}
this.applyTypeOrCategoryFilter(filter, query);
if (query.resourceType) {
filter.resourceType = query.resourceType.trim();
}
@@ -287,6 +299,12 @@ export class NotificationsService {
return 'Notification';
case 'collaboration_request':
return 'Collaboration request';
case 'follow_request':
return 'Follow request';
case 'follow_request_approved':
return 'Follow request approved';
case 'follow_request_rejected':
return 'Follow request rejected';
default:
return 'Notification';
}
@@ -295,6 +313,9 @@ export class NotificationsService {
private resolveResourceType(type: NotificationType): string {
switch (type) {
case 'follow':
case 'follow_request':
case 'follow_request_approved':
case 'follow_request_rejected':
return 'user';
case 'message':
return 'conversation';
@@ -330,4 +351,18 @@ export class NotificationsService {
return '';
}
private applyTypeOrCategoryFilter(
filter: Record<string, unknown>,
query: NotificationQueryDto,
): void {
if (query.type) {
filter.type = query.type;
return;
}
if (query.category) {
filter.type = { $in: NOTIFICATION_CATEGORY_TYPES[query.category] };
}
}
}

عرض الملف

@@ -15,6 +15,9 @@ export const NOTIFICATION_TYPES = [
'reply',
'system',
'collaboration_request',
'follow_request',
'follow_request_approved',
'follow_request_rejected',
] as const;
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];