From b4ce51b1b300413b3c68c03d0a2b7734f74e6693 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Mon, 6 Nov 2023 15:42:06 +0900 Subject: [PATCH 1/4] [backend] Use class serializer interceptor cf. Building a REST API with NestJS and Prisma: Handling Relational Data https://www.prisma.io/blog/nestjs-prisma-relational-data-7D056s1kOabc#add-a-user-model-to-the-database --- backend/src/main.ts | 5 ++-- backend/src/user/entities/user.entity.ts | 6 +++++ backend/src/user/user.controller.ts | 29 ++++++++++++++-------- backend/src/user/user.service.ts | 31 ++++++++---------------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/backend/src/main.ts b/backend/src/main.ts index 1dfaf08c..e8c064d7 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,7 +1,7 @@ -import { HttpAdapterHost, NestFactory } from '@nestjs/core'; +import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { ValidationPipe } from '@nestjs/common'; +import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'; import { PrismaClientExceptionFilter } from 'nestjs-prisma'; async function bootstrap() { @@ -10,6 +10,7 @@ async function bootstrap() { app.setGlobalPrefix('api'); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); // enable validation + app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // enable serialization const config = new DocumentBuilder() .setTitle('Pong API') diff --git a/backend/src/user/entities/user.entity.ts b/backend/src/user/entities/user.entity.ts index 2073b78a..424fce1d 100644 --- a/backend/src/user/entities/user.entity.ts +++ b/backend/src/user/entities/user.entity.ts @@ -1,7 +1,12 @@ import { User } from '@prisma/client'; import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; export class UserEntity implements User { + constructor(partial: Partial) { + Object.assign(this, partial); + } + @ApiProperty() id: number; @@ -11,5 +16,6 @@ export class UserEntity implements User { @ApiProperty({ required: false, nullable: true }) name: string | null; + @Exclude() password: string; } diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 6712444c..0257ee39 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -27,34 +27,43 @@ export class UserController { @Post() @ApiCreatedResponse({ type: UserEntity }) - create(@Body() createUserDto: CreateUserDto) { - return this.userService.create(createUserDto); + async create(@Body() createUserDto: CreateUserDto) { + return new UserEntity( + await this.userService.create(createUserDto) + ); } @Get() @ApiOkResponse({ type: [UserEntity] }) - findAll() { - return this.userService.findAll(); + async findAll() { + const users = await this.userService.findAll(); + return users.map(user => new UserEntity(user)); } @Get(':id') @ApiOkResponse({ type: UserEntity }) - findOne(@Param('id', ParseIntPipe) id: number) { - return this.userService.findOne(+id); + async findOne(@Param('id', ParseIntPipe) id: number) { + return new UserEntity( + await this.userService.findOne(id) + ); } @Patch(':id') @ApiOkResponse({ type: UserEntity }) - update( + async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ) { - return this.userService.update(+id, updateUserDto); + return new UserEntity( + await this.userService.update(id, updateUserDto) + ); } @Delete(':id') @ApiNoContentResponse() - remove(@Param('id', ParseIntPipe) id: number) { - return this.userService.remove(+id); + async remove(@Param('id', ParseIntPipe) id: number) { + return new UserEntity( + await this.userService.remove(id) + ); } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index dbc05d3c..91c4cbf7 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -5,53 +5,42 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { User, Prisma } from '@prisma/client'; import { hash } from 'bcrypt'; -type UserWithoutPassword = Omit; - -function excludePassword(user: User): Omit { - const { password, ...userWithoutPassword } = user; - return userWithoutPassword; -} - @Injectable() export class UserService { constructor(private prisma: PrismaService) {} - async create(createUserDto: CreateUserDto): Promise { + async create(createUserDto: CreateUserDto): Promise { const saltRounds = 10; const hashedPassword = await hash(createUserDto.password, saltRounds); const userData = { ...createUserDto, password: hashedPassword }; - return this.prisma.user.create({ data: userData }).then(excludePassword); + return this.prisma.user.create({ data: userData }); } - findAll(): Promise { + findAll(): Promise { return this.prisma.user - .findMany() - .then((users) => users.map(excludePassword)); + .findMany(); } - findOne(id: number): Promise { + findOne(id: number): Promise { return this.prisma.user - .findUniqueOrThrow({ where: { id: id } }) - .then(excludePassword); + .findUniqueOrThrow({ where: { id: id } }); } update( id: number, updateUserDto: UpdateUserDto, - ): Promise { + ): Promise { return this.prisma.user .update({ where: { id: id }, data: updateUserDto, - }) - .then(excludePassword); + }); } - remove(id: number): Promise { + remove(id: number): Promise { return this.prisma.user .delete({ where: { id: id }, - }) - .then(excludePassword); + }); } } From 215de6539656850e4d61489cc51806d9fc0a9e39 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Mon, 6 Nov 2023 15:43:07 +0900 Subject: [PATCH 2/4] [format] Format code with prettier --- backend/src/user/entities/user.entity.ts | 2 +- backend/src/user/user.controller.ts | 18 +++++----------- backend/src/user/user.service.ts | 27 +++++++++--------------- 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/backend/src/user/entities/user.entity.ts b/backend/src/user/entities/user.entity.ts index 424fce1d..ea479128 100644 --- a/backend/src/user/entities/user.entity.ts +++ b/backend/src/user/entities/user.entity.ts @@ -4,7 +4,7 @@ import { Exclude } from 'class-transformer'; export class UserEntity implements User { constructor(partial: Partial) { - Object.assign(this, partial); + Object.assign(this, partial); } @ApiProperty() diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 0257ee39..1b028047 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -28,24 +28,20 @@ export class UserController { @Post() @ApiCreatedResponse({ type: UserEntity }) async create(@Body() createUserDto: CreateUserDto) { - return new UserEntity( - await this.userService.create(createUserDto) - ); + return new UserEntity(await this.userService.create(createUserDto)); } @Get() @ApiOkResponse({ type: [UserEntity] }) async findAll() { const users = await this.userService.findAll(); - return users.map(user => new UserEntity(user)); + return users.map((user) => new UserEntity(user)); } @Get(':id') @ApiOkResponse({ type: UserEntity }) async findOne(@Param('id', ParseIntPipe) id: number) { - return new UserEntity( - await this.userService.findOne(id) - ); + return new UserEntity(await this.userService.findOne(id)); } @Patch(':id') @@ -54,16 +50,12 @@ export class UserController { @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ) { - return new UserEntity( - await this.userService.update(id, updateUserDto) - ); + return new UserEntity(await this.userService.update(id, updateUserDto)); } @Delete(':id') @ApiNoContentResponse() async remove(@Param('id', ParseIntPipe) id: number) { - return new UserEntity( - await this.userService.remove(id) - ); + return new UserEntity(await this.userService.remove(id)); } } diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index 91c4cbf7..e975dba3 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -17,30 +17,23 @@ export class UserService { } findAll(): Promise { - return this.prisma.user - .findMany(); + return this.prisma.user.findMany(); } findOne(id: number): Promise { - return this.prisma.user - .findUniqueOrThrow({ where: { id: id } }); + return this.prisma.user.findUniqueOrThrow({ where: { id: id } }); } - update( - id: number, - updateUserDto: UpdateUserDto, - ): Promise { - return this.prisma.user - .update({ - where: { id: id }, - data: updateUserDto, - }); + update(id: number, updateUserDto: UpdateUserDto): Promise { + return this.prisma.user.update({ + where: { id: id }, + data: updateUserDto, + }); } remove(id: number): Promise { - return this.prisma.user - .delete({ - where: { id: id }, - }); + return this.prisma.user.delete({ + where: { id: id }, + }); } } From 885d81959313afd94c8028b7779d4b50439441c5 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Mon, 6 Nov 2023 22:10:19 +0900 Subject: [PATCH 3/4] [backend] Return 204 on DELETE /users/:id --- backend/src/user/user.controller.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 1b028047..89af76d9 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -6,6 +6,7 @@ import { Patch, Param, Delete, + HttpCode, ParseIntPipe, } from '@nestjs/common'; import { UserService } from './user.service'; @@ -27,20 +28,21 @@ export class UserController { @Post() @ApiCreatedResponse({ type: UserEntity }) - async create(@Body() createUserDto: CreateUserDto) { - return new UserEntity(await this.userService.create(createUserDto)); + async create(@Body() createUserDto: CreateUserDto): Promise { + const user = await this.userService.create(createUserDto); + return new UserEntity(user); } @Get() @ApiOkResponse({ type: [UserEntity] }) - async findAll() { + async findAll(): Promise { const users = await this.userService.findAll(); return users.map((user) => new UserEntity(user)); } @Get(':id') @ApiOkResponse({ type: UserEntity }) - async findOne(@Param('id', ParseIntPipe) id: number) { + async findOne(@Param('id', ParseIntPipe) id: number): Promise { return new UserEntity(await this.userService.findOne(id)); } @@ -49,13 +51,14 @@ export class UserController { async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, - ) { + ): Promise { return new UserEntity(await this.userService.update(id, updateUserDto)); } @Delete(':id') + @HttpCode(204) @ApiNoContentResponse() - async remove(@Param('id', ParseIntPipe) id: number) { - return new UserEntity(await this.userService.remove(id)); + async remove(@Param('id', ParseIntPipe) id: number): Promise { + await this.userService.remove(id); } } From f0f585615199b589d2422776a930b320e63005a8 Mon Sep 17 00:00:00 2001 From: Shun Usami Date: Mon, 6 Nov 2023 22:29:49 +0900 Subject: [PATCH 4/4] [backend] Add hashPassword method to UserService --- backend/src/user/user.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index e975dba3..2a739dea 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -9,9 +9,13 @@ import { hash } from 'bcrypt'; export class UserService { constructor(private prisma: PrismaService) {} - async create(createUserDto: CreateUserDto): Promise { + hashPassword(password: string): Promise { const saltRounds = 10; - const hashedPassword = await hash(createUserDto.password, saltRounds); + return hash(password, saltRounds); + } + + async create(createUserDto: CreateUserDto): Promise { + const hashedPassword = await this.hashPassword(createUserDto.password); const userData = { ...createUserDto, password: hashedPassword }; return this.prisma.user.create({ data: userData }); }