feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s
هذا الالتزام موجود في:
@@ -1,12 +1,16 @@
|
||||
import { Controller, Delete, Get, Param, Post, Query, Body, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { SuperAdminPermissions } from '../../common/decorators/superadmin-permissions.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { SuperAdminPermissionsGuard } from '../../common/guards/superadmin-permissions.guard';
|
||||
import { SuperAdminJwtAuthGuard } from '../../common/guards/super-admin-jwt-auth.guard';
|
||||
import { JwtPayload } from '../../common/interfaces/jwt-payload.interface';
|
||||
import { AdminCommentQueryDto } from './dto/admin-comment-query.dto';
|
||||
import { CommentQueryDto } from './dto/comment-query.dto';
|
||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||
import { CommentsService } from './comments.service';
|
||||
import { SUPERADMIN_PERMISSIONS } from '../superadmin/superadmin-permissions';
|
||||
|
||||
@ApiTags('Comments')
|
||||
@Controller('comments')
|
||||
@@ -34,6 +38,14 @@ export class CommentsController {
|
||||
return this.commentsService.findReplies(commentId, query);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(SuperAdminJwtAuthGuard, SuperAdminPermissionsGuard)
|
||||
@SuperAdminPermissions(SUPERADMIN_PERMISSIONS.CONTENT_MODERATE)
|
||||
@Get('admin')
|
||||
async adminList(@Query() query: AdminCommentQueryDto) {
|
||||
return this.commentsService.findPlatformComments(query);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':commentId')
|
||||
@@ -42,7 +54,8 @@ export class CommentsController {
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(SuperAdminJwtAuthGuard)
|
||||
@UseGuards(SuperAdminJwtAuthGuard, SuperAdminPermissionsGuard)
|
||||
@SuperAdminPermissions(SUPERADMIN_PERMISSIONS.CONTENT_MODERATE)
|
||||
@Delete('admin/:commentId')
|
||||
async adminRemove(@CurrentUser() user: JwtPayload, @Param('commentId') commentId: string) {
|
||||
return this.commentsService.removeBySuperAdmin(user.email ?? user.sub, commentId);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { PostsModule } from '../posts/posts.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { Comment, CommentSchema } from './schemas/comment.schema';
|
||||
import { CommentsController } from './comments.controller';
|
||||
import { CommentsService } from './comments.service';
|
||||
@@ -12,6 +14,8 @@ import { CommentsRepository } from './comments.repository';
|
||||
AuditModule,
|
||||
MongooseModule.forFeature([{ name: Comment.name, schema: CommentSchema }]),
|
||||
PostsModule,
|
||||
NotificationsModule,
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [CommentsController],
|
||||
providers: [CommentsService, CommentsRepository],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { ClientSession, FilterQuery, Model, Types } from 'mongoose';
|
||||
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
|
||||
import { Comment, CommentDocument } from './schemas/comment.schema';
|
||||
|
||||
@Injectable()
|
||||
@@ -8,6 +9,14 @@ export class CommentsRepository {
|
||||
constructor(@InjectModel(Comment.name) private readonly commentModel: Model<CommentDocument>) {}
|
||||
|
||||
private withActiveFilter<T extends FilterQuery<CommentDocument>>(filter: T): FilterQuery<CommentDocument> {
|
||||
return {
|
||||
...filter,
|
||||
isDeleted: { $ne: true },
|
||||
moderationStatus: { $ne: ModerationStatus.HIDDEN },
|
||||
};
|
||||
}
|
||||
|
||||
private withAdminFilter<T extends FilterQuery<CommentDocument>>(filter: T): FilterQuery<CommentDocument> {
|
||||
return {
|
||||
...filter,
|
||||
isDeleted: { $ne: true },
|
||||
@@ -15,15 +24,24 @@ export class CommentsRepository {
|
||||
}
|
||||
|
||||
async create(
|
||||
payload: { postId: string; authorId: string; content: string; parentCommentId?: string },
|
||||
payload: {
|
||||
postId: string;
|
||||
authorId: string;
|
||||
content: string;
|
||||
mentionUsernames?: string[];
|
||||
parentCommentId?: string;
|
||||
},
|
||||
session?: ClientSession,
|
||||
) {
|
||||
return this.commentModel.create({
|
||||
const doc = new this.commentModel({
|
||||
postId: new Types.ObjectId(payload.postId),
|
||||
authorId: new Types.ObjectId(payload.authorId),
|
||||
content: payload.content,
|
||||
mentionUsernames: payload.mentionUsernames ?? [],
|
||||
...(payload.parentCommentId ? { parentCommentId: new Types.ObjectId(payload.parentCommentId) } : {}),
|
||||
}, { session });
|
||||
});
|
||||
|
||||
return session ? doc.save({ session }) : doc.save();
|
||||
}
|
||||
|
||||
async findById(commentId: string): Promise<CommentDocument | null> {
|
||||
@@ -55,11 +73,31 @@ export class CommentsRepository {
|
||||
return !!updated;
|
||||
}
|
||||
|
||||
async findMany(filter: FilterQuery<CommentDocument>, skip: number, limit: number) {
|
||||
async findMany(
|
||||
filter: FilterQuery<CommentDocument>,
|
||||
skip: number,
|
||||
limit: number,
|
||||
sort: Record<string, 1 | -1> = { createdAt: -1 },
|
||||
) {
|
||||
return this.commentModel
|
||||
.find(this.withActiveFilter(filter))
|
||||
.populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
|
||||
.sort({ createdAt: -1 })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async findManyAdmin(
|
||||
filter: FilterQuery<CommentDocument>,
|
||||
skip: number,
|
||||
limit: number,
|
||||
sort: Record<string, 1 | -1> = { createdAt: -1 },
|
||||
) {
|
||||
return this.commentModel
|
||||
.find(this.withAdminFilter(filter))
|
||||
.populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.exec();
|
||||
@@ -69,6 +107,31 @@ export class CommentsRepository {
|
||||
return this.commentModel.countDocuments(this.withActiveFilter(filter)).exec();
|
||||
}
|
||||
|
||||
async countAdmin(filter: FilterQuery<CommentDocument>): Promise<number> {
|
||||
return this.commentModel.countDocuments(this.withAdminFilter(filter)).exec();
|
||||
}
|
||||
|
||||
async updateModerationStatus(
|
||||
commentId: string,
|
||||
payload: Pick<Comment, 'moderationStatus' | 'moderationReason'>,
|
||||
): Promise<CommentDocument | null> {
|
||||
if (!Types.ObjectId.isValid(commentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.commentModel
|
||||
.findByIdAndUpdate(
|
||||
commentId,
|
||||
{
|
||||
moderationStatus: payload.moderationStatus,
|
||||
moderationReason: payload.moderationReason,
|
||||
},
|
||||
{ new: true },
|
||||
)
|
||||
.populate({ path: 'authorId', select: 'name username avatar stageName isVerified' })
|
||||
.exec();
|
||||
}
|
||||
|
||||
async countByPost(postId: string): Promise<number> {
|
||||
return this.commentModel
|
||||
.countDocuments({ postId: new Types.ObjectId(postId), isDeleted: { $ne: true } })
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Types } from 'mongoose';
|
||||
import { ModerationStatus } from '../../common/enums/moderation-status.enum';
|
||||
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
|
||||
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
|
||||
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { PostsRepository } from '../posts/posts.repository';
|
||||
import { UsersRepository } from '../users/users.repository';
|
||||
import { AdminCommentQueryDto } from './dto/admin-comment-query.dto';
|
||||
import { CommentQueryDto } from './dto/comment-query.dto';
|
||||
import { CreateCommentDto } from './dto/create-comment.dto';
|
||||
import { CommentsRepository } from './comments.repository';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsService {
|
||||
private readonly logger = new Logger(CommentsService.name);
|
||||
|
||||
constructor(
|
||||
private readonly commentsRepository: CommentsRepository,
|
||||
private readonly postsRepository: PostsRepository,
|
||||
private readonly auditService: AuditService,
|
||||
private readonly feedVersionService: FeedVersionService,
|
||||
private readonly notificationsService: NotificationsService,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
) {}
|
||||
|
||||
async create(userId: string, dto: CreateCommentDto) {
|
||||
@@ -19,20 +32,42 @@ export class CommentsService {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
|
||||
let parentRecipientId = '';
|
||||
if (dto.parentCommentId) {
|
||||
const parent = await this.commentsRepository.findById(dto.parentCommentId);
|
||||
if (!parent || parent.postId.toString() !== dto.postId) {
|
||||
throw new NotFoundException('Parent comment not found');
|
||||
}
|
||||
parentRecipientId = parent.authorId.toString();
|
||||
}
|
||||
|
||||
const content = dto.content.trim();
|
||||
const mentionResolution = await this.resolveMentionTargets(dto.mentionUsernames, content, userId);
|
||||
const comment = await this.commentsRepository.create({
|
||||
postId: dto.postId,
|
||||
authorId: userId,
|
||||
content: dto.content,
|
||||
content,
|
||||
mentionUsernames: mentionResolution.mentionUsernames,
|
||||
parentCommentId: dto.parentCommentId,
|
||||
});
|
||||
await this.syncCommentsCount(dto.postId);
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
const postAuthorId = this.extractEntityId(post.authorId);
|
||||
const previewText = content.slice(0, 160);
|
||||
const commentNotificationRecipients = await this.dispatchCommentNotifications(
|
||||
userId,
|
||||
postAuthorId,
|
||||
parentRecipientId,
|
||||
dto.postId,
|
||||
previewText,
|
||||
);
|
||||
await this.notifyMentionedUsers(
|
||||
userId,
|
||||
dto.postId,
|
||||
mentionResolution.mentionedUsers,
|
||||
previewText,
|
||||
commentNotificationRecipients,
|
||||
);
|
||||
return comment;
|
||||
}
|
||||
|
||||
@@ -48,6 +83,7 @@ export class CommentsService {
|
||||
|
||||
await this.commentsRepository.deleteById(commentId, userId);
|
||||
await this.syncCommentsCount(comment.postId.toString());
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -59,6 +95,7 @@ export class CommentsService {
|
||||
|
||||
await this.commentsRepository.deleteById(commentId, superAdminIdentifier);
|
||||
await this.syncCommentsCount(comment.postId.toString());
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
await this.auditService.logSuperAdminAction(
|
||||
superAdminIdentifier,
|
||||
'comment_delete',
|
||||
@@ -70,45 +107,281 @@ export class CommentsService {
|
||||
}
|
||||
|
||||
async findByPost(postId: string, query: CommentQueryDto) {
|
||||
if (!Types.ObjectId.isValid(postId)) {
|
||||
throw new BadRequestException('Invalid post id');
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const skip = (page - 1) * limit;
|
||||
const postObjectId = new Types.ObjectId(postId);
|
||||
const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.commentsRepository.findMany({ postId, parentCommentId: { $exists: false } }, skip, limit),
|
||||
this.commentsRepository.count({ postId, parentCommentId: { $exists: false } }),
|
||||
this.commentsRepository.findMany(
|
||||
{
|
||||
postId: postObjectId,
|
||||
$or: [{ parentCommentId: { $exists: false } }, { parentCommentId: null }],
|
||||
},
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
),
|
||||
this.commentsRepository.count({
|
||||
postId: postObjectId,
|
||||
$or: [{ parentCommentId: { $exists: false } }, { parentCommentId: null }],
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
return buildPaginatedResponse(items, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit) || 1,
|
||||
};
|
||||
offset: skip,
|
||||
});
|
||||
}
|
||||
|
||||
async findReplies(parentCommentId: string, query: CommentQueryDto) {
|
||||
if (!Types.ObjectId.isValid(parentCommentId)) {
|
||||
throw new BadRequestException('Invalid parent comment id');
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const skip = (page - 1) * limit;
|
||||
const parentObjectId = new Types.ObjectId(parentCommentId);
|
||||
const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.commentsRepository.findMany({ parentCommentId }, skip, limit),
|
||||
this.commentsRepository.count({ parentCommentId }),
|
||||
this.commentsRepository.findMany({ parentCommentId: parentObjectId }, skip, limit, sort),
|
||||
this.commentsRepository.count({ parentCommentId: parentObjectId }),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
return buildPaginatedResponse(items, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit) || 1,
|
||||
};
|
||||
offset: skip,
|
||||
});
|
||||
}
|
||||
|
||||
async findPlatformComments(query: AdminCommentQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const skip = (page - 1) * limit;
|
||||
const filter: Record<string, unknown> = {};
|
||||
|
||||
if (query.postId) {
|
||||
filter.postId = new Types.ObjectId(query.postId);
|
||||
}
|
||||
if (query.authorId) {
|
||||
filter.authorId = new Types.ObjectId(query.authorId);
|
||||
}
|
||||
if (query.q?.trim()) {
|
||||
filter.content = { $regex: query.q.trim(), $options: 'i' };
|
||||
}
|
||||
if (query.moderationStatus) {
|
||||
filter.moderationStatus = query.moderationStatus;
|
||||
}
|
||||
|
||||
const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
|
||||
const [items, total] = await Promise.all([
|
||||
this.commentsRepository.findManyAdmin(filter, skip, limit, sort),
|
||||
this.commentsRepository.countAdmin(filter),
|
||||
]);
|
||||
|
||||
return buildPaginatedResponse(items, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
offset: skip,
|
||||
});
|
||||
}
|
||||
|
||||
async updateModerationStatusBySuperAdmin(
|
||||
superAdminIdentifier: string,
|
||||
commentId: string,
|
||||
dto: { status: ModerationStatus; reason?: string },
|
||||
) {
|
||||
const comment = await this.commentsRepository.findById(commentId);
|
||||
if (!comment) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
const updated = await this.commentsRepository.updateModerationStatus(commentId, {
|
||||
moderationStatus: dto.status,
|
||||
moderationReason: dto.reason?.trim() ?? '',
|
||||
});
|
||||
if (!updated) {
|
||||
throw new NotFoundException('Comment not found');
|
||||
}
|
||||
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
await this.auditService.logSuperAdminAction(
|
||||
superAdminIdentifier,
|
||||
'comment_moderation_status_update',
|
||||
'comment',
|
||||
commentId,
|
||||
{
|
||||
previousStatus: comment.moderationStatus ?? ModerationStatus.ACTIVE,
|
||||
nextStatus: dto.status,
|
||||
reason: dto.reason?.trim() ?? '',
|
||||
},
|
||||
);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async syncCommentsCount(postId: string): Promise<void> {
|
||||
const totalComments = await this.commentsRepository.countByPost(postId);
|
||||
await this.postsRepository.setCommentsCount(postId, totalComments);
|
||||
}
|
||||
|
||||
private async dispatchCommentNotifications(
|
||||
actorId: string,
|
||||
postAuthorId: string,
|
||||
parentRecipientId: string,
|
||||
postId: string,
|
||||
previewText: string,
|
||||
): Promise<Set<string>> {
|
||||
const recipients = new Set<string>();
|
||||
if (postAuthorId && postAuthorId !== actorId) {
|
||||
recipients.add(postAuthorId);
|
||||
}
|
||||
if (parentRecipientId && parentRecipientId !== actorId) {
|
||||
recipients.add(parentRecipientId);
|
||||
}
|
||||
|
||||
for (const recipientId of recipients) {
|
||||
try {
|
||||
await this.notificationsService.createCommentNotification(actorId, recipientId, postId, {
|
||||
resourceType: 'post',
|
||||
previewText,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Comment notification failed for actor=${actorId} recipient=${recipientId}: ${
|
||||
error instanceof Error ? error.message : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
private normalizeMentionUsernames(input: string[] = []): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
input
|
||||
.map((username) => username?.trim().replace(/^@+/, '').toLowerCase())
|
||||
.filter((username): username is string => !!username),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private extractMentions(content: string): string[] {
|
||||
const matches = content.match(/@[\p{L}\p{N}_.]+/gu) ?? [];
|
||||
return this.normalizeMentionUsernames(matches.map((item) => item.replace('@', '')));
|
||||
}
|
||||
|
||||
private async resolveMentionTargets(
|
||||
explicitMentionUsernames: string[] | undefined,
|
||||
content: string,
|
||||
authorId: string,
|
||||
): Promise<{
|
||||
mentionUsernames: string[];
|
||||
mentionedUsers: Array<{ id: string; username: string }>;
|
||||
}> {
|
||||
const mergedMentionUsernames = Array.from(
|
||||
new Set([
|
||||
...this.extractMentions(content),
|
||||
...this.normalizeMentionUsernames(explicitMentionUsernames ?? []),
|
||||
]),
|
||||
);
|
||||
|
||||
if (mergedMentionUsernames.length > 30) {
|
||||
throw new BadRequestException('You can mention up to 30 users only');
|
||||
}
|
||||
|
||||
if (!mergedMentionUsernames.length) {
|
||||
return { mentionUsernames: [], mentionedUsers: [] };
|
||||
}
|
||||
|
||||
const users = await this.usersRepository.findByUsernames(mergedMentionUsernames);
|
||||
const userByUsername = new Map(
|
||||
users.map((user) => [user.username.toLowerCase(), { id: user.id, username: user.username.toLowerCase() }]),
|
||||
);
|
||||
|
||||
const mentionedUsers = mergedMentionUsernames
|
||||
.map((username) => userByUsername.get(username))
|
||||
.filter((user): user is { id: string; username: string } => !!user)
|
||||
.filter((user) => user.id !== authorId);
|
||||
|
||||
return {
|
||||
mentionUsernames: mentionedUsers.map((user) => user.username),
|
||||
mentionedUsers,
|
||||
};
|
||||
}
|
||||
|
||||
private async notifyMentionedUsers(
|
||||
actorId: string,
|
||||
postId: string,
|
||||
mentionedUsers: Array<{ id: string; username: string }>,
|
||||
previewText: string,
|
||||
excludedRecipientIds: Set<string>,
|
||||
): Promise<void> {
|
||||
if (!mentionedUsers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mentionedUser of mentionedUsers) {
|
||||
if (excludedRecipientIds.has(mentionedUser.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.notificationsService.createMentionNotification(actorId, mentionedUser.id, postId, {
|
||||
resourceType: 'comment',
|
||||
previewText,
|
||||
deepLink: `/posts/${postId}`,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Comment mention notification failed for actor=${actorId} recipient=${mentionedUser.id}: ${
|
||||
error instanceof Error ? error.message : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractEntityId(value: unknown): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value instanceof Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
const candidate = value as { _id?: unknown; id?: unknown };
|
||||
if (candidate._id instanceof Types.ObjectId) {
|
||||
return candidate._id.toString();
|
||||
}
|
||||
if (typeof candidate._id === 'string') {
|
||||
return candidate._id;
|
||||
}
|
||||
if (typeof candidate.id === 'string') {
|
||||
return candidate.id;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
26
src/modules/comments/dto/admin-comment-query.dto.ts
Normal file
26
src/modules/comments/dto/admin-comment-query.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsMongoId, IsOptional, IsString } from 'class-validator';
|
||||
import { ModerationStatus } from '../../../common/enums/moderation-status.enum';
|
||||
import { CommentQueryDto } from './comment-query.dto';
|
||||
|
||||
export class AdminCommentQueryDto extends CommentQueryDto {
|
||||
@ApiPropertyOptional({ description: 'Search comment content' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
q?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Optional post filter' })
|
||||
@IsOptional()
|
||||
@IsMongoId()
|
||||
postId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Optional author filter' })
|
||||
@IsOptional()
|
||||
@IsMongoId()
|
||||
authorId?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: ModerationStatus, description: 'Optional moderation status filter' })
|
||||
@IsOptional()
|
||||
@IsEnum(ModerationStatus)
|
||||
moderationStatus?: ModerationStatus;
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
|
||||
|
||||
export class CommentQueryDto extends PaginationQueryDto {}
|
||||
export class CommentQueryDto extends PaginationQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Use asc to display oldest comments first, or desc for newest first',
|
||||
default: 'desc',
|
||||
})
|
||||
declare sortOrder: PaginationQueryDto['sortOrder'];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsMongoId, IsOptional, IsString, Length } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { ArrayMaxSize, IsArray, IsMongoId, IsOptional, IsString, Length } from 'class-validator';
|
||||
import { toStringArray } from '../../../common/utils/array-transform.util';
|
||||
|
||||
export class CreateCommentDto {
|
||||
@ApiProperty()
|
||||
@@ -15,4 +17,13 @@ export class CreateCommentDto {
|
||||
@IsOptional()
|
||||
@IsMongoId()
|
||||
parentCommentId?: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [String], description: 'Mention usernames like rami_sabry (max 30)' })
|
||||
@IsOptional()
|
||||
@Transform(toStringArray)
|
||||
@IsArray()
|
||||
@ArrayMaxSize(30)
|
||||
@IsString({ each: true })
|
||||
@Length(1, 30, { each: true })
|
||||
mentionUsernames?: string[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { HydratedDocument, Types } from 'mongoose';
|
||||
import { ModerationStatus } from '../../../common/enums/moderation-status.enum';
|
||||
import { Post } from '../../posts/schemas/post.schema';
|
||||
import { User } from '../../users/schemas/user.schema';
|
||||
|
||||
@@ -19,6 +20,20 @@ export class Comment {
|
||||
@Prop({ required: true, maxlength: 1000 })
|
||||
content!: string;
|
||||
|
||||
@Prop({ type: [String], default: [] })
|
||||
mentionUsernames!: string[];
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: Object.values(ModerationStatus),
|
||||
default: ModerationStatus.ACTIVE,
|
||||
index: true,
|
||||
})
|
||||
moderationStatus!: ModerationStatus;
|
||||
|
||||
@Prop({ default: '', maxlength: 300 })
|
||||
moderationReason!: string;
|
||||
|
||||
@Prop({ default: false, index: true })
|
||||
isDeleted!: boolean;
|
||||
|
||||
@@ -32,3 +47,4 @@ export class Comment {
|
||||
export const CommentSchema = SchemaFactory.createForClass(Comment);
|
||||
CommentSchema.index({ postId: 1, createdAt: -1 });
|
||||
CommentSchema.index({ postId: 1, parentCommentId: 1, isDeleted: 1, createdAt: -1 });
|
||||
CommentSchema.index({ moderationStatus: 1, createdAt: -1 });
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم