efficiency & performance fine tuning
This commit is contained in:
33
src/friends/events/friend.events.ts
Normal file
33
src/friends/events/friend.events.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { FriendRequest, User } from '@prisma/client';
|
||||
|
||||
export enum FriendEvents {
|
||||
REQUEST_RECEIVED = 'friend.request.received',
|
||||
REQUEST_ACCEPTED = 'friend.request.accepted',
|
||||
REQUEST_DENIED = 'friend.request.denied',
|
||||
UNFRIENDED = 'friend.unfriended',
|
||||
}
|
||||
|
||||
export type FriendRequestWithRelations = FriendRequest & {
|
||||
sender: User;
|
||||
receiver: User;
|
||||
};
|
||||
|
||||
export interface FriendRequestReceivedEvent {
|
||||
userId: string;
|
||||
friendRequest: FriendRequestWithRelations;
|
||||
}
|
||||
|
||||
export interface FriendRequestAcceptedEvent {
|
||||
userId: string;
|
||||
friendRequest: FriendRequestWithRelations;
|
||||
}
|
||||
|
||||
export interface FriendRequestDeniedEvent {
|
||||
userId: string;
|
||||
friendRequest: FriendRequestWithRelations;
|
||||
}
|
||||
|
||||
export interface UnfriendedEvent {
|
||||
userId: string;
|
||||
friendId: string;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { FriendsController } from './friends.controller';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { StateGateway } from '../ws/state/state.gateway';
|
||||
// StateGateway removed
|
||||
|
||||
enum FriendRequestStatus {
|
||||
PENDING = 'PENDING',
|
||||
@@ -85,21 +86,21 @@ describe('FriendsController', () => {
|
||||
ensureUserExists: jest.fn(),
|
||||
};
|
||||
|
||||
const mockStateGateway = {
|
||||
emitFriendRequestReceived: jest.fn(),
|
||||
emitFriendRequestAccepted: jest.fn(),
|
||||
emitFriendRequestDenied: jest.fn(),
|
||||
emitUnfriended: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 60000,
|
||||
limit: 10,
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [FriendsController],
|
||||
providers: [
|
||||
{ provide: FriendsService, useValue: mockFriendsService },
|
||||
{ provide: UsersService, useValue: mockUsersService },
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: StateGateway, useValue: mockStateGateway },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -140,7 +141,7 @@ describe('FriendsController', () => {
|
||||
});
|
||||
|
||||
describe('sendFriendRequest', () => {
|
||||
it('should send friend request and emit WebSocket event', async () => {
|
||||
it('should send friend request', async () => {
|
||||
mockFriendsService.sendFriendRequest.mockResolvedValue(mockFriendRequest);
|
||||
|
||||
const result = await controller.sendFriendRequest(
|
||||
@@ -170,10 +171,6 @@ describe('FriendsController', () => {
|
||||
'user-1',
|
||||
'user-2',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestReceived).toHaveBeenCalledWith(
|
||||
'user-2',
|
||||
mockFriendRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,7 +207,7 @@ describe('FriendsController', () => {
|
||||
});
|
||||
|
||||
describe('acceptFriendRequest', () => {
|
||||
it('should accept friend request and emit WebSocket event', async () => {
|
||||
it('should accept friend request', async () => {
|
||||
const acceptedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.ACCEPTED,
|
||||
@@ -227,15 +224,11 @@ describe('FriendsController', () => {
|
||||
'request-1',
|
||||
'user-1',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestAccepted).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
acceptedRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('denyFriendRequest', () => {
|
||||
it('should deny friend request and emit WebSocket event', async () => {
|
||||
it('should deny friend request', async () => {
|
||||
const deniedRequest = {
|
||||
...mockFriendRequest,
|
||||
status: FriendRequestStatus.DENIED,
|
||||
@@ -252,10 +245,6 @@ describe('FriendsController', () => {
|
||||
'request-1',
|
||||
'user-1',
|
||||
);
|
||||
expect(mockStateGateway.emitFriendRequestDenied).toHaveBeenCalledWith(
|
||||
'user-1',
|
||||
deniedRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,7 +271,7 @@ describe('FriendsController', () => {
|
||||
});
|
||||
|
||||
describe('unfriend', () => {
|
||||
it('should unfriend user and emit WebSocket event', async () => {
|
||||
it('should unfriend user', async () => {
|
||||
mockFriendsService.unfriend.mockResolvedValue(undefined);
|
||||
|
||||
await controller.unfriend('user-2', mockAuthUser);
|
||||
@@ -291,10 +280,6 @@ describe('FriendsController', () => {
|
||||
'user-1',
|
||||
'user-2',
|
||||
);
|
||||
expect(mockStateGateway.emitUnfriended).toHaveBeenCalledWith(
|
||||
'user-2',
|
||||
'user-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ApiUnauthorizedResponse,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { ThrottlerGuard, Throttle } from '@nestjs/throttler';
|
||||
import { User, FriendRequest } from '@prisma/client';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
@@ -40,7 +41,6 @@ type FriendRequestWithRelations = FriendRequest & {
|
||||
receiver: User;
|
||||
};
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { StateGateway } from '../ws/state/state.gateway';
|
||||
|
||||
@ApiTags('friends')
|
||||
@Controller('friends')
|
||||
@@ -53,10 +53,11 @@ export class FriendsController {
|
||||
private readonly friendsService: FriendsService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly stateGateway: StateGateway,
|
||||
) {}
|
||||
|
||||
@Get('search')
|
||||
@UseGuards(ThrottlerGuard)
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
||||
@ApiOperation({
|
||||
summary: 'Search users by username',
|
||||
description: 'Search for users by username to send friend requests',
|
||||
@@ -137,11 +138,6 @@ export class FriendsController {
|
||||
sendRequestDto.receiverId,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestReceived(
|
||||
sendRequestDto.receiverId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@@ -236,11 +232,6 @@ export class FriendsController {
|
||||
user.id,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestAccepted(
|
||||
friendRequest.senderId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@@ -283,11 +274,6 @@ export class FriendsController {
|
||||
user.id,
|
||||
);
|
||||
|
||||
this.stateGateway.emitFriendRequestDenied(
|
||||
friendRequest.senderId,
|
||||
friendRequest,
|
||||
);
|
||||
|
||||
return this.mapFriendRequestToDto(friendRequest);
|
||||
}
|
||||
|
||||
@@ -360,8 +346,6 @@ export class FriendsController {
|
||||
this.logger.log(`User ${user.id} unfriending user ${friendId}`);
|
||||
|
||||
await this.friendsService.unfriend(user.id, friendId);
|
||||
|
||||
this.stateGateway.emitUnfriended(friendId, user.id);
|
||||
}
|
||||
|
||||
private mapFriendRequestToDto(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { FriendsService } from './friends.service';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ enum FriendRequestStatus {
|
||||
|
||||
describe('FriendsService', () => {
|
||||
let service: FriendsService;
|
||||
let eventEmitter: EventEmitter2;
|
||||
|
||||
const mockUser1 = {
|
||||
id: 'user-1',
|
||||
@@ -82,6 +84,10 @@ describe('FriendsService', () => {
|
||||
$transaction: jest.fn(),
|
||||
};
|
||||
|
||||
const mockEventEmitter = {
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
@@ -90,10 +96,15 @@ describe('FriendsService', () => {
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
{
|
||||
provide: EventEmitter2,
|
||||
useValue: mockEventEmitter,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<FriendsService>(FriendsService);
|
||||
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -110,6 +121,12 @@ describe('FriendsService', () => {
|
||||
mockPrismaService.friendRequest.create.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
// Mock transaction implementation
|
||||
mockPrismaService.$transaction.mockImplementation(
|
||||
async (callback: (prisma: any) => Promise<any>) => {
|
||||
return (await callback(mockPrismaService)) as unknown;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await service.sendFriendRequest('user-1', 'user-2');
|
||||
|
||||
@@ -128,6 +145,7 @@ describe('FriendsService', () => {
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
expect(mockEventEmitter.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to send request to self', async () => {
|
||||
@@ -141,6 +159,11 @@ describe('FriendsService', () => {
|
||||
|
||||
it('should throw NotFoundException when receiver does not exist', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(null);
|
||||
mockPrismaService.$transaction.mockImplementation(
|
||||
async (callback: (prisma: any) => Promise<any>) => {
|
||||
return (await callback(mockPrismaService)) as unknown;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'nonexistent'),
|
||||
@@ -153,6 +176,11 @@ describe('FriendsService', () => {
|
||||
it('should throw ConflictException when users are already friends', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser2);
|
||||
mockPrismaService.friendship.findFirst.mockResolvedValue(mockFriendship);
|
||||
mockPrismaService.$transaction.mockImplementation(
|
||||
async (callback: (prisma: any) => Promise<any>) => {
|
||||
return (await callback(mockPrismaService)) as unknown;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
@@ -168,6 +196,11 @@ describe('FriendsService', () => {
|
||||
mockPrismaService.friendRequest.findFirst.mockResolvedValue(
|
||||
mockFriendRequest,
|
||||
);
|
||||
mockPrismaService.$transaction.mockImplementation(
|
||||
async (callback: (prisma: any) => Promise<any>) => {
|
||||
return (await callback(mockPrismaService)) as unknown;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
@@ -185,6 +218,11 @@ describe('FriendsService', () => {
|
||||
senderId: 'user-2',
|
||||
receiverId: 'user-1',
|
||||
});
|
||||
mockPrismaService.$transaction.mockImplementation(
|
||||
async (callback: (prisma: any) => Promise<any>) => {
|
||||
return (await callback(mockPrismaService)) as unknown;
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.sendFriendRequest('user-1', 'user-2'),
|
||||
@@ -259,6 +297,7 @@ describe('FriendsService', () => {
|
||||
|
||||
expect(result).toEqual(acceptedRequest);
|
||||
expect(mockPrismaService.$transaction).toHaveBeenCalled();
|
||||
expect(mockEventEmitter.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when request does not exist', async () => {
|
||||
@@ -320,6 +359,7 @@ describe('FriendsService', () => {
|
||||
expect(mockPrismaService.friendRequest.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'request-1' },
|
||||
});
|
||||
expect(mockEventEmitter.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when request does not exist', async () => {
|
||||
@@ -392,6 +432,7 @@ describe('FriendsService', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(mockEventEmitter.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException when trying to unfriend self', async () => {
|
||||
|
||||
@@ -5,8 +5,16 @@ import {
|
||||
Logger,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { PrismaService } from '../database/prisma.service';
|
||||
import { User, FriendRequest, FriendRequestStatus } from '@prisma/client';
|
||||
import {
|
||||
FriendEvents,
|
||||
FriendRequestReceivedEvent,
|
||||
FriendRequestAcceptedEvent,
|
||||
FriendRequestDeniedEvent,
|
||||
UnfriendedEvent,
|
||||
} from './events/friend.events';
|
||||
|
||||
export type FriendRequestWithRelations = FriendRequest & {
|
||||
sender: User;
|
||||
@@ -17,7 +25,10 @@ export type FriendRequestWithRelations = FriendRequest & {
|
||||
export class FriendsService {
|
||||
private readonly logger = new Logger(FriendsService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
async sendFriendRequest(
|
||||
senderId: string,
|
||||
@@ -27,66 +38,82 @@ export class FriendsService {
|
||||
throw new BadRequestException('Cannot send friend request to yourself');
|
||||
}
|
||||
|
||||
const receiver = await this.prisma.user.findUnique({
|
||||
where: { id: receiverId },
|
||||
});
|
||||
const friendRequest = await this.prisma.$transaction(async (tx) => {
|
||||
const receiver = await tx.user.findUnique({
|
||||
where: { id: receiverId },
|
||||
});
|
||||
|
||||
if (!receiver) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const existingFriendship = await this.areFriends(senderId, receiverId);
|
||||
if (existingFriendship) {
|
||||
throw new ConflictException('You are already friends with this user');
|
||||
}
|
||||
|
||||
const existingRequest = await this.prisma.friendRequest.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ senderId, receiverId },
|
||||
{
|
||||
senderId: receiverId,
|
||||
receiverId: senderId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRequest) {
|
||||
if (existingRequest.status === FriendRequestStatus.PENDING) {
|
||||
if (existingRequest.senderId === senderId) {
|
||||
throw new ConflictException(
|
||||
'You already sent a friend request to this user',
|
||||
);
|
||||
} else {
|
||||
throw new ConflictException(
|
||||
'This user already sent you a friend request',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If there's an existing request that is not pending (accepted or denied), delete it so a new one can be created
|
||||
await this.prisma.friendRequest.delete({
|
||||
where: { id: existingRequest.id },
|
||||
});
|
||||
if (!receiver) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
}
|
||||
|
||||
const friendRequest = await this.prisma.friendRequest.create({
|
||||
data: {
|
||||
senderId,
|
||||
receiverId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
// Check for existing friendship using the transaction client
|
||||
const existingFriendship = await tx.friendship.findFirst({
|
||||
where: {
|
||||
userId: senderId,
|
||||
friendId: receiverId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingFriendship) {
|
||||
throw new ConflictException('You are already friends with this user');
|
||||
}
|
||||
|
||||
const existingRequest = await tx.friendRequest.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ senderId, receiverId },
|
||||
{
|
||||
senderId: receiverId,
|
||||
receiverId: senderId,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRequest) {
|
||||
if (existingRequest.status === FriendRequestStatus.PENDING) {
|
||||
if (existingRequest.senderId === senderId) {
|
||||
throw new ConflictException(
|
||||
'You already sent a friend request to this user',
|
||||
);
|
||||
} else {
|
||||
throw new ConflictException(
|
||||
'This user already sent you a friend request',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If there's an existing request that is not pending (accepted or denied), delete it so a new one can be created
|
||||
await tx.friendRequest.delete({
|
||||
where: { id: existingRequest.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await tx.friendRequest.create({
|
||||
data: {
|
||||
senderId,
|
||||
receiverId,
|
||||
status: FriendRequestStatus.PENDING,
|
||||
},
|
||||
include: {
|
||||
sender: true,
|
||||
receiver: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Friend request sent from ${senderId} to ${receiverId} (ID: ${friendRequest.id})`,
|
||||
);
|
||||
|
||||
// Emit event
|
||||
const event: FriendRequestReceivedEvent = {
|
||||
userId: receiverId,
|
||||
friendRequest,
|
||||
};
|
||||
this.eventEmitter.emit(FriendEvents.REQUEST_RECEIVED, event);
|
||||
|
||||
return friendRequest;
|
||||
}
|
||||
|
||||
@@ -183,6 +210,13 @@ export class FriendsService {
|
||||
`Friend request ${requestId} accepted. Users ${friendRequest.senderId} and ${friendRequest.receiverId} are now friends`,
|
||||
);
|
||||
|
||||
// Emit event
|
||||
const event: FriendRequestAcceptedEvent = {
|
||||
userId: friendRequest.senderId,
|
||||
friendRequest: result,
|
||||
};
|
||||
this.eventEmitter.emit(FriendEvents.REQUEST_ACCEPTED, event);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -227,6 +261,13 @@ export class FriendsService {
|
||||
|
||||
this.logger.log(`Friend request ${requestId} denied by user ${userId}`);
|
||||
|
||||
// Emit event
|
||||
const event: FriendRequestDeniedEvent = {
|
||||
userId: friendRequest.senderId,
|
||||
friendRequest: result,
|
||||
};
|
||||
this.eventEmitter.emit(FriendEvents.REQUEST_DENIED, event);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -268,6 +309,13 @@ export class FriendsService {
|
||||
});
|
||||
|
||||
this.logger.log(`User ${userId} unfriended user ${friendId}`);
|
||||
|
||||
// Emit event
|
||||
const event: UnfriendedEvent = {
|
||||
userId: friendId,
|
||||
friendId: userId,
|
||||
};
|
||||
this.eventEmitter.emit(FriendEvents.UNFRIENDED, event);
|
||||
}
|
||||
|
||||
async areFriends(userId: string, friendId: string): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user