Add Instagram-style social features and Postman collections

هذا الالتزام موجود في:
boutmoun123
2026-05-24 15:21:03 +03:00
الأصل fdc40192f7
التزام 367fce6557
56 ملفات معدلة مع 20266 إضافات و5965 حذوفات

عرض الملف

@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { Throttle } from '../../common/decorators/throttle.decorator';
@@ -49,4 +49,25 @@ export class FollowsController {
async suggestions(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) {
return this.followsService.getSuggestions(user.sub, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('requests')
async requests(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) {
return this.followsService.getPendingRequests(user.sub, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Patch('requests/:requestId/approve')
async approveRequest(@CurrentUser() user: JwtPayload, @Param('requestId') requestId: string) {
return this.followsService.approveRequest(user.sub, requestId);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Patch('requests/:requestId/reject')
async rejectRequest(@CurrentUser() user: JwtPayload, @Param('requestId') requestId: string) {
return this.followsService.rejectRequest(user.sub, requestId);
}
}

عرض الملف

@@ -5,6 +5,7 @@ import { UsersModule } from '../users/users.module';
import { FollowsController } from './follows.controller';
import { FollowsService } from './follows.service';
import { FollowsRepository } from './follows.repository';
import { FollowRequest, FollowRequestSchema } from './schemas/follow-request.schema';
import { Follow, FollowSchema } from './schemas/follow.schema';
@Module({
@@ -16,10 +17,14 @@ import { Follow, FollowSchema } from './schemas/follow.schema';
name: Follow.name,
schema: FollowSchema,
},
{
name: FollowRequest.name,
schema: FollowRequestSchema,
},
]),
],
controllers: [FollowsController],
providers: [FollowsService, FollowsRepository],
exports: [FollowsService],
exports: [FollowsService, FollowsRepository],
})
export class FollowsModule {}

عرض الملف

@@ -1,11 +1,16 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { ClientSession, FilterQuery, Model, Types } from 'mongoose';
import { FollowRequest, FollowRequestDocument } from './schemas/follow-request.schema';
import { Follow, FollowDocument } from './schemas/follow.schema';
@Injectable()
export class FollowsRepository {
constructor(@InjectModel(Follow.name) private readonly followModel: Model<FollowDocument>) {}
constructor(
@InjectModel(Follow.name) private readonly followModel: Model<FollowDocument>,
@InjectModel(FollowRequest.name)
private readonly followRequestModel: Model<FollowRequestDocument>,
) {}
async findOne(followerId: string, followingId: string): Promise<FollowDocument | null> {
return this.followModel
@@ -67,4 +72,66 @@ export class FollowsRepository {
return rows.map((row) => row.followingId.toString());
}
async findPendingRequest(requesterId: string, targetUserId: string): Promise<FollowRequestDocument | null> {
return this.followRequestModel
.findOne({
requesterId: new Types.ObjectId(requesterId),
targetUserId: new Types.ObjectId(targetUserId),
status: 'pending',
})
.exec();
}
async upsertPendingRequest(requesterId: string, targetUserId: string): Promise<FollowRequestDocument> {
return this.followRequestModel
.findOneAndUpdate(
{
requesterId: new Types.ObjectId(requesterId),
targetUserId: new Types.ObjectId(targetUserId),
},
{
requesterId: new Types.ObjectId(requesterId),
targetUserId: new Types.ObjectId(targetUserId),
status: 'pending',
},
{ new: true, upsert: true },
)
.exec();
}
async updateRequestStatus(
requestId: string,
targetUserId: string,
status: 'approved' | 'rejected',
): Promise<FollowRequestDocument | null> {
return this.followRequestModel
.findOneAndUpdate(
{ _id: new Types.ObjectId(requestId), targetUserId: new Types.ObjectId(targetUserId), status: 'pending' },
{ status },
{ new: true },
)
.exec();
}
async findPendingRequestsForTarget(
targetUserId: string,
skip: number,
limit: number,
sort: Record<string, 1 | -1> = { createdAt: -1 },
): Promise<FollowRequestDocument[]> {
return this.followRequestModel
.find({ targetUserId: new Types.ObjectId(targetUserId), status: 'pending' })
.populate({ path: 'requesterId', select: 'name username stageName avatar isVerified' })
.sort(sort)
.skip(skip)
.limit(limit)
.exec();
}
async countPendingRequestsForTarget(targetUserId: string): Promise<number> {
return this.followRequestModel
.countDocuments({ targetUserId: new Types.ObjectId(targetUserId), status: 'pending' })
.exec();
}
}

عرض الملف

@@ -46,6 +46,11 @@ export class FollowsService {
return { following: false };
}
if (targetUser.isPrivate) {
const request = await this.followsRepository.upsertPendingRequest(currentUserId, targetUserId);
return { following: false, requested: true, requestId: request.id };
}
const follow = await this.followsRepository.create(currentUserId, targetUserId);
await this.syncFollowCounts(currentUserId, targetUserId);
await this.feedVersionService.bumpGlobalVersion();
@@ -109,12 +114,64 @@ export class FollowsService {
}
const existing = await this.followsRepository.findOne(currentUserId, targetUserId);
const pendingRequest =
currentUserId === targetUserId
? null
: await this.followsRepository.findPendingRequest(currentUserId, targetUserId);
return {
following: !!existing,
requested: !!pendingRequest,
targetUserId,
};
}
async getPendingRequests(currentUserId: string, query: PaginationQueryDto) {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const skip = (page - 1) * limit;
const sort = { createdAt: resolveMongoSortDirection(query.sortOrder) } as Record<string, 1 | -1>;
const [items, total] = await Promise.all([
this.followsRepository.findPendingRequestsForTarget(currentUserId, skip, limit, sort),
this.followsRepository.countPendingRequestsForTarget(currentUserId),
]);
return buildPaginatedResponse(items, { page, limit, total, offset: skip });
}
async approveRequest(currentUserId: string, requestId: string) {
if (!Types.ObjectId.isValid(requestId)) {
throw new BadRequestException('Invalid follow request id');
}
const request = await this.followsRepository.updateRequestStatus(requestId, currentUserId, 'approved');
if (!request) {
throw new NotFoundException('Follow request not found');
}
const requesterId = request.requesterId.toString();
const existing = await this.followsRepository.findOne(requesterId, currentUserId);
if (!existing) {
await this.followsRepository.create(requesterId, currentUserId);
await this.syncFollowCounts(requesterId, currentUserId);
await this.feedVersionService.bumpGlobalVersion();
}
return { approved: true, following: true, requesterId };
}
async rejectRequest(currentUserId: string, requestId: string) {
if (!Types.ObjectId.isValid(requestId)) {
throw new BadRequestException('Invalid follow request id');
}
const request = await this.followsRepository.updateRequestStatus(requestId, currentUserId, 'rejected');
if (!request) {
throw new NotFoundException('Follow request not found');
}
return { rejected: true, requesterId: request.requesterId.toString() };
}
async getSuggestions(currentUserId: string, query: PaginationQueryDto) {
const page = query.page ?? 1;
const limit = query.limit ?? 20;

عرض الملف

@@ -0,0 +1,21 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
import { User } from '../../users/schemas/user.schema';
export type FollowRequestDocument = HydratedDocument<FollowRequest>;
@Schema({ timestamps: true, versionKey: false })
export class FollowRequest {
@Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true })
requesterId!: Types.ObjectId;
@Prop({ type: Types.ObjectId, ref: User.name, required: true, index: true })
targetUserId!: Types.ObjectId;
@Prop({ type: String, enum: ['pending', 'approved', 'rejected'], default: 'pending', index: true })
status!: 'pending' | 'approved' | 'rejected';
}
export const FollowRequestSchema = SchemaFactory.createForClass(FollowRequest);
FollowRequestSchema.index({ requesterId: 1, targetUserId: 1 }, { unique: true });
FollowRequestSchema.index({ targetUserId: 1, status: 1, createdAt: -1 });