feat: add notification category filters
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
هذا الالتزام موجود في:
@@ -5,6 +5,14 @@ import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
|
|||||||
import { toBoolean } from '../../../common/utils/query-transform.util';
|
import { toBoolean } from '../../../common/utils/query-transform.util';
|
||||||
import { NOTIFICATION_TYPES, NotificationType } from '../schemas/notification.schema';
|
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 {
|
export class NotificationQueryDto extends PaginationQueryDto {
|
||||||
@ApiPropertyOptional({ default: false })
|
@ApiPropertyOptional({ default: false })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -21,4 +29,9 @@ export class NotificationQueryDto extends PaginationQueryDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
resourceType?: string;
|
resourceType?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: NOTIFICATION_CATEGORIES })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(NOTIFICATION_CATEGORIES)
|
||||||
|
category?: NotificationCategory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,80 @@ describe('NotificationsService', () => {
|
|||||||
expect(notificationsRepository.countUnread).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
|
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 () => {
|
it('creates system notifications with system resource mapping when requested', async () => {
|
||||||
const notificationsRepository = {
|
const notificationsRepository = {
|
||||||
create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
|
create: jest.fn().mockResolvedValue({ toJSON: () => ({ _id: 'notification-1' }) }),
|
||||||
|
|||||||
@@ -3,11 +3,27 @@ import { Types } from 'mongoose';
|
|||||||
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
||||||
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
||||||
import { CreateNotificationDto } from './dto/create-notification.dto';
|
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 { NotificationsGateway } from './notifications.gateway';
|
||||||
import { NotificationsRepository } from './notifications.repository';
|
import { NotificationsRepository } from './notifications.repository';
|
||||||
import { NotificationType } from './schemas/notification.schema';
|
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()
|
@Injectable()
|
||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -162,9 +178,7 @@ export class NotificationsService {
|
|||||||
if (typeof query.read === 'boolean') {
|
if (typeof query.read === 'boolean') {
|
||||||
filter.read = query.read;
|
filter.read = query.read;
|
||||||
}
|
}
|
||||||
if (query.type) {
|
this.applyTypeOrCategoryFilter(filter, query);
|
||||||
filter.type = query.type;
|
|
||||||
}
|
|
||||||
if (query.resourceType) {
|
if (query.resourceType) {
|
||||||
filter.resourceType = query.resourceType.trim();
|
filter.resourceType = query.resourceType.trim();
|
||||||
}
|
}
|
||||||
@@ -197,9 +211,7 @@ export class NotificationsService {
|
|||||||
if (typeof query.read === 'boolean') {
|
if (typeof query.read === 'boolean') {
|
||||||
filter.read = query.read;
|
filter.read = query.read;
|
||||||
}
|
}
|
||||||
if (query.type) {
|
this.applyTypeOrCategoryFilter(filter, query);
|
||||||
filter.type = query.type;
|
|
||||||
}
|
|
||||||
if (query.resourceType) {
|
if (query.resourceType) {
|
||||||
filter.resourceType = query.resourceType.trim();
|
filter.resourceType = query.resourceType.trim();
|
||||||
}
|
}
|
||||||
@@ -287,6 +299,12 @@ export class NotificationsService {
|
|||||||
return 'Notification';
|
return 'Notification';
|
||||||
case 'collaboration_request':
|
case 'collaboration_request':
|
||||||
return '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:
|
default:
|
||||||
return 'Notification';
|
return 'Notification';
|
||||||
}
|
}
|
||||||
@@ -295,6 +313,9 @@ export class NotificationsService {
|
|||||||
private resolveResourceType(type: NotificationType): string {
|
private resolveResourceType(type: NotificationType): string {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
case 'follow_request':
|
||||||
|
case 'follow_request_approved':
|
||||||
|
case 'follow_request_rejected':
|
||||||
return 'user';
|
return 'user';
|
||||||
case 'message':
|
case 'message':
|
||||||
return 'conversation';
|
return 'conversation';
|
||||||
@@ -330,4 +351,18 @@ export class NotificationsService {
|
|||||||
|
|
||||||
return '';
|
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',
|
'reply',
|
||||||
'system',
|
'system',
|
||||||
'collaboration_request',
|
'collaboration_request',
|
||||||
|
'follow_request',
|
||||||
|
'follow_request_approved',
|
||||||
|
'follow_request_rejected',
|
||||||
] as const;
|
] as const;
|
||||||
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
||||||
|
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم