feat: expand backend admin marketplace and scaling
فشلت بعض الفحوصات
/ deploy (push) Failing after 1m22s

هذا الالتزام موجود في:
2026-05-14 16:17:12 +03:00
الأصل 0e76a4a9fc
التزام 5bd5e19a89
158 ملفات معدلة مع 19563 إضافات و3315 حذوفات

عرض الملف

@@ -25,14 +25,14 @@ export class FollowsController {
@UseGuards(JwtAuthGuard)
@Get('followers/:userId')
async followers(@Param('userId') userId: string, @Query() query: PaginationQueryDto) {
return this.followsService.getFollowers(userId, query.page, query.limit);
return this.followsService.getFollowers(userId, query);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('following/:userId')
async following(@Param('userId') userId: string, @Query() query: PaginationQueryDto) {
return this.followsService.getFollowing(userId, query.page, query.limit);
return this.followsService.getFollowing(userId, query);
}
@ApiBearerAuth()
@@ -47,6 +47,6 @@ export class FollowsController {
@Get('suggestions')
@Throttle(60, 60_000)
async suggestions(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) {
return this.followsService.getSuggestions(user.sub, query.page, query.limit);
return this.followsService.getSuggestions(user.sub, query);
}
}

عرض الملف

@@ -38,12 +38,17 @@ export class FollowsRepository {
await this.followModel.findByIdAndDelete(id, { session }).exec();
}
async findMany(filter: FilterQuery<FollowDocument>, skip: number, limit: number): Promise<FollowDocument[]> {
async findMany(
filter: FilterQuery<FollowDocument>,
skip: number,
limit: number,
sort: Record<string, 1 | -1> = { createdAt: -1 },
): Promise<FollowDocument[]> {
return this.followModel
.find(filter)
.populate({ path: 'followerId', select: 'name username stageName avatar isVerified isDisabled' })
.populate({ path: 'followingId', select: 'name username stageName avatar isVerified isDisabled' })
.sort({ createdAt: -1 })
.sort(sort)
.skip(skip)
.limit(limit)
.exec();

عرض الملف

@@ -26,6 +26,7 @@ describe('FollowsService', () => {
followsRepository as any,
usersRepository as any,
outboxService as any,
{ bumpGlobalVersion: jest.fn().mockResolvedValue(1) } as any,
);
await expect(service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({

عرض الملف

@@ -1,5 +1,9 @@
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { PaginationQueryDto } from '../../common/dto/pagination-query.dto';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { OutboxService } from '../outbox/outbox.service';
import { UsersRepository } from '../users/users.repository';
import { UserDocument } from '../users/schemas/user.schema';
@@ -14,6 +18,7 @@ export class FollowsService {
private readonly followsRepository: FollowsRepository,
private readonly usersRepository: UsersRepository,
private readonly outboxService: OutboxService,
private readonly feedVersionService: FeedVersionService,
) {}
async toggleFollow(currentUserId: string, dto: ToggleFollowDto) {
@@ -37,11 +42,13 @@ export class FollowsService {
if (existing) {
await this.followsRepository.deleteById(existing.id);
await this.syncFollowCounts(currentUserId, targetUserId);
await this.feedVersionService.bumpGlobalVersion();
return { following: false };
}
const follow = await this.followsRepository.create(currentUserId, targetUserId);
await this.syncFollowCounts(currentUserId, targetUserId);
await this.feedVersionService.bumpGlobalVersion();
try {
await this.outboxService.enqueueFollowNotification(currentUserId, targetUserId, follow.id);
@@ -56,36 +63,40 @@ export class FollowsService {
return { following: true };
}
async getFollowers(userId: string, page = 1, limit = 20) {
async getFollowers(userId: 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.findMany({ followingId: userId }, skip, limit),
this.followsRepository.findMany({ followingId: userId }, skip, limit, sort),
this.followsRepository.count({ followingId: userId }),
]);
return {
items,
return buildPaginatedResponse(items, {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
};
offset: skip,
});
}
async getFollowing(userId: string, page = 1, limit = 20) {
async getFollowing(userId: 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.findMany({ followerId: userId }, skip, limit),
this.followsRepository.findMany({ followerId: userId }, skip, limit, sort),
this.followsRepository.count({ followerId: userId }),
]);
return {
items,
return buildPaginatedResponse(items, {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
};
offset: skip,
});
}
async getFollowStatus(currentUserId: string, targetUserId: string) {
@@ -104,7 +115,9 @@ export class FollowsService {
};
}
async getSuggestions(currentUserId: string, page = 1, limit = 20) {
async getSuggestions(currentUserId: string, query: PaginationQueryDto) {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const currentUser = await this.usersRepository.findById(currentUserId);
if (!currentUser) {
throw new NotFoundException('Current user not found');
@@ -128,6 +141,10 @@ export class FollowsService {
}))
.sort((a, b) => b.score - a.score || b.user.followersCount - a.user.followersCount);
if (query.sortOrder === 'asc') {
ranked.reverse();
}
const total = ranked.length;
const skip = (page - 1) * limit;
const items = ranked.slice(skip, skip + limit).map((entry) => ({
@@ -136,13 +153,12 @@ export class FollowsService {
reasons: this.buildSuggestionReasons(currentUser, entry.user),
}));
return {
items,
return buildPaginatedResponse(items, {
page,
limit,
total,
totalPages: Math.ceil(total / limit) || 1,
};
offset: skip,
});
}
private calculateSuggestionScore(currentUser: UserDocument, candidate: UserDocument): number {