Skip to content

Commit

Permalink
Merge pull request #2 from adelinaenache/feat/connections-controller
Browse files Browse the repository at this point in the history
feat: add connections controller and move specific endpoints from use…
  • Loading branch information
adelinaenache authored May 22, 2024
2 parents f59328f + c6d19f2 commit 5b9b1f0
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 121 deletions.
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PrismaModule } from '../prisma/prisma.module';
import { MailModule } from '../mail/mail.module';
import { ProviderModule } from '../provider/provider.module';
import { ReviewsModule } from 'src/reviews/reviews.module';
import { ConnectionsModule } from 'src/connections/connections.module';

@Module({
imports: [
Expand All @@ -23,6 +24,7 @@ import { ReviewsModule } from 'src/reviews/reviews.module';
MailModule,
ProviderModule,
ReviewsModule,
ConnectionsModule,
],
controllers: [AppController],
providers: [
Expand Down
17 changes: 17 additions & 0 deletions src/connections/DTO/Connection.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Type } from 'class-transformer';
import { IsDate } from 'class-validator';

export class ConnectionDto {
name?: string;
headline?: string;
profilePictureUrl?: string;
id: string;
email: string;
@Type(() => Date)
@IsDate()
joinedAt: Date;

reviewsCount: number;
connectionsCount: number;
isConnection: boolean;
}
18 changes: 18 additions & 0 deletions src/connections/connections.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConnectionsController } from './connections.controller';

describe('ConnectionsController', () => {
let controller: ConnectionsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ConnectionsController],
}).compile();

controller = module.get<ConnectionsController>(ConnectionsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
82 changes: 82 additions & 0 deletions src/connections/connections.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { CurrentUser } from 'src/decorators/current-user.decorator';
import { ConnectionsService } from './connections.service';
import { ConnectionDto } from './DTO/Connection.dto';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiNotFoundResponse,
} from '@nestjs/swagger';
import { User } from '@prisma/client';
import { Public } from 'src/decorators/public.decorator';

@ApiBearerAuth()
@Controller('connections')
export class ConnectionsController {
constructor(private connectionsService: ConnectionsService) {}
/**
*
* Get all of the current user's connections
*/
@Get()
async getAllConnections(@CurrentUser() user: User): Promise<ConnectionDto[]> {
return this.connectionsService.getUserConnections(user.id);
}

/**
* Get "suggested for review" connections.
*/
@Get('/suggested')
async getSuggestedConnections(
@CurrentUser() user: User,
): Promise<ConnectionDto[]> {
return this.connectionsService.getUserConnections(user.id);
}

/**
* Search an user by query.
*/
@Get('/search/:query')
@Public()
@ApiBadRequestResponse()
async searchUser(
@Param('query') query: string,
@CurrentUser() user?: User,
): Promise<ConnectionDto[]> {
return this.connectionsService.searchUsers(user?.id, query);
}

/**
* Create a connection between current User and another user.
*/
@Post('/connect/:userId')
async connectWithUser(
@CurrentUser() user: User,
@Param('userId') userId: string,
): Promise<ConnectionDto> {
return this.connectionsService.connectWithUser(user.id, userId);
}
/**
*
* Remove connection between current user and another user.
*/
@Delete('/connect/:userId')
async unconnectWithUser(
@CurrentUser() user: User,
@Param('userId') userId: string,
): Promise<ConnectionDto> {
return this.connectionsService.unconnectWithUser(user.id, userId);
}

/**
* Get a connection.
*/
@Get('/:userId')
@ApiNotFoundResponse()
async getUser(
@CurrentUser() user: User,
@Param('userId') userId: string,
): Promise<ConnectionDto> {
return this.connectionsService.getConnection(userId, user.id);
}
}
9 changes: 9 additions & 0 deletions src/connections/connections.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ConnectionsService } from './connections.service';
import { ConnectionsController } from './connections.controller';

@Module({
providers: [ConnectionsService],
controllers: [ConnectionsController],
})
export class ConnectionsModule {}
18 changes: 18 additions & 0 deletions src/connections/connections.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConnectionsService } from './connections.service';

describe('ConnectionsService', () => {
let service: ConnectionsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ConnectionsService],
}).compile();

service = module.get<ConnectionsService>(ConnectionsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
132 changes: 132 additions & 0 deletions src/connections/connections.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { User } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { ConnectionDto } from './DTO/Connection.dto';
import { ApiTags } from '@nestjs/swagger';

@Injectable()
@ApiTags('Connections controller')
export class ConnectionsService {
constructor(private readonly prisma: PrismaService) {}

// retuns an object to be used with prisma's include
private includeWithUserConnection(currentUserId?: User['id']) {
return {
_count: {
select: {
followings: true,
reviewsReceived: true,
},
},
followers: currentUserId
? {
where: {
followerId: currentUserId,
},
}
: false,
};
}

// transforms an user from db to a Connection DTO
private transformUserConnection(
user: User & { _count: { followings: number; reviewsReceived: number } } & {
followers?: { id: number; followerId: string; followingId: string }[];
},
): ConnectionDto {
return {
id: user.id,
email: user.email,
name: user.name,
headline: user.headline,
profilePictureUrl: user.profilePictureUrl,

joinedAt: user.joinedAt,

reviewsCount: user._count.reviewsReceived,
connectionsCount: user._count.followings,
isConnection: user.followers && user.followers.length !== 0,
};
}

async getConnection(connectionId: User['id'], currentUserId: User['id']) {
const user = await this.prisma.user.findUnique({
where: {
id: connectionId,
},
include: this.includeWithUserConnection(currentUserId),
});

if (!user) {
throw new NotFoundException(`User with ${connectionId} not found`);
}

return this.transformUserConnection(user);
}

async getUserConnections(userId: User['id']) {
const connections = await this.prisma.connection.findMany({
where: {
followerId: userId,
},
include: {
following: {
include: this.includeWithUserConnection(userId),
},
},
});

return connections.map((c) => this.transformUserConnection(c.following));
}

async connectWithUser(currentUserId: User['id'], userId: User['id']) {
await this.prisma.connection.upsert({
where: {
followerId_followingId: {
followerId: currentUserId,
followingId: userId,
},
},
update: {},
create: {
followerId: currentUserId,
followingId: userId,
},
});

return this.getConnection(userId, currentUserId);
}

async unconnectWithUser(currentUserId: User['id'], userId: User['id']) {
await this.prisma.connection.delete({
where: {
followerId_followingId: {
followerId: currentUserId,
followingId: userId,
},
},
});
return this.getConnection(userId, currentUserId);
}

async searchUsers(userId?: User['id'], searchTerm?: string) {
if (!searchTerm || searchTerm === '') {
throw new BadRequestException('Search term is required');
}
const users = await this.prisma.user.findMany({
where: {
OR: [
{ email: { contains: searchTerm, mode: 'insensitive' } },
{ name: { contains: searchTerm, mode: 'insensitive' } },
],
},
include: this.includeWithUserConnection(userId),
});

return users.map((u) => this.transformUserConnection(u));
}
}
61 changes: 2 additions & 59 deletions src/user/controller/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
Body,
Controller,
Get,
Param,
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { Body, Controller, Get, Post, Put } from '@nestjs/common';
import { CurrentUser } from '../../decorators/current-user.decorator';
import { UserService } from '../service/user.service';
import { User } from '@prisma/client';
Expand All @@ -21,13 +12,11 @@ import {
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';
import { userExtraProps, userProperties } from '../../schemas/user.properties';
import { userProperties } from '../../schemas/user.properties';

import { Public } from '../../decorators/public.decorator';
import { AuthGuard } from '../../auth/guard/auth/auth.guard';

@Controller('user')
@ApiBearerAuth()
Expand All @@ -51,25 +40,6 @@ export class UserController {
return this.userService.getSelf(user);
}

@Get('/:userId')
@ApiOperation({
summary: 'Get another user',
description: 'Get the currently logged in user',
})
@ApiOkResponse({
description: 'User found',
schema: {
type: 'object',
properties: { ...userExtraProps, ...userProperties },
},
})
async getUser(
@CurrentUser() user: User,
@Param('userId') userId: User['id'],
) {
return this.userService.getUser(user.id, userId);
}

@Put()
@ApiOperation({
summary: 'Update current user',
Expand Down Expand Up @@ -147,31 +117,4 @@ export class UserController {
) {
await this.userService.linkSocialAccount(userId, provider, accessToken);
}

@Get('search')
@ApiOperation({
summary: 'Search users',
description: 'Search for users',
})
@ApiOkResponse({
description: 'Users found',
schema: {
type: 'array',
items: {
type: 'object',
properties: { ...userExtraProps, ...userProperties },
},
},
})
@ApiQuery({
name: 'query',
type: 'string',
})
@UseGuards(AuthGuard)
async searchUsers(
@CurrentUser() user: User,
@Query() { query }: { query: string },
) {
return this.userService.searchUsers(user.id, query);
}
}
Loading

0 comments on commit 5b9b1f0

Please sign in to comment.