Archive

[Nomard Coder] 우버이츠 클론코딩

안다희 2021. 2. 8. 00:09
728x90

Uber Eats

전체코드

NestJS 문서

다 배운 후

  • 질문: 모두 해결하기
  • 질문: typeorm vs prisma 차이점
  • 숙제!

핵심

    • 모든 orm은 sql을 이용해서 db에 직접 접근할 수 있게 해준다. (# 10.18)

실행

  • dev: npm run start:dev

0.4 Requirements

  • "NestJS로 API 만들기" 듣기
  • "GraphQL로 영화 API 만들기", "영화 웹 앱 만들기" 듣기

0.5 How to Get Help

0.6 Backend Setup

nest g application
// 이름 입력
npm i
npm run start:dev

0.7 This Course Structure

  • db 작업 하기 전에, (user, auth) graphql이 nestjs에서 어떻게 작동하는지부터 볼거야.
  • 같은 컨셉으로 작은 결과물부터 만든다. 미니 프로젝트~ 그 후에 데이터베이스.
  • nestjs, graphql, db 활용을 잘하게 되면 클론코딩은 아주 빠르게 할 수 있어.

1 GraphQL API

1.0 Apllo Server Setup

npm i @nestjs/graphql graphql-tools graphql apollo-server-express

1.1 Our First Resolver

  • https://www.apollographql.com/docs/apollo-server/api/apollo-server/
    • 공식문서를 보면, GraphQLModule은 Apollo Server를 기반으로 동작한다고 나와있음.
    • NestJS는 안하고, 그저 Apollo Server와 대화하는 것.
  • https://docs.nestjs.com/graphql/quick-start#code-first
    • 코드우선 방식을 택할 것이다.
    • GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/schema.gql'), }),
    • 이제 이 에러가 날거다.
    • Query root type must be provided.
    • 우리의 GraphQL 모듈이 query, resolver를 찾고 있다는 것이다.
  • GraphQL이 Code First 로써 어떻게 동작하는지 보여줄 것이다.
  • nest g mo restaurants // 나중에 삭제할거임
  • restaurant.resolver.ts와 같이 작성해주면,,, schema.gql이 자동으로 생긴다.. 대박....
  • /* restaurant.resolver.ts */ import { Resolver, Query } from '@nestjs/graphql'; // import { Query } from '@nestjs/common'; // 이거 아님!!!!!!!!!!!!1 @Resolver() export class RestaurantResolver { // Query는 1번째 argument로 'function'이 필요하다. () => Boolean = reutnrs => Boolean @Query((returns) => String) isPizzaGood(): String { return ''; } }
  • http://localhost:3000/graphql 들어가보면 query가 생겨있지~ 대박,,,
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/733e9d2388ab3d15b37f2850ffb7154211c7a2af

1.2 ObjectType (05:45)

  • What is Entity?
    • db에 있는 모델을 생각하면 됨. 작명이 비슷함.
  • Object type 만들어주기
    • src/restaurants/entities/restaurant.entity.ts
    • import { ObjectType, Field } from '@nestjs/graphql'; @ObjectType() export class Restaurant { @Field((type) => String) name: string; @Field((type) => Boolean, { nullable: true }) // for graphql isGood?: boolean; // for typescript }
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/e86ea2c96e83217859b611ae1b0e397c28749a62

1.3 Arguments (03:56)

1.4 InputTypes and ArgumentTypes (08:54)

1.5 Validating ArgsTypes (04:28)

npm i class-validator class-transformer

2 DATABASE CONFIGURATION

2.0 TypeORM and PostgreSQL (05:11)

  • 타입스크립트나 NestJS에서 데이터베이스와 통신하기 위해서는 TYPE ORM을 사용할 필요가 있다.
    • type 사용 가능, NestJS와 연결가능
    • Object Relational Mapping (객체 관계 매핑)
    • ORM 쓰면 SQL 안쓰고도 db와 통신 가능
    • supported platform: react-native도! 와우!
    • 우리는 nodejs로 쓸거다.
    • db는 postgres
  • Download postgres
  • Download postico (GUI) - db 시각화

2.1 MacOS Setup PostgreSQL (04:58)

  • postgre
    • 5000 쓰고 connect
    • postgre 컴 켜면 자동으로 돌아가긴 한다.
  • postico
    • 포트번호 5000 => initalize
      • localhost > username 으로 들어가는데, localhost 클릭하면 postgre 내용물과 똑같은 데이터베이스 보인다.
        • database, nuber-eats database 생성하자!
      • 일단 데이터베이스에 유저를 만들어줘야 함. 그래야 연결 가능.
  • postgre
    • nuber-eats db 보임 - 더블클릭 - 터미널 열림 - 이제 유저명 변경해줄거야. 비밀번호도.
    • 원래 기본 유저명: 컴 유저명 (daheeahn)
      ```
      \du;
      // 결과Role name | Attributes | Member of
    • List of roles
    • ----------+------------------------------------------------------------+-----------
      daheeahn | Superuser, Create role, Create DB | {}
      postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
    • ALTER USER daheeahn WITH PASSWORD '12345';
      // daheeahn 비밀번호 12345로 바뀜.
      // 중요: 나중에 데이터베이스 연결할 때 필요하다. 꼭 기억하자.
  • postgres 앱을 서버로 구동해야해.
  • db와 상호작용할 땐 postico를 쓸거다 (SQL 쓸 줄 알면 안써도 되는데 편하다.)

2.3 TypeORM Setup (06:57)

  • 데이터베이스 세팅 끝, NestJS와 연결할 것이다.
  • NestJS는 아주 세련된 TypORM integration을 가지고 있다.
  • NestJS를 쓸 때 또 다른 ORM 패키지인 Sequelize도 쓸 수 있따. 꼭 typeORM 쓸 필요 X
  • 우리는 typeorm 쓸 것이다.
  • typeorm은 타입스크립트에 기반되어 있다. sequelize는 js 기반. NodeJS에서만 돌아감.
  • typeorm은 멀티플랫폼 가능!
  • https://docs.nestjs.com/techniques/database
  • npm install --save @nestjs/typeorm typeorm pg // mysql은 안쓰고 pg 쓸 것이다. (postgre)
  • https://typeorm.io/#/undefined/quick-start
    • 이제 TypeORM 모듈을 앱모듈에 설치할 것이다
      • 2) ormconfig.json 이라는 파일에 쓰거나
      • 우리는 1번!
      • port는 Postgres 앱에서 Server Settings 확인
    • - 1) 코드에 직접 쓰거나
npm run start:dev

2.4 Introducing ConfigService (06:13)

  • .env 파일 활용
    • dotenv 라는 패키지 이용할 수도 있는데, NestJS만의 방식도 있다.
    • // https://docs.nestjs.com/techniques/configuration npm i --save @nestjs/config
    • dotenv의 최상위에서 실행된다. NestJS의 방식으로 돌아가는 것 뿐.
  • dev, test, prod에 따라 환경 변수 다르게 하기
    • 가상 변수를 설정할 수 있게 해준다.
  • npm i cross-env
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d5ed915f445d69997032323eb134b5745bbcdbec

2.5 Configuring ConfigService (06:12)

2.6 Validating ConfigService (05:44)

3 TYPEORM AND NEST

  • TypeORM이 어떻게 돌아가는가

3.0 Our First Entity (07:44)

  • https://typeorm.io/#/entities
  • Entity: 데이터베이스에 저장되는 데이터의 형태를 보여주는 모델. 클래스.
  • Object Type: 자동으로 스키마를 빌드하기 위해 사용하는 GraphQL decorator.
    • 이 2개를 한꺼번에 사용할 수 있다. 최종코드에서 확인.
  • 이제 postico 연다.
  • typeorm에게 우리가 만든 entity가 어디있는지 알려줘야 한다.
    1. entities: [Restaurant],
    2. ??
    • 이제 저장하면 콘솔에 SQL문이 쏟아질 것이다.여서 그렇다.
    • synchronize: true // 1) // synchronize: process.env.NODE_ENV !== 'prod', // 2) prod은 따로 하고 싶을 수 있으니까 // 2와 같이 바꿔주자.
    • postico에서 새로고침도 하면 restaurant라는 테이블이 생겼다! (synchronize true여서 자동으로~)
    • => graphql에서 사용하는 스키마도 자동생성, DB에도 자동으로 반영
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/b028bdcd2b38f55ade758f56dab8c0c9ffa0ba90
  • https://medium.com/@hckcksrl/typescript%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4-typeorm-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-cb7140b69c6c
    • ORM
      • ORM (Object-relational mapping)은 객체지향 프로그래밍(Object-Oriented-Programming) 과 관계형 데이터베이스 (Relational-Database)사이의 호환되지 않는 데이터를 변환하는 시스템이다. 객체지향 프로그래밍은 Class를 사용하고 관계형 데이터베이스는 Table을 사용한다.
    • TypeOrm
      • TypeOrm 이란 TypeScript 와 JavaScript(ES5 , ES6 , ES7) 용 ORM이다. MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL 데이터베이스를 지원한다. 여기서는 PostgreSQL을 사용할 것이다.

3.1 Data Mapper vs Active Record (07:04)

  • 자, 어떻게 typescript를 이용해서 DB에 있는 Restaurant에 접근할 수 있을까?
    • => Typeormmodule - Repository 사용할 것이다.
  • What is Repository? (Data Mapper)
    1. Active Record
      • extends BaseEntity
    2. Data Mapper
      • 우리가 한 방식과 거의 같다. (restaurant.entity.ts)
      • Repository를 사용한다. Entity랑 상호작용 담당.
      • ** repository가 추가적으로 필요한 것이다. (코드 한줄? maybe?)
  • Why Data Mapper?
    • NestJS 애플리케이션에서 Data Mapper 사용하는 이유는 NestJS + TypeORM 개발 환경에서 Repository를 사용하는 모듈을 쓸 수 있기 때문. 이 Repository 사용하면 어디서든지 접근 가능. 실제 서비스에서 접근 가능, 테스팅 할 때도 접근 가능.
    • NestJS에서는 자동으로 Repository를 사용할 수 있도록 클래스에서 알아서 준비해준다.

3.2 Injecting The Repository (07:44)

  1. repository를 import 해보자.
    • restaurant.module에서. (restaurant.module에서 restaurant repository가 필요해서)
    • // restaurant.module.ts imports: [TypeOrmModule.forFeature([Restaurant])],
  2. RestaurantService에서 repository를 사용하기 위해 service를 RestaurantResolver에 import 했다.
  3. // restaurant.resolver.ts constructor(private readonly restaurantService: RestaurantService) {}
  4. 이제 repository를 service.ts에 inject해보자.
    • 그래서 restaurant.module에 import 해준거.

3.3 Recap (04:15)

  • 지금까지 정리
    1. TypeOrmModule에 Restaurant라고 하는 entity를 가지고 있다. => Restaurant가 db가 된다.
    2. module에서는 Restaurant를 import했다. forFeature: TypeOrmModule이 특정 feature를 import할 수 있게 해준다.
      • 이 경우, feature is Restaurant entity.
    3. resolver에 RestaurantService를 추가했다.
      • 이건 restaurants.module.ts에서 provider에 있어야한다.
      • why? RestaurantResolver class에서 restaurantService를 inject할 수 있어야 하기 때문이다. (in contructor)
    4. restaurant.service.ts에서 restaurants는 뭘까? restaurant entity의 repository이다.
    5. 이게 바로 NestJS TypeORM을 사용하는 이유.
      • => repository의 모든 옵션 사용 가능
  • later: queryRunner - repository를 사용해서 entity를 만드는 방법을 보여줄 것이다.

3.4 Create Restaurant (08:59)

3.5 Mapped Types (12:23)

3.6 Optional Types and Columns (08:42)

3.7 Update Restaurant part One (08:05)

  • @InputType(): @Args('input')
  • @ArgsType(): @Args()
  • 최종코드: 3.8과 함께

3.8 Update Restaurant part Two (07:56)

4 USER CRUD

4.0 User Module Introduction (02:06)

  • 이제 backend 클론을 시작해보자!
  • User Module
  • 최종코드:

4.1 User Model (07:07)

4.2 User Resolver and Service (05:20)

4.3 Create Account Mutation part One (06:31)

4.4 Create Account Mutation part Two (08:07)

4.5 Create Account Mutation part Three (07:00)

4.6 An Alternative Error (05:05)

4.7 Hashing Passwords (09:02)

  • hash: 단방향 함수. 비밀번호 to 해시. NO 해시 to 비밀번호.
  • listener and subscriber - BeforeInsert
  • bcrypt 이용할 것이다. (hash password)
    • this.users.save 전에 create했을 때 이미 우리는 instance를 가지고 있다. (이 때 생성된다.)
    • create는 단지 entity를 만들 뿐이야. 이 entity를 save하기 전에 hashPassword가 실행되는거고. BeforeInsert
    • this.password에 접근할 수 있다. 그래서 해시!
  • npm i bcrypt @types/bcrypt
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/2cabd62cc9de2f1acebcf5509fec905d72bf38f7

4.8 Log In part One (07:39)

4.9 Log In part Two (10:20)

5 USER AUTHENTICATION

5.0 Introduction to Authentication (02:00)

  1. 우리가 직접 적용
    • 먼저 수작업으로 해보자~!
    • app.module.ts 보면 마법같은 모듈 많지만, jwt는 우리가 직접 만들어보자. root를 위한 모듈을 직접 만들어보자구!
  2. nestjs/passports를 적용시킨 후 passport-jwt와 nestjs/jwt를 활용

5.1 Generating JWT (08:18)

  • https://www.npmjs.com/package/jsonwebtoken
    • sign token만 하면 됨.
    • jwt.sign(우리가 원하는 데이터, privateKey, algorithm)
    • privateKey come from .env => app.module.ts 수정 (SECRET_KEY: Joi~) token을 지정하기 위해 사용하는 privateKey
    • 사용자도 jwt 해독 가능 => 개인정보 넣지말고, id 정도만 알 수 있게.
  • npm i jsonwebtoken npm i @types/jsonwebtoken --only-dev // js만 있어서 type 따로 설치
  • secret key
    • https://randomkeygen.com/
    • CodeIgniter Encryption Keys - Can be used for any other 256-bit key requirement.
      • 이곳에서 하나 골라서 쓰기
  • nestjs: app.module에서 모듈 설정해주고, 각 모듈 ex UserModule에서 ConfigService import 가능 => UserService에서 ConfigService 사용 가능! 연결연결연결~
    • this.config.get('SECRET_KEY') = process.env.SECRET_KEY
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/a6d6ef3222242d03f8b2b38ca1382288cf6ac71a

5.2 JWT and Modules (06:05)

  • 정리: dependency injection: 원하는 것의 class만 적으면 nestjs가 그 정보를 가져다줌.
    • => app.module에서 ConfigModule 설정해줬다. -> ConfigService를 자동으로 가지게 해준다.
    • => users.module에서 ConfigService를 import하는 것만으로 ConfigService를 쓸 수 있다.
    • 우리만의 forRoot를 적용해봄으로써 어떻게 자동으로 적용되는건지 보자.
  • jwt 토큰의 목적
    • 내부에 담겨진 정보 자체가 아닌, 정보의 진위 여부가 더 중요. 이게 우리가 만든건지 아닌지를 알 수 있음.
    • 아무도 수정하지 않았는지 확인 가능.
    • 유저 정보가 수정되었을 때 이전 jwt 토큰은 더이상 유효하지 않기 때문에 우리만이 유효한 인증을 할 수 있다.
  • 이제 우리만의 token 모듈을 만들어보자.
    • this.config처럼 이런 식으로 jwt 모듈을 사용하고 싶어서.
      jwt.sign({ id: user.id }, this.config.get('SECRET_KEY'));
      이렇게 말고, this.jwt.sign 이렇게 사용하고 싶어서.
    • nest g mo jwt
    • module의 종류는 2가지. (설정이 있느냐 없느냐.)
      1. static module
        • UsersModule
      2. dynamic module
        • GraphQL.forRoot
    • dynamic module 이겠지~
      • 동적모듈은 사실 결과적으로 정적모듈이 된다.결국 이렇게 만들게 된다.
        결국 동적모듈은 중간과정인 것.
      • GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/schema.gql'), }), // 정적 // to GraphQLModule // 동적 // 근데 어떻게 이렇게 되는거징.
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/1de7ee0c12ae14ea6ec82d449983031b9a37c548

5.3 JWT Module part One (06:10)

  • 아직 JwtModule이 정적이지. (5.2 최종코드)
  • 이제 DI부터 작동하게 해보자.
    • 의존성 주입: Dependency Injection = DI
  • forRoot를 보면 static 메소드이고, dynamic module을 반환한다는걸 알 수 있다. (마우스 갖다대면 알 수 있음)
    • 이걸 JwtModule에도 똑같이 적용시켜보자.
      ```
      nest g s jwt
      // spec.ts 테스트파일은 잠시 삭제.
  • global module로 설정해주면 굳이 import 안해줘도 된다. like ConfigModule
    • isGlobal()
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/7992f75c85622227888ce41c27fbd6d7ad76959e

5.4 JWT Module part Two (08:23)

5.5 JWT Module part Three (05:43)

5.6 Middlewares in NestJS (11:18)

  • 이제 토큰을 전달해보자! 미들웨어를 사용해야한다.
  • 요청을 받고, 요청 처리 후에 next() 함수를 호출한다.
  • middleware에서 토큰 받고, 그 토큰 가진 사용자 찾아줄 것.
  • nestjs는 express와 미들웨어 처리방법이 똑같다.
  • https://docs.nestjs.com/middleware
    • Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects, and the next() middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next.
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/b3c90ad4d3f1e9b4012034d9cb440ad958374941

5.7 JWT Middleware (12:34)

  • middleware를 어디서든지 쓰고싶다면 functional middleware여야하고, class middleware 쓰려면 app.module에서 해야한다.
    • JwtMiddleware, jwtMiddleware 차이 보면 됨.
  • hasOwnProperty 이런식으로 쓰는구나~ 나는 그냥 decoded?.id 로 사용했었는데.
    ```
    if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {}
  • console.log(decoded['id']);
  • dependency injection 아직 잘 모르겠다.
  • 질문: UsersService는 export만 해주면 JwtService에서 쓸 수 있는데, 왜 그런거야? UsersService를 JwtService에 넣어준 적이 없는데?
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d02a05eab62cc5cc7ca294dc1f74d0d7482f5f4f

5.8 GraphQL Context (05:43)

  • 이제 req['user'] = user; 이 request를 graphql로 공유하도록 할 것이다.
    • http request to graphql resolver
    • 그러려면 gql module이 apllo server에서 모든걸 가져와 사용할 수 있다는 걸 기억해둬야 함.
  • Context
    • https://github.com/apollographql/apollo-server#context
    • request context는 각 request에서 사용이 가능하다.
    • A request context is available for each request. When context is defined as a function, it will be called on each request and will receive an object containing a req property, which represents the request itself.
    • => 5.7에서 request property에 넣어줬고, 이제 user는 모든 resolver에서 공유 가능하단 말.
    • By returning an object from the context function, it will be available as the third positional parameter of the resolvers:
    • GraphQLModule.forRoot({ context: ({ req }) => ({ user: req['user'] }), }),
  • 이제 resolver에서 다 받을 수 있지만, me에서처럼 항상 context.user 확인할 순 없지. 그래서 guard를 배워볼 것이다!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/12e27825743f2bb1ed3f2cc23ef8d3c3add4fb72

5.9 AuthGuard (08:22)

5.10 AuthUser Decorator (05:30)

  • login되어있지 않다면 request를 멈추게 했지? 이제 request가 true이면 user를 받아야하니까 받아보자!
    • @Context도 가능하지만, AuthUser가 더 직관적이고 편하다! 직접 만든 Decorator. (최종코드에서 자세히)
  • @Query((returns) => User) @UseGuards(AuthGuard) // 이렇게 추가해주는 것보다 더 좋은 방법 있다. (추후에 다시~!) // me(@Context() context) { // 이렇게 해줘도 되는데, 바로 user 받도록 decorator를 만들어보자. me(@AuthUser() authUser) {}
  • 지금까지 authentication 했다.
    • module, dynamic module, providers, dependency injection, middleware, guard, decorators, context FOR AUTHENTICATION
    • Autorization은 더 멋있을거야! Role에 따라서 특정 resolver만 보여줄 수 있음. restaurant 할 때 할거~ Metadata 쓸거임.
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/e7e94c2b6f1638eb4afbd2725c1ad9bf8c64dc27

5.11 Recap (07:35)

  • How Authentication works.
    1. header에 토큰을 보낸다.
    2. header는 http 기술이 쓰인다.
      • http 기술 쓰기 위해 middleware를 만들었다.
      • 이 미들웨어는 header를 가져다가 jwtService.verify 한다.
      • id를 찾으면, 해당 id 가진 user를 찾는다.
      • user 찾으면 user를 request object에 붙인다.
      • 모든 resolver에서 이 request object 사용 가능.
      • 만약 token이 없거나 에러가 있다면, token에서 user 찾을 수 없으면 => request에 뭣도 붙이지 않는다.
    3. apllo server나 graphql의 content는 모든 resolver에 정보를 보낼 수 있는 property다.
      • context가 매 request마다 호출될 것이다.
      • JwtMiddleware 거치고, graphql context에 request user 보내는거야.
      • ** 이제 request user라 하지 않고, context user라 하겠음
    4. Guard
      • function의 기능을 보충해준다.
      • false return하면 request를 중지시킨다.
      • context를 gql context로 바꿔주는 작업도 필요하다.
    5. request가 middleware, apollo server, guard 거쳐서 resolver에 온다.
      • decorator로 온다 이제. AuthUser => graphql context로!
      • decorator는 value를 return 해야만 한다.
    6. 다시 정리.
      • header에 token 보낸다.
      • token을 decrypt, verify하는 middleware
      • request object에 user 추가
      • 이게 gql context에 들어간다.
      • guard가 user 찾는다.
      • guard에 의해 request가 authorize되면 resolver에 decorator로 드디어 user를 가져온다. authUser!
  • 최종코드(에러수정): https://github.com/daheeahn/nuber-eats-backend/commit/d13644b43a961ff0ae75d5bb9358c46595353fb5

5.12 userProfile Mutation (09:56)

5.13 updateProfile part One (09:37)

  • resolver 작성중~
  • this.users.update는 UpdateResult를 반환한다. => 이걸 사용하진 않기 때문에 그냥 await만 해준다.
  • await this.users.update~

5.14 updateProfile part Two (06:33)

  • { ...editProfileInput }
    • 이렇게 해야 업데이트할 column만 수정된다.
  • user entity 생기기 전에도 @BeforeInsert해서 패스워드 해시화. 업데이트할 때도 필요하겠지?
    • @BeforeUpdate
      • 근데 왜 안먹힘? => 다음 강의

5.15 updateProfile part Three (05:49)

5.16 Recap (04:50)

  1. email: "email to update", password: undefined 이면 password는 nullable이 아니기 때문에. 에러가 난다.
    • => this.users.update(userId, { ...editProfileInput }) 으로 해줘야 필요한 column만 업데이트된다.
    • for excuting hook!
  2. udpate는 query만 날릴 뿐, entity를 직접 업데이트하는 것이 아니다. => BeforeUpdate가 안먹힌다.
    • => .save
    • but, restaurant의 경우에서 .update로 할거야. 직접 user.~ user.~ 해줄 순 없으니까~
  • 최종코드:

6 EMAIL VERIFICATION

6.0 Verification Entity (06:54)

  • 이메일 모듈을 만들어보자! jwt 모듈과 같은 동적 모듈을 직접 만들어보자.
  • 1:1 관계
    • A가 오로지 하나의 B만 포함한다. B도 오로지 하나의 A만 포함한다.
    • User : Verification = 1:1
  • User로부터 Verification에 접근하고 싶다.
    • JoinColumn이 User쪽에 있어야 한다.
  • 우리의 경우는 반대다.
    • Verfication으로부터 User에 접근하고 싶다.
    • => JoinColumn이 Verfication에 있어야 한다.
  • 원하는 쪽에서 JoinColumn을 써야한다.
  • 테이블 간의 관계를 하나 배웠다!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/685ebff6e950e0e70a5b1eab694b198acd553068

6.1 Creating Verifications (08:03)

6.2 Verifying User part One (11:40)

6.3 Verifying User part Two (11:56)

  • findOne
    • 명시된 필드만 가져온다. 그래서 ['id, 'password'] 모두 명시해줘야한다.
  • 기본적으로 쿼리에 필드를 포함시키고 싶지 않을 때.
    ``````
    • 예) 패스워드는 쿼리에서 항상 불러오면 X 안그러면 .save할 때 패스워드도 딸려오기 때문에 해시된 패스워드가 또! 해시됨.
  • @Column({ select: false })
  • CASCADE
    • @OneToOne((type) => User, { onDelete: 'CASCADE' })
    • Verfication은 User가 삭제될 때 같이 삭제된다.
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/7d61f387e03ff7489fa96aed995d1708872c765e

6.4 Cleaning the Code (05:13)

  • resolver는 doorman 역할만! service에서 모든 로직 처리하기.

6.5 Mailgun Setup (07:12)

  • mailgun: 메일을 보내는 최고의 서비스! sendgrid는 조금 불편.
  • 계정 가입 후 카드 등록하면 5000개의 메일 보낼 수 있음.
  1. 가입하기
    • 가입 이미되어있음.
  2. API KEY
  3. Authorized Recipients
    • 대시보드 보면 오른쪽에 Authorized Recipients가 보일텐데, 신용카드 안등록하면 여기에 등록된 사람만 이메일을 받을 수 있고, 등록하면 아무나 보낼 수 있음.

6.6 Mail Module Setup (08:34)

  • 이제 이메일 모듈을 만들어보자!
    • JwtModule과 매우매우 비슷.
    • 직접 만들지 않아도됨. @nest-modules/mailer라는 모듈 이용해도 됨. 좀 더 복잡한 메일 보낼 수 있다!
      MailModule.forRoot({
      apiKey: process.env.MAILGUN_API_KEY,
      emailDomain: process.env.MAILGUN_DOMAIL_NAME,
      fromEmail: process.env.MAILGUN_FROM_EMAIL,
      }),
    • nest g mo mail
    • emailDomain
    • fromEmail: 발신자.
  • API KEY
  • 최종코드: 6.9에 같이

6.7 Mailgun API (13:25)

6.8 Beautiful Emails (07:59)

  • 예쁘게 html로 이메일 보내기
  • 패스

6.9 Refactor (12:35)

테스트는 나중에~!

10 RESTAURANT CRUD

10.0 Restaurant Models (11:00)

  • OneToMany, ManyToOne

10.1 Relationships and InputTypes (08:19)

10.2 createRestaurant part One (07:36)

10.3 createRestaurant part Two (14:12)

10.4 Roles part One (10:19)

  • role based authorization
    • 역할에 따라 리졸버를 실행할 수 있는지 없는지 판단
  • authentication: who are you?
  • authorization: 이 resolver에 접근할 수 있니?
  • metadata!
    • metadata: resolver의 extra data임.enum을 이런식으로 활용할 수 있구나!
    • type AllowedRoles = keyof typeof UserRole | 'Any'; export const Role = (roles: AllowedRoles[]) => SetMetadata('roles', roles);
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/35951aea6b8b5d33337eb9a367e7d519aa292cdf

10.5 Roles part Two (13:18)

10.6 Roles Recap (12:00)

10.7 Edit Restaurant part One (09:08)

10.8 Edit Restaurant part Two (12:43)

  • 이젠 다 셋업되어있어서 정말 편하쥬~
  • findOneOrFail: 못찾으면 에러를 뿜는다.
  • 기본적으로 restaurant.owner는 가져오지 않는다. 그래서 가져올 때 항상 옵션을 줘야한다.
    • loadRelationIds or relations: ['owner'] etc...
    • but, 이렇게 안하고 @RelationId를 이용할 수도 있다.
  • @RelationId
  • defensive programming
    • 에러를 핸들링한 후 코드를 다룬다.
    • 와우 내가 직접 터득한 방식이네?

10.9 Edit Restaurant part Three (10:59)

  • Custom Repository
    • https://typeorm.io/#/custom-repository
    • 10.8에서 restaurant.service에서 중복으로 쓰이는 메소드를 만들어줬는데, categories repository에 들어가도 될만한 메소드를 만들어서! 뭔 느낌인지 알지~
    • this.getOrCreateCategory to this.categories.getOrCreate
    • @EntityRepository(Category)
    • 방법은 3가지 (문서 참고)
    • 첫번째 방법인 extends.
      • https://typeorm.io/#/custom-repository/custom-repository-extends-standard-repository
      • why? repository는 모든 method를 접근가능하게 해줌. abstract repository는 그렇지 않다.
      • public method를 원하는지 아닌지에 달려있다. 차이는 그게 끝.
        • repository를 entity에서 가져오지 말고, 직접 만든 repository로 import 해줘야 한다.
          • restaurant.module.ts 참고
      • ...(category && { category })

10.10 Edit Restaurant Testing (05:55)

10.11 Delete Restaurant (07:10)

10.12 Categories part One (10:41)

  • computed field, dynamic field
    • db와 Entity에 저장되지 않는, 존재하지 않는 field
    • request가 있을 때마다 계산해서(computed) 보여주는 field
    • 보통 로그인된 사용자의 상태에 따라 계산되는 field.
    • ex) isLiked. 이건 db에 저장되어 있지 않지만 프론트에서 필요하지.
    • 지금은! category에 해당하는 restaurant가 몇 개인지 보여주는 field를 만들 것이다.
      • 여기서 restaurantCount는 db에 없는데 마치 category table에 있는 column처럼 조회할 수 있단 말이쥐
    • { allCategories { ok categories { slug name restaurantCount # 이건 db에 없다구! } error } }
    • 그것이 바로 ResolveField
      • 매 request마다 계산된 field를 만들어준다.
      • 근데 왜 이걸 Resolver에 써주는거지?
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/530266cb5bdc9f62b55604b64cd330e34af6c2f9

10.13 Categories part Two (05:01)

10.14 Category (08:21)

10.15 Pagination (10:01)

  • query
  • findCategoryBySlug(~) { ok error totalPages # this~ 이걸 해볼거다! category { # { relations: ['restaurants'] }, 로 조회할 수 있지만 300개면 300개 다 가져와야함. # 그래서 pagination으로 restaurants를 부분적으로 가져올거야. restaurants { id name } } }
  • result
    • findCategoryBySlug 안에 또 category가 있어서 이상해서 고쳐보자. (다음강의)
  • { "data": { "findCategoryBySlug": { "ok": true, "totalPages": 4, "category": { "name": "korean bbq", "slug": "korean-bbq", "restaurants": [ { "id": 7, "name": "BBQ House 4" }, { "id": 8, "name": "BBQ House 5" }, { "id": 9, "name": "BBQ House 6" } ] } } } }
  • pagination
  • const restaurants = await this.restaurants.find({ where: { category, }, take: 3, skip: (page - 1) * 3, }); // find has many many options
  • pagination 다루는 nestjs Package도 존재한다!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/897cbc2ba7e51817aef2b774c38e6de6ce29fe83

10.16 Restaurants (08:50)

10.17 Restaurant and Search (12:08)

10.18 Search part Two (08:15)

  • pagination을 위한 repository를 만들어보면 어떨까?
    • 숙제!
  • ILike: Insensitive: 대문자 소문자를 신경쓰지 않는다.
  • 모든 orm은 sql을 이용해서 db에 직접 접근할 수 있게 해준다.
  • 그러나 강의시간 기준으로 ILike가 나오지 않아서, sql을 이용해서 name을 직접 검색해볼 것이다.
  • const [restaurants, totalResults] = await this.restaurants.findAndCount({ where: { // name: ILike(`%${query}%`), // typeorm name: Raw((name) => `${name} ILIKE '%${query}%'`), // sql }, take: 3, skip: (page - 1) * 3, });
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/aee2c19ed6220cfc568620d831dd43e23100ac66

11 DISH AND ORDER CRUD

11.0 Dish Entity (08:59)

  • OneToMany, ManyToOne
  • 최종코드:

11.1 Create Dish part One (14:20)

  • DishOption
    1. 정석대로 Entity를 만든다.
    2. json field (니꼬는 이 방식을 선호한다.)
  • 일단 지금은 Dish를 만들어보자.
    • DishOption json 방식은 나중에!
  • 질문:
    • => DishOptionInputType이 자동으로 생겼다.
    • 질문: 근데 왜 DishOptionInputType만 생기고 DishInputType은 플레이그라운드에서 안보임?
  • @InputType('DishOptionInputType', { isAbstract: true }) // 이걸 왜 해야하는지 아직 모르겠음. @ObjectType() class DishOption { @Field((type) => String) name: string; @Field((type) => [String]) choices: string[]; @Field((type) => Int) extra: number; }
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d112b235814e4d99d196cbe444278d8c92ae8d7b

11.2 Create Dish part Two (11:20)

11.3 Edit and Delete Dish (14:14)

11.4 Order Entity (14:58)

nest g mo orders

11.5 Create Order part One (08:28)

11.6 Order Items (09:33)

  • order에는 dishes가 있어. 근데 사실 order에 dish array를 저장할게 아니라, 사실은...
  • DishOption이야.
  • 치킨을 주문하고 dish option에서 고르겠지? 그건 Dish의 options라는 컬럼을 통해 json 형태로 저장된다.
  • 이런걸 저장할 model이 필요하지만, 우리는 그런 모델이 없어.
    • => new entity 만들거야! = OrderItem
    • dish 안의 options는 entity가 아니라 json이기 때문에.
    • DishOption을 json으로 하면 어떤 음식을 주문할 때 이 음식의 옵션은 단순히 text여야 함. 왜냐하면 내일 주인이 옵션을 지울 수도 있잖아! 매번 주문이 유효했으면 좋겠거든.
    • 아하........ 언제 사라질지 모르니까....!!!!
    • 그래서 OrderItem이라는 Entity를 또 만들었다!
      ```
      @InputType('OrderItemInputType', { isAbstract: true })
      @ObjectType()
      @Entity()
      export class OrderItem extends CoreEntity {}
      • inverseSide 주목!!!!
    • ```
    • // Order에서 restaurant는 Restaurant에서 orders를 어떻게 가져와야하는지 생각했지만, 여기는 그럴 필요가 없다. // 왜? => 항상 반대의 관계에서 어떻게 되는지 명시해줄 필요는 없다. 항상 필요한건 X (inverseSide가 항상 필요하진 X) // inverseSide: 반대쪽 관계에서 접근을 하고 싶을 때만 해주면 됨. // inverseSide: restaurant.orders가 필요했었지! 그래서 inverseSide 자리에 restaurant.orders가 들어가있었지! // 근데 Dish쪽에서 orderItem으로 접근을 원하지 않아서 ㄱㅊ @Field((type) => Dish) @ManyToOne((type) => Dish, { nullable: true, onDelete: 'CASCADE' }) dish: Dish; // option은 언제 없어질지 모르니까 그때그때 json으로 저장하는거야. @Field((type) => [DishOption], { nullable: true }) @Column({ type: 'json', nullable: true }) options?: DishOption[];
  • 근데 createOrder 때 dish, options 에서 dish가 아니라 dishId만 받고싶어. 그건 다음강의에서!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/09312fbeb27c3d16f2c4cbae3d821d2bdae52eb5

11.7 Create Order part Two (07:21)

11.8 Create Order part Three (16:39)

11.9 Create Order part Four (09:27)

11.10 Create Order part Five (13:45)

11.11 Create Order part Six (12:10)

11.12 getOrders part One (17:31)

11.13 getOrders and getOrder (14:00)

11.14 Edit Order (13:34)

12 ORDER SUBSCRIPTIONS

12.0 Subscriptions part One (11:49)

  • subscription: resolver에서 변경사항이나 업데이트를 수신할 수 있게 해준다.
  • 간단한 subsription을 만들어보자!
    • 이제 인스턴스를 생성할거다. 바로 PubSub!
      • PubSub: publish and subscribe. 이걸로 app 내부에서 메세지를 교환할 수 있다.
    • 코드 (asyncIterator)
    • const pubsub = new PubSub(); @Subscription((returns) => String) // orderSubscription() { hotPotatos() { // GraphQL상으로는 string을 return하지만, 실제로는 asyncIterator을 return할거야. 이게 규칙이다! return pubsub.asyncIterator('hotPotatos'); }
    • 실행 후 에러
      • WS도 프로토콜이고, 이것은 Real Time을 처리하는 Web Socket을 말한다.
      • => Web Socket을 활성화 해야한다.
      • => Mutation, Query는 HTTP가 필요하고, Subscription은 WebSocket이 필요하다.
    • { "error": "Could not connect to websocket endpoint ws://localhost:3000/graphql. Please check if the endpoint url is correct." }
    • 코드
    • GraphQLModule.forRoot({ installSubscriptionHandlers: true, // 서버가 웹소켓 기능을 가지게 된다. ... }),
    • 실행 후 에러
      • why? subscription 연결하는 방법이 우리가 API를 연결하는 방법과 달라서 그렇다.
      • context: ({ req}) 에서 req(http)를 로그찍어보면 query, mutation 때는 잘 찍히는데, subscription을 했을 때는 req = undefined. 웹소켓에는 request가 없기 때문이지!
      • 웹소켓에는 다른게 있어! 다음 영상에서~
    • { "error": { "message": "Cannot read property 'user' of undefined" } }
  • npm i graphql-subscriptions // https://www.npmjs.com/package/graphql-subscriptions
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/398bc55cc4111ca61c16ffee5674470cd83e75c9

12.1 Subscriptions part Two (09:03)

  • 아무튼, query, mutation - http 요청은 완벽히 됨.
  • 그런데, 웹소켓을 통해 연결하는 순간 에러. req.user를 못찾는다.
  • 그래서 HTTP 프로토콜이 아닌, 웹소켓 프로토콜이 필요하다.
  • HTTP에는 request가 있지만 웹소켓에는 connection이라는 것이 있다.
    • 이제 이렇게 바꾸면 subscription run 했을 때 로딩바가 계속 돌아가면서 listening 중이게 됨.
    • 그리고 console.log('connection', connection);는 연결할 때 딱 한 번만 발생한다. HTTP와 웹소켓의 차이점이지.
    • HTTP는 request마다 토큰을 보내는데, 웹소켓은 딱 한번만 토큰을 보낸다.
    • 웹소켓은 연결을 시작할 때 토큰을 보내는데, 연결이 끝나지는 않는다.
    • 그래서 많은 이벤트를 받고 보낼 수 있음.
    • subscription은 public하게 만들 수 없음. 특정사람의 주문의 업데이트를 listening해야하니까.
    • 아무튼 나중에 connection을 이용할거야.
  • context: ({ req, connection }) => { if (req) { // console.log('req', req); return { user: req['user'] }; } else { console.log('connection', connection); } },
  • 이제 mutation을 만들어보자.
    • hotPotatos끼리 같아야하고, readyPotato끼리 같아야한다.
    • mutation을 날리면 subscription에서 listen할 수 있다.
  • @Mutation((returns) => Boolean) potatoReady() { // payload에는 resolver 함수의 이름이 있어야 한다. 트리거 이름이 아니다. pubsub.publish('hotPotatos', { readyPotato: 'Your potato is ready.' }); return true; } @Subscription((returns) => String) // orderSubscription() { readyPotato() { // GraphQL상으로는 string을 return하지만, 실제로는 asyncIterator을 return할거야. 이게 규칙이다! return pubsub.asyncIterator('hotPotatos'); }
  • 이제 누군가 주문을 업데이트할 때 그 주문을 publish할거야!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/55391c0d807df11b47f46750d5e33b7a917cd358

12.2 Subscription Authentication part One (13:27)

  • 이제 readyPotato resolver를 보호하고싶다!
    • @Role 데코레이터를 사용하자.
    • 근데 user가 안온다? 현재 jwt 토큰은 HTTP 헤더에서 온다. 근데 subscription은 웹소켓이잖아!
    • 그래서 jwt middleware가 쓸모가 없다.
  • Query&Mutation&Subscription에서의 Authentication!
    1. JWT module 삭제...상태로 다시 만들기.
    2. export class AppModule {}
    • 그런데도 query, subscription 날려보면 Forbidden resource 에러메세지가 뜬다.
    • 바로 Guard가 HTTP, WS를 위해 동작하고 있는 것이다.
    • auth.guard.ts에서 gqlContext 로그를 찍어보자.
      • query 날릴 때
        • header에 x-jwt 있음
      • subscription
        • { req: undefined }
        • 이 context는 어디서 온거지?
    1. Guard에게 필요한 정보 보내기 (.headers)
      • 이제 gqlContext에서 x-jwt 알 수 있음.
      • 이제는 auth.guard.ts에서 jwt.middleware.ts가 하던 일을 해야해
      • => HTTP resolver와 웹소켓 resolver를 한꺼번에 인증할 수 있어~ 다음강의에서!
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/33af2862d18f67f6a77c1d4a5d4abcffdecabac2

12.3 Subscription Authentication part Two (07:00)

  • HTTP, WS 호출은 app.module.ts - GraphQLModule.forRoot로 오기 전에 JwtMiddleware를 거쳤는데, 이제 WS도 인증해야해서 JwtMiddleware 삭제. 그래서 그냥 바로 GraphQLModule.forRoot로 옴
    • HTTP ? req : connection
    • 그리고 이제 guard가 호출됨. auth.guard.ts. 이제 여기서 JwtMiddleware가 해주던 decode를 대신 해줘야 함. 다 하고 gqlContext['user'] 에 넣어주면 그 후 @AuthUser decorator에서 받아볼 수 있음.
  • 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/345a3d57410efc82d2be2749effb955a62d2f33a

12.4 PUB_SUB (08:45)

  • pubSub 글로벌하게 사용하기!
    • 왜? pubSub은 하나만 써야하기 때문.
  • const pubsub = new PubSub();
    • 이건 전체 애플리케이션에서 하나여야함.
  • PubSub은 데모용입니다. 서버가 단일 인스턴스인 경우만.
    • = 동시에 많은 서버를 가지고 있는 경우, 이 PubSub으로 구현하면 안됨.
    • 모든 서버가 동일한 PubSub으로 통신해야함.
    • => 다른 분리된 서버에 PubSub을 저장하라.
    • => https://www.npmjs.com/package/graphql-redis-subscriptions
    • => RedisPubSub을 사용한다.
    • npm install graphql-redis-subscriptions 해서 RedisPubSub 부분만 바꿔줘도 됨.
    • 근데 대부분 한 서버만 써서 쓸 일은 거의 없을거. 일단 넘어간다~

12.5 Subscription Filter (09:40)

  • 이제 filtering을 해보자! 모든 update를 listen할 필요가 없기 때문이다.
  • order도 내 order 말고는 listen할 필요 없겠지!?
  • EX) 자, potatoId: 1인 potato의 update만 listen한다고 쳐보자!
    • 근데 readyPotato: potatoId여서 가능했지.
    • readayPotato: "Your potato ${id} is ready" 로 받고싶어. 이건 다음 강의에서!
  • @Mutation((returns) => Boolean) async potatoReady(@Args('potatoId') potatoId: number) { // payload에는 resolver 함수의 이름이 있어야 한다. 트리거 이름이 아니다. await this.pubSub.publish('hotPotatos', { readyPotato: potatoId, }); return true; } @Subscription((returns) => String, { // filter: (payload, variables, context) => { filter: ({ readyPotato }, { potatoId }, context) => { // console.log('payload', payload); // console.log('variables', variables); // console.log('context', context); return readyPotato === potatoId; }, }) @Role(['Any']) readyPotato(@AuthUser() user: User, @Args('potatoId') potatoId: number) { // console.log('😍 user'); // console.log(user); // GraphQL상으로는 string을 return하지만, 실제로는 asyncIterator을 return할거야. 이게 규칙이다! return this.pubSub.asyncIterator('hotPotatos'); }

12.6 Subscription Resolve (11:44)

  • readayPotato: "Your potato ${id} is ready" 로 받고싶어.
    • resolve 이용!
      @Subscription((returns) => String, {
          ...
        // 사용자가 받는 update 알림의 형태를 바꿔준다. output을 바꿔준다.
        resolve: ({ readyPotato }) =>
          `Your potato with the id ${readyPotato} is ready!`, // 원래 이걸 publish('hotPotatos', here) 에서 here자리에 넣었었잖아!
      })
  • pubSub은 db에서만 쓸 수 있는게 아니야.
    • driver가 위치를 보고하면 db에 저장하지 않고 pubSub에 push하도록 만들 수 있어. db에 저장할 필요는 없으니까.
  • subscription 필요 개수
    • Owner: Pending Orders
      • subscription: newOrder
      • trigger: createOrder(newOrder) // createOrder 할 때마다 newOrder를 trigger하겠다.
    • All: Order Status
      • s: orderUpdate
      • t: editOrder(orderUpdate)
    • Delivery: Pending Pickup Order
      • s: orderUpdate
      • t: editOrder(orderUpdate)
    • 언제 알림을 publish하고, 누가 subscription을 만들지 알아야한다.

'Archive' 카테고리의 다른 글

for architecture arm64 에러  (0) 2020.10.29
다니엘 콜 - 봉제인형 살인사건 (스포주의)  (0) 2020.10.24
2020년 개발 일지  (0) 2020.03.05
[Roubit] 개발일지  (2) 2020.02.25
[한이음/IoT] 연결  (0) 2019.06.22
출처: https://mingos-habitat.tistory.com/34 [밍고의서식지:티스토리]