[nestjs] restapi + prisma 05

in kr •  2 years ago 

이전 글

PART4

NestJS REST API에서 관계형 데이터를 처리하는 방법을 배웁니다.

이 장에서는 데이터 계층과 API 계층에서 관계형 데이터를 처리하는 방법을 배웁니다.

먼저, 일대다 관계 기사 레코드(즉, 한 사용자가 여러 기사를 가질 수 있음)가 있는 데이터베이스 스키마에 사용자 모델을 추가합니다.

다음으로 사용자 레코드에 대한 CRUD(만들기, 읽기, 업데이트 및 삭제) 작업을 수행하기 위해 사용자 끝점에 대한 API 경로를 구현합니다.

마지막으로 API 계층에서 사용자-기사 관계를 모델링하는 방법을 배웁니다.

DB에 User 모델 추가

현재는 Article 모델만 있는데, 이에 User 모델을 추가합니다.

이후 npx prisma migrate dev --name "add-user-model" 명령어를 실행합니다.

// prisma/schema.prisma

model Article {
  id          Int      @id @default(autoincrement())
  title       String   @unique
  description String?
  body        String
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  author      User?    @relation(fields: [authorId], references: [id])
  authorId    Int?
}

model User {
  id        Int       @id @default(autoincrement())
  name      String?
  email     String    @unique
  password  String
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  articles  Article[]
}

seed 스크립트 수정

최초 더미 데이터 적재를 위하여 seed.ts 스크립트를 수정합니다.

이후 npx prisma db seed 명령어를 실행합니다.

async function main() {
  // create two dummy users
  const user1 = await prisma.user.upsert({
    where: { email: '[email protected]' },
    update: {},
    create: {
      email: '[email protected]',
      name: 'Sabin Adams',
      password: 'password-sabin',
    },
  });

  const user2 = await prisma.user.upsert({
    where: { email: '[email protected]' },
    update: {},
    create: {
      email: '[email protected]',
      name: 'Alex Ruheni',
      password: 'password-alex',
    },
  });

  // create three dummy articles
  const post1 = await prisma.article.upsert({
    where: { title: 'Prisma Adds Support for MongoDB' },
    update: {
      authorId: user1.id,
    },
    create: {
      title: 'Prisma Adds Support for MongoDB',
      body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
      description:
        "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
      published: false,
      authorId: user1.id,
    },
  });

  const post2 = await prisma.article.upsert({
    where: { title: "What's new in Prisma? (Q1/22)" },
    update: {
      authorId: user2.id,
    },
    create: {
      title: "What's new in Prisma? (Q1/22)",
      body: 'Our engineers have been working hard, issuing new releases with many improvements...',
      description:
        'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
      published: true,
      authorId: user2.id,
    },
  });

  const post3 = await prisma.article.upsert({
    where: { title: 'Prisma Client Just Became a Lot More Flexible' },
    update: {},
    create: {
      title: 'Prisma Client Just Became a Lot More Flexible',
      body: 'Prisma Client extensions provide a powerful new way to add functionality to Prisma in a type-safe manner...',
      description:
        'This article will explore various ways you can use Prisma Client extensions to add custom functionality to Prisma Client..',
      published: true,
    },
  });

  console.log({ user1, user2, post1, post2, post3 });
}

Article Entity 수정

// src/articles/entities/article.entity.ts

import { Article } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';

export class ArticleEntity implements Article {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  body: string;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  authorId: number | null;
}

user 를 위한 resource 생성하기

npx nest generate resource

  • users
  • REST API
  • Yes

위와 같이 선택하여 생성합니다. src/users 폴더가 생성되었을 것입니다.

http://localhost:3000/api 에 접속하면 swagger default 에 user 관련 API가 추가된 것을 확인할 수 있습니다.

PrismaClient 에 Users 모듈 추가하기

  • user module 에 prisma module 을 추가
  • user service 에 prisma service 를 추가
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  imports: [PrismaModule],
})
export class UsersModule {}

// src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  // CRUD operations
}

사용자 entity 설정

DTO 란 (data transfer object 데이터 전송 개체) 데이터가 네트워크를 통해 전송되는 방법을 정의하는 개체임

password 의 경우 swagger 에서 노출되지 않도록 설정합니다. (@ApiProperty() 를 사용하지 않으면 됨)

사용자를 생성하고 업데이트할 때 각각 API로 보낼 데이터를 정의하려면 CreateUserDto 및 UpdateUserDto 클래스를 구현해야 합니다.

다음과 같이 create-user.dto.ts 파일 내에서 CreateUserDto 클래스를 정의합니다.

UpdateUserDto의 정의는 CreateUserDto 정의에서 자동으로 유추되므로 명시적으로 정의할 필요가 없습니다.

// src/users/entities/user.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { User } from '@prisma/client';

export class UserEntity implements User {
  @ApiProperty()
  id: number;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty()
  name: string;

  @ApiProperty()
  email: string;

  password: string;
}

// src/users/dto/create-user.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  name: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  email: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(6)
  @ApiProperty()
  password: string;
}

User 서비스 정의

UsersService는 Prisma 클라이언트를 사용하여 데이터베이스에서 데이터를 수정 및 가져오고 이를 UsersController에 제공하는 일을 담당합니다.

이 클래스에서 create(), findAll(), findOne(), update() 및 remove() 메서드를 구현합니다.

// src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  create(createUserDto: CreateUserDto) {
    return this.prisma.user.create({ data: createUserDto });
  }

  findAll() {
    return this.prisma.user.findMany();
  }

  findOne(id: number) {
    return this.prisma.user.findUnique({ where: { id } });
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    return this.prisma.user.update({ where: { id }, data: updateUserDto });
  }

  remove(id: number) {
    return this.prisma.user.delete({ where: { id } });
  }
}

User 컨트롤러 정의

// src/users/users.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';

@Controller('users')
@ApiTags('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  @ApiCreatedResponse({ type: UserEntity })
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  @ApiOkResponse({ type: UserEntity, isArray: true })
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  @ApiOkResponse({ type: UserEntity })
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Patch(':id')
  @ApiCreatedResponse({ type: UserEntity })
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  @ApiOkResponse({ type: UserEntity })
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}

User 응답에서 password 제거

예상대로 동작하지만 응답에 암호가 포함되는 중대한 보안 결함이 있습니다.

이를 해결하기 위한 2가지 방법이 존재 합니다.

  • 컨트롤러 경로 처리기의 응답 본문에서 암호를 수동으로 제거
  • 인터셉터를 사용하여 응답 본문에서 암호를 자동으로 제거

첫 번째 옵션은 오류가 발생하기 쉽고 불필요한 코드 중복을 초래합니다. 따라서 두 번째 방법을 사용합니다.

Interceptor 를 사용하여 응답 필드의 값을 제거하여 반환

NestJS의 인터셉터를 사용하면 요청-응답 주기에 연결할 수 있고 경로 처리기가 실행되기 전과 후에 추가 논리를 실행할 수 있습니다.

이 경우 응답 본문에서 암호 필드를 제거하는 데 사용합니다.

NestJS에는 개체를 변환하는 데 사용할 수 있는 내장 ClassSerializerInterceptor가 있습니다. 이 인터셉터를 사용하여 응답 개체에서 암호 필드를 제거합니다.

먼저 main.ts를 업데이트하여 ClassSerializerInterceptor를 전역적으로 활성화합니다.

생성자는 개체를 사용하고 Object.assign() 메서드를 사용하여 부분 개체에서 UserEntity 인스턴스로 속성을 복사합니다.

부분 유형은 Partial<UserEntity> 입니다. 이는 부분 객체가 UserEntity 클래스에 정의된 속성의 하위 집합을 포함할 수 있음을 의미합니다.

다음으로 Prisma.User 개체 대신 UserEntity 를 반환하도록 UsersController 경로 처리기를 업데이트합니다.

// src/main.ts

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

  const config = new DocumentBuilder()
    .setTitle('Median')
    .setDescription('The Median API description')
    .setVersion('0.1')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

// src/users/entities/user.entity.ts

import { ApiProperty } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { Exclude } from 'class-transformer';

export class UserEntity implements User {
  @ApiProperty()
  id: number;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty()
  name: string;

  @ApiProperty()
  email: string;

  @Exclude()
  password: string;
}

Article 에 User 상세정보 포함

Article 개체와 함께 작성자를 반환하여 이를 개선할 수 있습니다.

데이터 액세스 논리는 ArticlesService 내부에서 구현됩니다.

Article 개체와 함께 작성자를 반환하도록 findOne() 메서드를 업데이트합니다.

// src/articles/articles.service.ts

  findOne(id: number) {
    return this.prisma.article.findUnique({
      where: { id },
      include: {
        author: true,
      },
    });
  }

하지만, 또 author 정보에 password 가 노출기 때문에 Article 정보를 update 하도록 한다
UserEntity 추가 및 생성자에서 UserEntity 초기화를 진행

// src/articles/entities/article.entity.ts

import { Article } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';
import { UserEntity } from 'src/users/entities/user.entity';

export class ArticleEntity implements Article {
  @ApiProperty()
  id: number;

  @ApiProperty()
  title: string;

  @ApiProperty({ required: false, nullable: true })
  description: string | null;

  @ApiProperty()
  body: string;

  @ApiProperty()
  published: boolean;

  @ApiProperty()
  createdAt: Date;

  @ApiProperty()
  updatedAt: Date;

  @ApiProperty({ required: false, nullable: true })
  authorId: number | null;

  @ApiProperty({ required: false, type: UserEntity })
  author?: UserEntity;

  constructor({ author, ...data }: Partial<ArticleEntity>) {
    Object.assign(this, data);

    if (author) {
      this.author = new UserEntity(author);
    }
  }
}

이제 ArticleEntity 개체의 인스턴스를 반환하도록 ArticlesController를 업데이트합니다.

return 될 결과를 new ArticleEntity 로 wrap 해주면 됨

// src/articles/articles.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ParseIntPipe,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ArticleEntity } from './entities/article.entity';

@Controller('articles')
@ApiTags('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Post()
  @ApiCreatedResponse({ type: ArticleEntity })
  async create(@Body() createArticleDto: CreateArticleDto) {
    return new ArticleEntity(
      await this.articlesService.create(createArticleDto),
    );
  }

  @Get()
  @ApiOkResponse({ type: ArticleEntity, isArray: true })
  async findAll() {
    const articles = await this.articlesService.findAll();
    return articles.map((article) => new ArticleEntity(article));
  }

  @Get('drafts')
  @ApiOkResponse({ type: ArticleEntity, isArray: true })
  async findDrafts() {
    const drafts = await this.articlesService.findDrafts();
    return drafts.map((draft) => new ArticleEntity(draft));
  }

  @Get(':id')
  @ApiOkResponse({ type: ArticleEntity })
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return new ArticleEntity(await this.articlesService.findOne(id));
  }

  @Patch(':id')
  @ApiCreatedResponse({ type: ArticleEntity })
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateArticleDto: UpdateArticleDto,
  ) {
    return new ArticleEntity(
      await this.articlesService.update(id, updateArticleDto),
    );
  }

  @Delete(':id')
  @ApiOkResponse({ type: ArticleEntity })
  async remove(@Param('id', ParseIntPipe) id: number) {
    return new ArticleEntity(await this.articlesService.remove(id));
  }
}

PART4 끝

이 장에서는 Prisma를 사용하여 NestJS 애플리케이션에서 관계형 데이터를 모델링하는 방법을 배웠습니다.

또한 ClassSerializerInterceptor와 엔터티 클래스를 사용하여 클라이언트에 반환되는 데이터를 제어하는 방법에 대해서도 배웠습니다.

참조링크

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

[광고] STEEM 개발자 커뮤니티에 참여 하시면, 다양한 혜택을 받을 수 있습니다.

image.png

Upvoted! Thank you for supporting witness @jswit.