first commit
هذا الالتزام موجود في:
155
src/modules/posts/posts.service.ts
Normal file
155
src/modules/posts/posts.service.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Types } from 'mongoose';
|
||||
import { PostType } from '../../common/enums/post-type.enum';
|
||||
import { PostVisibility } from '../../common/enums/post-visibility.enum';
|
||||
import { UsersRepository } from '../users/users.repository';
|
||||
import { CreatePostDto } from './dto/create-post.dto';
|
||||
import { PostQueryDto } from './dto/post-query.dto';
|
||||
import { UpdatePostDto } from './dto/update-post.dto';
|
||||
import { PostDocument } from './schemas/post.schema';
|
||||
import { PostsRepository } from './posts.repository';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
constructor(
|
||||
private readonly postsRepository: PostsRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
) {}
|
||||
|
||||
async create(userId: string, dto: CreatePostDto): Promise<PostDocument> {
|
||||
const postType = this.resolvePostType(dto.videoUrl, dto.audioUrl);
|
||||
const hashtags = this.extractHashtags(dto.content);
|
||||
|
||||
const post = await this.postsRepository.create(userId, {
|
||||
content: dto.content,
|
||||
videoUrl: dto.videoUrl ?? '',
|
||||
audioUrl: dto.audioUrl ?? '',
|
||||
postType,
|
||||
visibility: dto.visibility ?? PostVisibility.PUBLIC,
|
||||
hashtags,
|
||||
});
|
||||
|
||||
await this.usersRepository.incrementPostsCount(userId, 1);
|
||||
return post;
|
||||
}
|
||||
|
||||
async update(userId: string, postId: string, dto: UpdatePostDto): Promise<PostDocument> {
|
||||
const post = await this.postsRepository.findById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
|
||||
if (post.authorId.toString() !== userId) {
|
||||
throw new ForbiddenException('You can only update your own posts');
|
||||
}
|
||||
|
||||
const hasVideoUpdate = typeof dto.videoUrl !== 'undefined';
|
||||
const hasAudioUpdate = typeof dto.audioUrl !== 'undefined';
|
||||
|
||||
const nextVideoUrl = hasVideoUpdate ? dto.videoUrl ?? '' : post.videoUrl ?? '';
|
||||
const nextAudioUrl = hasAudioUpdate ? dto.audioUrl ?? '' : post.audioUrl ?? '';
|
||||
const nextPostType = this.resolvePostType(nextVideoUrl, nextAudioUrl);
|
||||
|
||||
const payload: UpdatePostDto & { postType: PostType } = {
|
||||
...dto,
|
||||
postType: nextPostType,
|
||||
};
|
||||
|
||||
if (typeof dto.content === 'string') {
|
||||
(payload as UpdatePostDto & { postType: PostType; hashtags: string[] }).hashtags =
|
||||
this.extractHashtags(dto.content);
|
||||
}
|
||||
|
||||
if (hasVideoUpdate && !hasAudioUpdate) {
|
||||
payload.audioUrl = '';
|
||||
}
|
||||
if (hasAudioUpdate && !hasVideoUpdate) {
|
||||
payload.videoUrl = '';
|
||||
}
|
||||
|
||||
const updated = await this.postsRepository.updateById(postId, payload);
|
||||
if (!updated) {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async remove(userId: string, postId: string): Promise<void> {
|
||||
const post = await this.postsRepository.findById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
|
||||
if (post.authorId.toString() !== userId) {
|
||||
throw new ForbiddenException('You can only delete your own posts');
|
||||
}
|
||||
|
||||
await this.postsRepository.deleteById(postId, userId);
|
||||
await this.usersRepository.incrementPostsCount(userId, -1);
|
||||
}
|
||||
|
||||
async findById(postId: string): Promise<PostDocument> {
|
||||
const post = await this.postsRepository.findById(postId);
|
||||
if (!post) {
|
||||
throw new NotFoundException('Post not found');
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
async findUserPosts(userId: string, query: PostQueryDto) {
|
||||
if (!Types.ObjectId.isValid(userId)) {
|
||||
throw new BadRequestException('Invalid user id');
|
||||
}
|
||||
|
||||
const page = query.page ?? 1;
|
||||
const limit = query.limit ?? 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const filter: Record<string, unknown> = { authorId: new Types.ObjectId(userId) };
|
||||
if (query.visibility) {
|
||||
filter.visibility = query.visibility;
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.postsRepository.findMany(filter, skip, limit),
|
||||
this.postsRepository.count(filter),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit) || 1,
|
||||
};
|
||||
}
|
||||
|
||||
private resolvePostType(videoUrl?: string, audioUrl?: string): PostType {
|
||||
const hasVideo = !!videoUrl?.trim();
|
||||
const hasAudio = !!audioUrl?.trim();
|
||||
|
||||
if (hasVideo && hasAudio) {
|
||||
throw new BadRequestException('Post can contain either video or audio, not both');
|
||||
}
|
||||
|
||||
if (hasVideo) {
|
||||
return PostType.VIDEO;
|
||||
}
|
||||
|
||||
if (hasAudio) {
|
||||
return PostType.AUDIO;
|
||||
}
|
||||
|
||||
return PostType.TEXT;
|
||||
}
|
||||
|
||||
private extractHashtags(content: string): string[] {
|
||||
const matches = content.match(/#[\p{L}\p{N}_]+/gu) ?? [];
|
||||
const normalized = matches
|
||||
.map((item) => item.replace('#', '').trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
return Array.from(new Set(normalized)).slice(0, 20);
|
||||
}
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم