feat: stabilize social backend contracts
هذا الالتزام موجود في:
58
src/modules/follows/follows-users.controller.ts
Normal file
58
src/modules/follows/follows-users.controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationQueryDto } from '../../common/dto/pagination-query.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { JwtPayload } from '../../common/interfaces/jwt-payload.interface';
|
||||
import { FollowsService } from './follows.service';
|
||||
|
||||
@ApiTags('Users')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('users')
|
||||
export class FollowsUsersController {
|
||||
constructor(private readonly followsService: FollowsService) {}
|
||||
|
||||
@Get('me/followers')
|
||||
async myFollowers(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) {
|
||||
return this.followsService.getFollowers(user.sub, query, user.sub);
|
||||
}
|
||||
|
||||
@Get('me/following')
|
||||
async myFollowing(@CurrentUser() user: JwtPayload, @Query() query: PaginationQueryDto) {
|
||||
return this.followsService.getFollowing(user.sub, query, user.sub);
|
||||
}
|
||||
|
||||
@Post(':userId/follow')
|
||||
async followUser(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) {
|
||||
return this.followsService.followUser(user.sub, targetUserId);
|
||||
}
|
||||
|
||||
@Delete(':userId/follow')
|
||||
async unfollowUser(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) {
|
||||
return this.followsService.unfollowUser(user.sub, targetUserId);
|
||||
}
|
||||
|
||||
@Get(':userId/follow-status')
|
||||
async followStatus(@CurrentUser() user: JwtPayload, @Param('userId') targetUserId: string) {
|
||||
return this.followsService.getFollowStatus(user.sub, targetUserId);
|
||||
}
|
||||
|
||||
@Get(':userId/followers')
|
||||
async followers(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('userId') targetUserId: string,
|
||||
@Query() query: PaginationQueryDto,
|
||||
) {
|
||||
return this.followsService.getFollowers(targetUserId, query, user.sub);
|
||||
}
|
||||
|
||||
@Get(':userId/following')
|
||||
async following(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('userId') targetUserId: string,
|
||||
@Query() query: PaginationQueryDto,
|
||||
) {
|
||||
return this.followsService.getFollowing(targetUserId, query, user.sub);
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,23 @@ export class FollowsController {
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('followers/:userId')
|
||||
async followers(@Param('userId') userId: string, @Query() query: PaginationQueryDto) {
|
||||
return this.followsService.getFollowers(userId, query);
|
||||
async followers(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('userId') userId: string,
|
||||
@Query() query: PaginationQueryDto,
|
||||
) {
|
||||
return this.followsService.getFollowers(userId, query, user.sub);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('following/:userId')
|
||||
async following(@Param('userId') userId: string, @Query() query: PaginationQueryDto) {
|
||||
return this.followsService.getFollowing(userId, query);
|
||||
async following(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('userId') userId: string,
|
||||
@Query() query: PaginationQueryDto,
|
||||
) {
|
||||
return this.followsService.getFollowing(userId, query, user.sub);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { BlocksModule } from '../blocks/blocks.module';
|
||||
import { OutboxModule } from '../outbox/outbox.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { FollowsController } from './follows.controller';
|
||||
import { FollowsUsersController } from './follows-users.controller';
|
||||
import { FollowsService } from './follows.service';
|
||||
import { FollowsRepository } from './follows.repository';
|
||||
import { FollowRequest, FollowRequestSchema } from './schemas/follow-request.schema';
|
||||
@@ -10,6 +12,7 @@ import { Follow, FollowSchema } from './schemas/follow.schema';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BlocksModule,
|
||||
UsersModule,
|
||||
OutboxModule,
|
||||
MongooseModule.forFeature([
|
||||
@@ -23,7 +26,7 @@ import { Follow, FollowSchema } from './schemas/follow.schema';
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [FollowsController],
|
||||
controllers: [FollowsController, FollowsUsersController],
|
||||
providers: [FollowsService, FollowsRepository],
|
||||
exports: [FollowsService, FollowsRepository],
|
||||
})
|
||||
|
||||
@@ -39,6 +39,15 @@ export class FollowsRepository {
|
||||
return follow;
|
||||
}
|
||||
|
||||
isDuplicateKeyError(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
(error as { code?: number }).code === 11000
|
||||
);
|
||||
}
|
||||
|
||||
async deleteById(id: string, session?: ClientSession): Promise<void> {
|
||||
await this.followModel.findByIdAndDelete(id, { session }).exec();
|
||||
}
|
||||
@@ -83,6 +92,28 @@ export class FollowsRepository {
|
||||
.exec();
|
||||
}
|
||||
|
||||
async findFollowerUserIds(followingId: string, skip: number, limit: number, sort: Record<string, 1 | -1>) {
|
||||
return this.followModel
|
||||
.find({ followingId: new Types.ObjectId(followingId) })
|
||||
.select({ followerId: 1 })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean()
|
||||
.exec();
|
||||
}
|
||||
|
||||
async findFollowingUserIds(followerId: string, skip: number, limit: number, sort: Record<string, 1 | -1>) {
|
||||
return this.followModel
|
||||
.find({ followerId: new Types.ObjectId(followerId) })
|
||||
.select({ followingId: 1 })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean()
|
||||
.exec();
|
||||
}
|
||||
|
||||
async upsertPendingRequest(requesterId: string, targetUserId: string): Promise<FollowRequestDocument> {
|
||||
return this.followRequestModel
|
||||
.findOneAndUpdate(
|
||||
|
||||
@@ -21,12 +21,16 @@ describe('FollowsService', () => {
|
||||
const outboxService = {
|
||||
enqueueFollowNotification: jest.fn().mockRejectedValue(new Error('socket down')),
|
||||
};
|
||||
const blocksRepository = {
|
||||
findAnyBetween: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const service = new FollowsService(
|
||||
followsRepository as any,
|
||||
usersRepository as any,
|
||||
outboxService as any,
|
||||
{ bumpGlobalVersion: jest.fn().mockResolvedValue(1) } as any,
|
||||
blocksRepository as any,
|
||||
);
|
||||
|
||||
await expect(service.toggleFollow(currentUserId, { targetUserId })).resolves.toEqual({
|
||||
@@ -40,4 +44,51 @@ describe('FollowsService', () => {
|
||||
'follow-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents users from following themselves', async () => {
|
||||
const userId = '507f1f77bcf86cd799439011';
|
||||
const service = new FollowsService(
|
||||
{} as any,
|
||||
{} as any,
|
||||
{} as any,
|
||||
{ bumpGlobalVersion: jest.fn() } as any,
|
||||
{ findAnyBetween: jest.fn() } as any,
|
||||
);
|
||||
|
||||
await expect(service.followUser(userId, userId)).rejects.toThrow('You cannot follow yourself');
|
||||
});
|
||||
|
||||
it('returns already-following response without creating duplicate notification', async () => {
|
||||
const currentUserId = '507f1f77bcf86cd799439011';
|
||||
const targetUserId = '507f191e810c19729de860ea';
|
||||
const followsRepository = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 'existing-follow' }),
|
||||
count: jest.fn().mockResolvedValueOnce(1).mockResolvedValueOnce(2),
|
||||
create: jest.fn(),
|
||||
};
|
||||
const usersRepository = {
|
||||
findById: jest.fn().mockResolvedValue({ id: targetUserId, isDisabled: false, isPrivate: false }),
|
||||
setFollowingCount: jest.fn(),
|
||||
setFollowersCount: jest.fn(),
|
||||
};
|
||||
const outboxService = {
|
||||
enqueueFollowNotification: jest.fn(),
|
||||
};
|
||||
const service = new FollowsService(
|
||||
followsRepository as any,
|
||||
usersRepository as any,
|
||||
outboxService as any,
|
||||
{ bumpGlobalVersion: jest.fn() } as any,
|
||||
{ findAnyBetween: jest.fn().mockResolvedValue(null) } as any,
|
||||
);
|
||||
|
||||
await expect(service.followUser(currentUserId, targetUserId)).resolves.toMatchObject({
|
||||
message: 'Already following user',
|
||||
isFollowing: true,
|
||||
followersCount: 2,
|
||||
followingCount: 1,
|
||||
});
|
||||
expect(followsRepository.create).not.toHaveBeenCalled();
|
||||
expect(outboxService.enqueueFollowNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { BlocksRepository } from '../blocks/blocks.repository';
|
||||
import { OutboxService } from '../outbox/outbox.service';
|
||||
import { UsersRepository } from '../users/users.repository';
|
||||
import { UserDocument } from '../users/schemas/user.schema';
|
||||
@@ -19,23 +20,19 @@ export class FollowsService {
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly outboxService: OutboxService,
|
||||
private readonly feedVersionService: FeedVersionService,
|
||||
private readonly blocksRepository: BlocksRepository,
|
||||
) {}
|
||||
|
||||
async toggleFollow(currentUserId: string, dto: ToggleFollowDto) {
|
||||
const targetUserId = dto.targetUserId;
|
||||
|
||||
if (!Types.ObjectId.isValid(targetUserId)) {
|
||||
throw new BadRequestException('Invalid target user id');
|
||||
}
|
||||
|
||||
if (currentUserId === targetUserId) {
|
||||
throw new BadRequestException('You cannot follow yourself');
|
||||
}
|
||||
|
||||
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||
if (!targetUser) {
|
||||
throw new NotFoundException('Target user not found');
|
||||
}
|
||||
const targetUser = await this.findActiveTargetUser(targetUserId);
|
||||
|
||||
const existing = await this.followsRepository.findOne(currentUserId, targetUserId);
|
||||
|
||||
@@ -46,37 +43,107 @@ export class FollowsService {
|
||||
return { following: false };
|
||||
}
|
||||
|
||||
const block = await this.blocksRepository.findAnyBetween(currentUserId, targetUserId);
|
||||
if (block) {
|
||||
throw new BadRequestException('You cannot follow this user');
|
||||
}
|
||||
|
||||
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);
|
||||
let followId: string | null = null;
|
||||
try {
|
||||
const follow = await this.followsRepository.create(currentUserId, targetUserId);
|
||||
followId = follow.id;
|
||||
} catch (error) {
|
||||
if (!this.followsRepository.isDuplicateKeyError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await this.syncFollowCounts(currentUserId, targetUserId);
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
|
||||
try {
|
||||
await this.outboxService.enqueueFollowNotification(currentUserId, targetUserId, follow.id);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Follow notification failed for actor=${currentUserId} recipient=${targetUserId}: ${
|
||||
error instanceof Error ? error.message : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
if (followId) {
|
||||
await this.enqueueFollowNotification(currentUserId, targetUserId, followId);
|
||||
}
|
||||
|
||||
return { following: true };
|
||||
}
|
||||
|
||||
async getFollowers(userId: string, query: PaginationQueryDto) {
|
||||
async followUser(currentUserId: string, targetUserId: string) {
|
||||
await this.assertCanFollowTarget(currentUserId, targetUserId);
|
||||
const targetUser = await this.findActiveTargetUser(targetUserId);
|
||||
|
||||
const existing = await this.followsRepository.findOne(currentUserId, targetUserId);
|
||||
if (existing) {
|
||||
const counts = await this.syncFollowCounts(currentUserId, targetUserId);
|
||||
return this.buildFollowActionResponse('Already following user', true, targetUserId, counts);
|
||||
}
|
||||
|
||||
if (targetUser.isPrivate) {
|
||||
const request = await this.followsRepository.upsertPendingRequest(currentUserId, targetUserId);
|
||||
const counts = await this.getFollowCounts(currentUserId, targetUserId);
|
||||
return {
|
||||
...this.buildFollowActionResponse('Follow request sent', false, targetUserId, counts),
|
||||
requested: true,
|
||||
requestId: request.id,
|
||||
};
|
||||
}
|
||||
|
||||
let followId: string | null = null;
|
||||
try {
|
||||
const follow = await this.followsRepository.create(currentUserId, targetUserId);
|
||||
followId = follow.id;
|
||||
} catch (error) {
|
||||
if (!this.followsRepository.isDuplicateKeyError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const counts = await this.syncFollowCounts(currentUserId, targetUserId);
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
|
||||
if (followId) {
|
||||
await this.enqueueFollowNotification(currentUserId, targetUserId, followId);
|
||||
}
|
||||
|
||||
return this.buildFollowActionResponse('User followed successfully', true, targetUserId, counts);
|
||||
}
|
||||
|
||||
async unfollowUser(currentUserId: string, targetUserId: string) {
|
||||
if (!Types.ObjectId.isValid(targetUserId)) {
|
||||
throw new BadRequestException('Invalid target user id');
|
||||
}
|
||||
if (currentUserId === targetUserId) {
|
||||
throw new BadRequestException('You cannot follow yourself');
|
||||
}
|
||||
|
||||
const existing = await this.followsRepository.findOne(currentUserId, targetUserId);
|
||||
if (existing) {
|
||||
await this.followsRepository.deleteById(existing.id);
|
||||
await this.feedVersionService.bumpGlobalVersion();
|
||||
}
|
||||
|
||||
const counts = await this.syncFollowCounts(currentUserId, targetUserId);
|
||||
return this.buildFollowActionResponse('User unfollowed successfully', false, targetUserId, counts);
|
||||
}
|
||||
|
||||
async getFollowers(userId: string, query: PaginationQueryDto, viewerUserId?: string) {
|
||||
await this.assertListTargetExists(userId);
|
||||
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, sort),
|
||||
const [followRows, total] = await Promise.all([
|
||||
this.followsRepository.findFollowerUserIds(userId, skip, limit, sort),
|
||||
this.followsRepository.count({ followingId: userId }),
|
||||
]);
|
||||
const items = await this.decorateFollowUsers(
|
||||
followRows.map((row) => row.followerId.toString()),
|
||||
viewerUserId,
|
||||
);
|
||||
|
||||
return buildPaginatedResponse(items, {
|
||||
page,
|
||||
@@ -86,15 +153,20 @@ export class FollowsService {
|
||||
});
|
||||
}
|
||||
|
||||
async getFollowing(userId: string, query: PaginationQueryDto) {
|
||||
async getFollowing(userId: string, query: PaginationQueryDto, viewerUserId?: string) {
|
||||
await this.assertListTargetExists(userId);
|
||||
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, sort),
|
||||
const [followRows, total] = await Promise.all([
|
||||
this.followsRepository.findFollowingUserIds(userId, skip, limit, sort),
|
||||
this.followsRepository.count({ followerId: userId }),
|
||||
]);
|
||||
const items = await this.decorateFollowUsers(
|
||||
followRows.map((row) => row.followingId.toString()),
|
||||
viewerUserId,
|
||||
);
|
||||
|
||||
return buildPaginatedResponse(items, {
|
||||
page,
|
||||
@@ -110,16 +182,28 @@ export class FollowsService {
|
||||
}
|
||||
|
||||
if (currentUserId === targetUserId) {
|
||||
return { following: false, targetUserId };
|
||||
return {
|
||||
following: false,
|
||||
isFollowing: false,
|
||||
isFollowedBy: false,
|
||||
isMutual: false,
|
||||
targetUserId,
|
||||
};
|
||||
}
|
||||
|
||||
const existing = await this.followsRepository.findOne(currentUserId, targetUserId);
|
||||
const [existing, reverseExisting] = await Promise.all([
|
||||
this.followsRepository.findOne(currentUserId, targetUserId),
|
||||
this.followsRepository.findOne(targetUserId, currentUserId),
|
||||
]);
|
||||
const pendingRequest =
|
||||
currentUserId === targetUserId
|
||||
? null
|
||||
: await this.followsRepository.findPendingRequest(currentUserId, targetUserId);
|
||||
return {
|
||||
following: !!existing,
|
||||
isFollowing: !!existing,
|
||||
isFollowedBy: !!reverseExisting,
|
||||
isMutual: !!existing && !!reverseExisting,
|
||||
requested: !!pendingRequest,
|
||||
targetUserId,
|
||||
};
|
||||
@@ -282,7 +366,10 @@ export class FollowsService {
|
||||
return count;
|
||||
}
|
||||
|
||||
private async syncFollowCounts(currentUserId: string, targetUserId: string): Promise<void> {
|
||||
private async syncFollowCounts(
|
||||
currentUserId: string,
|
||||
targetUserId: string,
|
||||
): Promise<{ followingCount: number; followersCount: number }> {
|
||||
const [followingCount, followersCount] = await Promise.all([
|
||||
this.followsRepository.count({ followerId: currentUserId }),
|
||||
this.followsRepository.count({ followingId: targetUserId }),
|
||||
@@ -292,5 +379,102 @@ export class FollowsService {
|
||||
this.usersRepository.setFollowingCount(currentUserId, followingCount),
|
||||
this.usersRepository.setFollowersCount(targetUserId, followersCount),
|
||||
]);
|
||||
|
||||
return { followingCount, followersCount };
|
||||
}
|
||||
|
||||
private async getFollowCounts(
|
||||
currentUserId: string,
|
||||
targetUserId: string,
|
||||
): Promise<{ followingCount: number; followersCount: number }> {
|
||||
const [followingCount, followersCount] = await Promise.all([
|
||||
this.followsRepository.count({ followerId: currentUserId }),
|
||||
this.followsRepository.count({ followingId: targetUserId }),
|
||||
]);
|
||||
|
||||
return { followingCount, followersCount };
|
||||
}
|
||||
|
||||
private async assertCanFollowTarget(currentUserId: string, targetUserId: string): Promise<void> {
|
||||
if (!Types.ObjectId.isValid(targetUserId)) {
|
||||
throw new BadRequestException('Invalid target user id');
|
||||
}
|
||||
|
||||
if (currentUserId === targetUserId) {
|
||||
throw new BadRequestException('You cannot follow yourself');
|
||||
}
|
||||
|
||||
const block = await this.blocksRepository.findAnyBetween(currentUserId, targetUserId);
|
||||
if (block) {
|
||||
throw new BadRequestException('You cannot follow this user');
|
||||
}
|
||||
}
|
||||
|
||||
private async findActiveTargetUser(targetUserId: string): Promise<UserDocument> {
|
||||
const targetUser = await this.usersRepository.findById(targetUserId);
|
||||
if (!targetUser || targetUser.isDisabled) {
|
||||
throw new NotFoundException('Target user not found');
|
||||
}
|
||||
return targetUser;
|
||||
}
|
||||
|
||||
private async assertListTargetExists(userId: string): Promise<void> {
|
||||
if (!Types.ObjectId.isValid(userId)) {
|
||||
throw new BadRequestException('Invalid user id');
|
||||
}
|
||||
await this.findActiveTargetUser(userId);
|
||||
}
|
||||
|
||||
private async enqueueFollowNotification(actorId: string, recipientId: string, followId: string): Promise<void> {
|
||||
try {
|
||||
await this.outboxService.enqueueFollowNotification(actorId, recipientId, followId);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Follow notification failed for actor=${actorId} recipient=${recipientId}: ${
|
||||
error instanceof Error ? error.message : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildFollowActionResponse(
|
||||
message: string,
|
||||
isFollowing: boolean,
|
||||
targetUserId: string,
|
||||
counts: { followingCount: number; followersCount: number },
|
||||
) {
|
||||
return {
|
||||
message,
|
||||
following: isFollowing,
|
||||
isFollowing,
|
||||
targetUserId,
|
||||
followersCount: counts.followersCount,
|
||||
followingCount: counts.followingCount,
|
||||
};
|
||||
}
|
||||
|
||||
private async decorateFollowUsers(userIds: string[], viewerUserId?: string) {
|
||||
const users = await this.usersRepository.findManyByIds(userIds);
|
||||
const usersById = new Map(users.map((user) => [user.id, user]));
|
||||
const viewerFollowingIds = viewerUserId ? new Set(await this.followsRepository.findFollowingIds(viewerUserId)) : new Set();
|
||||
|
||||
return userIds
|
||||
.map((id) => usersById.get(id))
|
||||
.filter((user): user is UserDocument => !!user)
|
||||
.map((user) => {
|
||||
const object = user.toObject();
|
||||
return {
|
||||
_id: object._id,
|
||||
name: object.name,
|
||||
stageName: object.stageName,
|
||||
username: object.username,
|
||||
avatar: object.avatar,
|
||||
isVerified: object.isVerified,
|
||||
isDisabled: object.isDisabled,
|
||||
followersCount: object.followersCount ?? 0,
|
||||
followingCount: object.followingCount ?? 0,
|
||||
isFollowing: viewerFollowingIds.has(user.id),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,5 @@ export class Follow {
|
||||
|
||||
export const FollowSchema = SchemaFactory.createForClass(Follow);
|
||||
FollowSchema.index({ followerId: 1, followingId: 1 }, { unique: true });
|
||||
FollowSchema.index({ followerId: 1, createdAt: -1 });
|
||||
FollowSchema.index({ followingId: 1, createdAt: -1 });
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم