هذا الالتزام موجود في:
2026-04-20 15:12:16 +03:00
التزام 28f7241bcd
172 ملفات معدلة مع 21907 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsMongoId } from 'class-validator';
export class ToggleLikeDto {
@ApiProperty()
@IsMongoId()
targetId!: string;
@ApiProperty({ enum: ['post', 'comment'] })
@IsIn(['post', 'comment'])
targetType!: 'post' | 'comment';
}

عرض الملف

@@ -0,0 +1,43 @@
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { JwtPayload } from '../../common/interfaces/jwt-payload.interface';
import { ToggleLikeDto } from './dto/toggle-like.dto';
import { LikesService } from './likes.service';
@ApiTags('Likes')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('likes')
export class LikesController {
constructor(private readonly likesService: LikesService) {}
@Post()
async like(@CurrentUser() user: JwtPayload, @Body() dto: ToggleLikeDto) {
return this.likesService.like(user.sub, dto);
}
@Delete(':targetType/:targetId')
async unlike(
@CurrentUser() user: JwtPayload,
@Param('targetId') targetId: string,
@Param('targetType') targetType: 'post' | 'comment',
) {
return this.likesService.unlike(user.sub, { targetId, targetType });
}
@Get('status/:targetType/:targetId')
async getStatus(
@CurrentUser() user: JwtPayload,
@Param('targetId') targetId: string,
@Param('targetType') targetType: 'post' | 'comment',
) {
return this.likesService.getStatus(user.sub, { targetId, targetType });
}
@Post('toggle')
async toggle(@CurrentUser() user: JwtPayload, @Body() dto: ToggleLikeDto) {
return this.likesService.toggle(user.sub, dto);
}
}

عرض الملف

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CommentsModule } from '../comments/comments.module';
import { PostsModule } from '../posts/posts.module';
import { Like, LikeSchema } from './schemas/like.schema';
import { LikesController } from './likes.controller';
import { LikesRepository } from './likes.repository';
import { LikesService } from './likes.service';
@Module({
imports: [
MongooseModule.forFeature([{ name: Like.name, schema: LikeSchema }]),
PostsModule,
CommentsModule,
],
controllers: [LikesController],
providers: [LikesService, LikesRepository],
exports: [LikesService],
})
export class LikesModule {}

عرض الملف

@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Like, LikeDocument } from './schemas/like.schema';
@Injectable()
export class LikesRepository {
constructor(@InjectModel(Like.name) private readonly likeModel: Model<LikeDocument>) {}
async findOne(userId: string, targetId: string, targetType: 'post' | 'comment'): Promise<LikeDocument | null> {
return this.likeModel
.findOne({
userId: new Types.ObjectId(userId),
targetId: new Types.ObjectId(targetId),
targetType,
})
.exec();
}
async create(userId: string, targetId: string, targetType: 'post' | 'comment'): Promise<LikeDocument> {
return this.likeModel.create({
userId: new Types.ObjectId(userId),
targetId: new Types.ObjectId(targetId),
targetType,
});
}
async deleteById(id: string): Promise<void> {
await this.likeModel.findByIdAndDelete(id).exec();
}
}

عرض الملف

@@ -0,0 +1,30 @@
import { LikesService } from './likes.service';
describe('LikesService', () => {
it('returns liked false from status when target post no longer exists', async () => {
const likesRepository = {
findOne: jest.fn(),
};
const postsRepository = {
findById: jest.fn().mockResolvedValue(null),
};
const commentsRepository = {
findById: jest.fn(),
};
const service = new LikesService(
likesRepository as any,
postsRepository as any,
commentsRepository as any,
);
await expect(
service.getStatus('user-1', { targetId: '507f1f77bcf86cd799439011', targetType: 'post' }),
).resolves.toEqual({
liked: false,
targetId: '507f1f77bcf86cd799439011',
targetType: 'post',
});
expect(likesRepository.findOne).not.toHaveBeenCalled();
});
});

عرض الملف

@@ -0,0 +1,78 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { CommentsRepository } from '../comments/comments.repository';
import { PostsRepository } from '../posts/posts.repository';
import { LikesRepository } from './likes.repository';
import { ToggleLikeDto } from './dto/toggle-like.dto';
@Injectable()
export class LikesService {
constructor(
private readonly likesRepository: LikesRepository,
private readonly postsRepository: PostsRepository,
private readonly commentsRepository: CommentsRepository,
) {}
async toggle(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
return existing ? this.unlike(userId, dto) : this.like(userId, dto);
}
async like(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
await this.assertTargetExists(dto);
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
if (existing) {
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
}
await this.likesRepository.create(userId, dto.targetId, dto.targetType);
if (dto.targetType === 'post') {
await this.postsRepository.incrementLikesCount(dto.targetId, 1);
}
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
}
async unlike(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
await this.assertTargetExists(dto);
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
if (!existing) {
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
}
await this.likesRepository.deleteById(existing.id);
if (dto.targetType === 'post') {
await this.postsRepository.incrementLikesCount(dto.targetId, -1);
}
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
}
async getStatus(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
const targetExists = await this.targetExists(dto);
if (!targetExists) {
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
}
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
return { liked: !!existing, targetId: dto.targetId, targetType: dto.targetType };
}
private async assertTargetExists(dto: ToggleLikeDto): Promise<void> {
const targetExists = await this.targetExists(dto);
if (!targetExists) {
throw new NotFoundException(dto.targetType === 'post' ? 'Post not found' : 'Comment not found');
}
}
private async targetExists(dto: ToggleLikeDto): Promise<boolean> {
if (dto.targetType === 'post') {
const post = await this.postsRepository.findById(dto.targetId);
return !!post;
}
const comment = await this.commentsRepository.findById(dto.targetId);
return !!comment;
}
}

عرض الملف

@@ -0,0 +1,19 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
export type LikeDocument = HydratedDocument<Like>;
@Schema({ timestamps: true, versionKey: false })
export class Like {
@Prop({ type: Types.ObjectId, required: true, index: true })
userId!: Types.ObjectId;
@Prop({ type: Types.ObjectId, required: true, index: true })
targetId!: Types.ObjectId;
@Prop({ required: true, enum: ['post', 'comment'] })
targetType!: 'post' | 'comment';
}
export const LikeSchema = SchemaFactory.createForClass(Like);
LikeSchema.index({ userId: 1, targetId: 1, targetType: 1 }, { unique: true });