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
- vscode extension 추천
- VSCODE - EXTENSION - gitignore (CodeZombie) - install
- Command Palette (Cmd + p) - >gitignore - 원하는 언어 선택 - 자동으로 gitignore 만들어줌!
- 최종 코드: https://github.com/daheeahn/nuber-eats-backend/commit/f27a99334ade15d74b01c3ea1fe16aa5d664dfa0
0.7 This Course Structure
- db 작업 하기 전에, (user, auth) graphql이 nestjs에서 어떻게 작동하는지부터 볼거야.
- 같은 컨셉으로 작은 결과물부터 만든다. 미니 프로젝트~ 그 후에 데이터베이스.
- nestjs, graphql, db 활용을 잘하게 되면 클론코딩은 아주 빠르게 할 수 있어.
1 GraphQL API
1.0 Apllo Server Setup
- NestJS - GraphQL 문서
- apllo-server-express 기반으로 작동한다.
npm i @nestjs/graphql graphql-tools graphql apollo-server-express
- AppModule은 main.ts로 import되는 유.일.한 모듈이다!
- => GraphQL 모듈은 AppModule에 import 되어야 함
- 최종코드와 같이 쓰고 서버 켜면 다음과 같은 에러가 난다.
- 맞다. GraphQL은 이런걸 같이 만들어줘야하지. 1.1에서 해결하자!
Apollo Server requires either an existing schema, modules or typeDefs
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/950956a6c303e689b94956c0f2bd2472f6ff41b1
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)
- 질문: veganOnly를 optional로 만들고 싶다면?
- ???
- Args 추가하기
@Query((returns) => [Restaurant]) restaurants(@Args('veganOnly') veganOnly: boolean): Restaurant[] { console.log('veganOnly', veganOnly); return []; }
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/fd62c173605a059c5cffae73df5cc3450418fe8d
1.4 InputTypes and ArgumentTypes (08:54)
- CreateRestaurantDto 이용 & not InputType but ArgsType
- 질문: hi-nest에서 했듯이 이렇게는 못써주나요? 이렇게 하니까 args들이 안나오긴 하는데 안되는 이유가 뭐죠? 똑같은걸 또 써줘야하는게 싫다.[ ] 질문 올리기
export class CreateRestaurantDto extends Restaurant { }
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/6fc1ad503bbaeec84791eae746be4617bea1b483
1.5 Validating ArgsTypes (04:28)
npm i class-validator class-transformer
- dto 타입체크, 글자 수 제한 validator & decorator. perfect!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/9b200c7e27dee15d739f037240b606327c750ca5
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 생성하자!
- 일단 데이터베이스에 유저를 만들어줘야 함. 그래야 연결 가능.
- localhost > username 으로 들어가는데, localhost 클릭하면 postgre 내용물과 똑같은 데이터베이스 보인다.
- 포트번호 5000 => initalize
- 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
- https://typeorm.io/#/undefined/installation
- 여기 보면 쓰는 db에 따라 각각 다른걸 까는 것을 알 수 있다. 우리는 PostgreSQL 쓰니까 pg!
- https://typeorm.io/#/undefined/installation
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) 코드에 직접 쓰거나
- 이제 TypeORM 모듈을 앱모듈에 설치할 것이다
npm run start:dev
- 작동 잘 된다!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/0fe9bbc537998930f0f30419356e96dfea534149
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)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/7bc1b3755c3295f66d4820dd76bbc956984e48af
- 에러해결: https://github.com/daheeahn/nuber-eats-backend/commit/2f59aef2be4d462a663c5b9efce75ddbe7c5b88b
- 이것 땜에 env가 안먹혔음... ㅎㅎ
2.6 Validating ConfigService (05:44)
- joi
- 데이터 유효성 검사 툴
- https://joi.dev/api/
npm i joi
- all javascript. => * as 로 import
- https://docs.nestjs.com/techniques/configuration#schema-validation
- nestjs에서도 joi 사용
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/363ff8a084968056fbf23d6c8f684382d4cf9b7a
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가 어디있는지 알려줘야 한다.
entities: [Restaurant],
- ??
- 이제 저장하면 콘솔에 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을 사용할 것이다.
- ORM
3.1 Data Mapper vs Active Record (07:04)
- 자, 어떻게 typescript를 이용해서 DB에 있는 Restaurant에 접근할 수 있을까?
- => Typeormmodule - Repository 사용할 것이다.
- What is Repository? (Data Mapper)
- https://typeorm.io/#/active-record-data-mapper
- DB와 상호작용할 때 쓰는 패턴
- Ruby on Rails, Django 아마 Active Record 사용할 것.
- NestJS에서는 Data Mapper 패턴 사용
- Active Record
- extends BaseEntity
- 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)
- repository를 import 해보자.
- restaurant.module에서. (restaurant.module에서 restaurant repository가 필요해서)
// restaurant.module.ts imports: [TypeOrmModule.forFeature([Restaurant])],
- RestaurantService에서 repository를 사용하기 위해 service를 RestaurantResolver에 import 했다.
// restaurant.resolver.ts constructor(private readonly restaurantService: RestaurantService) {}
- 이제 repository를 service.ts에 inject해보자.
- 그래서 restaurant.module에 import 해준거.
- 최종코드
3.3 Recap (04:15)
- 지금까지 정리
- TypeOrmModule에 Restaurant라고 하는 entity를 가지고 있다. => Restaurant가 db가 된다.
- module에서는 Restaurant를 import했다. forFeature: TypeOrmModule이 특정 feature를 import할 수 있게 해준다.
- 이 경우, feature is Restaurant entity.
- resolver에 RestaurantService를 추가했다.
- 이건 restaurants.module.ts에서 provider에 있어야한다.
- why? RestaurantResolver class에서 restaurantService를 inject할 수 있어야 하기 때문이다. (in contructor)
- restaurant.service.ts에서 restaurants는 뭘까? restaurant entity의 repository이다.
- reposity를 inject했으므로 getRepository()가 작동되는 것이다.
- 이게 바로 NestJS TypeORM을 사용하는 이유.
- => repository의 모든 옵션 사용 가능
- later: queryRunner - repository를 사용해서 entity를 만드는 방법을 보여줄 것이다.
3.4 Create Restaurant (08:59)
- method: class 안에 있는 function
- create vs save on typeorm
- new Photo (typeorm 문서)
- js, ts 측면에서 class를 생성하는 것 밖에 안된다. 전혀 DB를 건들지 않는다. just create
- new Photo (typeorm 문서)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/461f1f4c7294423965f1259c2e2f5438e01dd4e3
3.5 Mapped Types (12:23)
- graphql schema와 db는 모두 entity.ts 한 파일에서 관리되는데, dto는 통합되어있지 않지.
- 이 3개를 모두 한 번에 만들어낼거야.
- Code First
- => Mapped Types 사용
- https://docs.nestjs.com/graphql/mapped-types
- base type 바탕으로 다른 버전들을 만들 수 있게 해준다.
- 전부 이걸 쓰면 InputType이 되기 때문에 resolver에서도 ArgsType 말고 InputType을 써준다.
- 나머지 자세한 사항은 코드에서
- dto 유효성 검사는 다음 강의에서!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/4c3a9579d9257cbe8745846da1b7a0623cffa2ac
3.6 Optional Types and Columns (08:42)
- dto 유효성 검사도 entity.ts에서!
- default value:
@IsOptional(), @Column({ default: true })
- nullable을 쓸 수도 있다.
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/c56caec3bc9180355671a82e8b4292793a40ad33
3.7 Update Restaurant part One (08:05)
- @InputType(): @Args('input')
- @ArgsType(): @Args()
- 최종코드: 3.8과 함께
3.8 Update Restaurant part Two (07:56)
- update
- 이 때, { ...data } 대신 data를 넣으면 오류가 난다. 왜그럴까?
this.restaurants.update(id, { ...data });
- data vs { ...data }
- 그냥 data를 하면 타입이 좀 이상한가보다.
======== data [Object: null prototype] { name: '222334', address: '강남' } -------- { ...data } { name: '222334', address: '강남' }
- 없는 id를 업데이트해도 에러가 안난다. .update가 없는 id를 상관하진 않기 때문이다.
- 최종코드
4 USER CRUD
4.0 User Module Introduction (02:06)
- 이제 backend 클론을 시작해보자!
- User Module
- 최종코드:
4.1 User Model (07:07)
- User Model을 만들어보자.
- 기본적으로 공유되는 모든 것들은 commonModule로 갈 것이다.
nest g mo users nest g mo common // 기본적으로 공유되는 모든 것들을 위해.
- CoreEntity
- CreatedAt, UpatedAt
- https://typeorm.io/#/entities/special-columns
- 이제 postico 보자. User Table 생겼지.
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/c24d61dcbd8b16086655a4d8e782cac22055f762
4.2 User Resolver and Service (05:20)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/24a91d093a03434ae91a93e76e4485839f5cd638
4.3 Create Account Mutation part One (06:31)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/977aba7bfe19bd0039ab73c87c726bfb9d7eabbd
4.4 Create Account Mutation part Two (08:07)
- enum: registerEnumType
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/ce5cb84791eb66ef96d9670fe5e54e9f812bcf82
4.5 Create Account Mutation part Three (07:00)
- 에러를 내가 조절한다. 에러 메세지도 내가 만들고.
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/e2bde68be08488fcfeb05e7f7aac5e2df12eaba2
4.6 An Alternative Error (05:05)
- 하나의 방식 추천. 코드 개선할 수 있을거야!
- string | undefined return하는 대신에 array or object를 리턴해보자.
- 원하는걸 해봐~
- object가 좋다! [false, 'error messgae']보다 직관적이다. 아래 최종코드와 같이 리턴할 것이다.
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/69326c3b72b1164c87a04d1f6804dbdf9eda194e
4.7 Hashing Passwords (09:02)
- hash: 단방향 함수. 비밀번호 to 해시. NO 해시 to 비밀번호.
- listener and subscriber - BeforeInsert
- https://typeorm.io/#/listeners-and-subscribers
- entity에 무슨 일이 생길 때 실행된다.
- @AfterLoad: post를 load할 때마다 load한 다음 뭔가 다른 걸 할 수 있다.
- 핵심: @BeforeInsert: hashPassword
- 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)
- 공통으로 쓰이는 MutationOutput, user entity validation
- rename MutationOuput to CoreOutput
- ** output.dto.ts에 @ObjectType() 써주기. 빼먹음
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/8ef6bcbf181548606d3e39d7568a0e67378c9e3e
4.9 Log In part Two (10:20)
- checkPassword를 entity 안에 넣어줬다. java가 생각나네,,, ㅎㅎ 오랜만이다.
- findOne한 user를 user.checkPassword로 접근. 이게 바로 method.
- bcrypt.compare
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/c0666b534eabe914e5d326a8a76a788539474f53
5 USER AUTHENTICATION
5.0 Introduction to Authentication (02:00)
- 우리가 직접 적용
- 먼저 수작업으로 해보자~!
- app.module.ts 보면 마법같은 모듈 많지만, jwt는 우리가 직접 만들어보자. root를 위한 모듈을 직접 만들어보자구!
- 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가지. (설정이 있느냐 없느냐.)
- static module
- UsersModule
- dynamic module
- GraphQL.forRoot
- static module
- dynamic module 이겠지~
- 동적모듈은 사실 결과적으로 정적모듈이 된다.결국 이렇게 만들게 된다.
결국 동적모듈은 중간과정인 것. GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/schema.gql'), }), // 정적 // to GraphQLModule // 동적 // 근데 어떻게 이렇게 되는거징.
- 동적모듈은 사실 결과적으로 정적모듈이 된다.결국 이렇게 만들게 된다.
- this.config처럼 이런 식으로 jwt 모듈을 사용하고 싶어서.
- 최종코드: 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 테스트파일은 잠시 삭제.
- 이걸 JwtModule에도 똑같이 적용시켜보자.
- global module로 설정해주면 굳이 import 안해줘도 된다. like ConfigModule
isGlobal()
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/7992f75c85622227888ce41c27fbd6d7ad76959e
5.4 JWT Module part Two (08:23)
- option을 어떻게 jwtServic로 보낼까?
- 최종코드에서 확인
- useClass, useFactory, useValue 등 사용 가능.
- 이렇게 직접 모듈을 만들어봤다!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/4457adeeb54d886b33ac34c88d5278c3c1870bbf
5.5 JWT Module part Three (05:43)
- 이 모듈을 이 프로젝트에서만 사용할건지, 아닌지는 본인 선택. => userId를 받으면 다른 곳에서는 조금 쓰긴 어렵겠지.
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/1baf2b9a1be95f967f24ec8ca171755e022be834
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)
- https://docs.nestjs.com/guards
- guard: 방패. request를 멈추게 할 것이다.
nest g mo auth
- middleware처럼 injectable을 사용해서 guard를 만들어보자.
- authentication: 끝. 누가 자원을 요청하는지 확인하는 과정. token으로 identity 확인.
- authorization: user가 어떤 일 하기 전에 permission 확인. 이건 아직 안했어!
- => restaurant를 다룰 때가 되면 guards에 대해서 좀 더 깊게 볼 것이다.
- Comming soon...
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/3bf3d198a4f204e4570032378b14e25792135ffc
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.
- header에 토큰을 보낸다.
- header는 http 기술이 쓰인다.
- http 기술 쓰기 위해 middleware를 만들었다.
- 이 미들웨어는 header를 가져다가 jwtService.verify 한다.
- id를 찾으면, 해당 id 가진 user를 찾는다.
- user 찾으면 user를 request object에 붙인다.
- 모든 resolver에서 이 request object 사용 가능.
- 만약 token이 없거나 에러가 있다면, token에서 user 찾을 수 없으면 => request에 뭣도 붙이지 않는다.
- apllo server나 graphql의 content는 모든 resolver에 정보를 보낼 수 있는 property다.
- context가 매 request마다 호출될 것이다.
- JwtMiddleware 거치고, graphql context에 request user 보내는거야.
- ** 이제 request user라 하지 않고, context user라 하겠음
- Guard
- function의 기능을 보충해준다.
- false return하면 request를 중지시킨다.
- context를 gql context로 바꿔주는 작업도 필요하다.
- request가 middleware, apollo server, guard 거쳐서 resolver에 온다.
- decorator로 온다 이제. AuthUser => graphql context로!
- decorator는 value를 return 해야만 한다.
- 다시 정리.
- 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)
- 팁
Boolean(user) // user가 있으면 true 없으면 false
- 실수
- CoreOutput (구 MutationOutput)에 ObjectType 꼭 써주기. (4.8에서 안써줌)
- rename MutationOutput to CoreOutput
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/0ddb5ae36ef7f4d49431c1c995e176e0c73092c9
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)
.save
를 해야 BeforeUpdate가 먹힌다.- 왜? this.users.update는 진짜 entity를 update하는 것이 아니거든. 그래서 BeforeUdpate가 안먹히거든.
- .save는
- email verify하는 모듈 만들어볼거야~!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/922fd85b0590c1b2d23acf606d7bbd290a411db4
5.16 Recap (04:50)
- email: "email to update", password: undefined 이면 password는 nullable이 아니기 때문에. 에러가 난다.
- =>
this.users.update(userId, { ...editProfileInput })
으로 해줘야 필요한 column만 업데이트된다. - for excuting hook!
- =>
- 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)
- Random Code
- random
Math.random().toString(36).substing(2)
- uuid
- https://www.npmjs.com/package/uuid
npm i uuid
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/685ebff6e950e0e70a5b1eab694b198acd553068
- 6.0으로 잘못 커밋했음~ 6.1 맞음!
6.2 Verifying User part One (11:40)
{ relations: ['user'] }
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/b3f460e19618078cb9174e7af10daa2808ad8487
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개의 메일 보낼 수 있음.
- 가입하기
- 가입 이미되어있음.
- API KEY
- 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)
- 이제 메일을 보내보자!
- https://app.mailgun.com/app/sending/domains/sandboxbb0a1cfbcdf044e49f7ef9e8a07aa110.mailgun.org
- 여기에서 cURL 명령어 복붙!
- nodejs에는 프론트처럼 fetch가 없어.
- nodejs에서 리퀘스트하려면 모듈 설치해야함.
- https://github.com/sindresorhus/got
npm i got npm i form-data
- 터미널신기...
node > Buffer.from('api:').toString('base64') 'YXBpOg=='
- 이제 got으로 post 리퀘스트를 보낼 것이다.
- 최종코드: 6.9에 같이
6.8 Beautiful Emails (07:59)
- 예쁘게 html로 이메일 보내기
- 패스
6.9 Refactor (12:35)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/9f19843fe7757eef74ae559b615808ee66152cec
테스트는 나중에~!
10 RESTAURANT CRUD
10.0 Restaurant Models (11:00)
- OneToMany, ManyToOne
10.1 Relationships and InputTypes (08:19)
- Restaurant Entity
- Category Entity
- 한 카테고리는 여러 식당을 가질 수 있다.
- https://typeorm.io/#/many-to-one-one-to-many-relations
- 카테고리를 지우면? Restaurant의 categoryId를 null로? or 아예 Restaurant를 지워?
- 전자.
- 에러
- ObjecType과 InputType이 같은 name을 사용하면 X이럴 땐 InputType('name')에 name을 써줘서 분리해야 한다.
@InputType('CategoryInputType', { isAbstract: true }) @ObjectType() // for graphql automatic schema export calss Category extends CoreEntity { ... }
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/fb7cc579368ca3bd2b496b72774dd0d248941d26
10.2 createRestaurant part One (07:36)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d5e8aabc0d57118410762dd8a35b40b8130b2332
10.3 createRestaurant part Two (14:12)
- slug: url에 id 대신 문자로 표현하는 것.
const categorySlug = categoryName.replace(/ /g, '-');
@Column({ unique: true })
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/fde98f955d9a1d9c61158808abb801265c7e89e1
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)
- guard를 앱 모든 곳에서 사용하고 싶다면
- just provide APP_GUARD. awesome!
- => 이제 @UseGuards는 필요없다.
- 그러나 createAccount에서는 허용되면 안되잖아? 예외를 적용해야겠지.
if (!role) { return true; }
- => 이제 @UseGuards는 필요없다.
- just provide APP_GUARD. awesome!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/cc3189211b5d34969b9bc8a908285fe207ef0e8c
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 참고
- repository를 entity에서 가져오지 말고, 직접 만든 repository로 import 해줘야 한다.
...(category && { category })
10.10 Edit Restaurant Testing (05:55)
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d67b6ab5712342577dfc6451eb2571cd99ac6090
10.11 Delete Restaurant (07:10)
- edit과 정말 비슷하다! 하나하나 내 손으로 했는데 전혀 문제 없었다~
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/c1770e7b919f29fe4f99c9800db83b3493f1cc3b
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)
- @Parent에 접근!!!! 와우!!!
@ResolveField((type) => Int) restaurantCount(@Parent() category: Category): Promise<number> { return this.restaurantService.countRestaurants(category); }
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/2d1d0423bbd00fae250da8ac0ab9c3f0263a9fd8
10.14 Category (08:21)
- { relations: ['restaurants'] }, // 카테고리 안의 레스토랑을 보고싶다면 꼭!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/846df596122faead12819e63f6fdf2db601543a8
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)
- findAndCount
- 레스토랑 리스트~ 이것도 정말 쉽다! 전에 하던 것과 똑같.
- with PaginationInput, PaginationOutput
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/694ce1118d648e38798dfda4caaf6cba75d3b2b0
10.17 Restaurant and Search (12:08)
- where Like
- https://github.com/typeorm/typeorm/blob/master/docs/find-options.md#advanced-options
- https://www.tutorialspoint.com/sql/sql-like-clause.htm
- 200%: 200으로 시작
- %200%: 어느곳에라도 200이 포함된 곳
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
- 정석대로 Entity를 만든다.
- 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)
- dish repository가 필요해!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d51e0013c44a0d775cf8d77159b3fdc9e7d9398a
11.3 Edit and Delete Dish (14:14)
- id 넘겨주면서 save 하면 해당 entity를 update해준다! id가 없으면 create.
- DishChoice 추가
- edit: save, delete: delete
- findOne할 때 relations
- dish가 있는지, 해당 식당 주인만 할 수 있는지 depensive programming!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/803a7ffa0e69da92cb37e7abb521224192b4af7b
11.4 Order Entity (14:58)
nest g mo orders
- OneToMany, ManyToOne
- ManyToMany + JoinTable
- https://typeorm.io/#/many-to-many-relations
- JoinTable은 소유하고 있는 쪽의 relation에 추가해주면 된다.
- Question : Category = M : N
- Order : Dish = M : N
- Order(Question) entiry에서 dishes(categories)를 정의해줄 때 ManyToMany & JoinTable을 써준다.
- Dish entity에는 orders를 따로 정의하지 X
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/45e6ad0a8a9afbb0282c27347e5b03838f9ff46e
11.5 Create Order part One (08:28)
- 근데 create order 때 dishes가 필요하긴한데, dishes의 모든 것이 필요하진 않아!
- 다음 강의에서 설명!
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/d563a5595e03aee63f54a8cdf84bdeef0da42518
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)
- order.items.options는 json으로 입력된다는 사실을 잊지말기! option은 언제 없어질지 모르니까 그때그때 json으로 저장하는거야.
- OrderItemOption 이라는 것도 만들었음.
- 완전히 이해 안가지만, 대강 이해는 간다. 어떤 식으로 해야겠구나~
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/55d13d77bbd02bc2a03d5803794ea09fcaffbf77
11.9 Create Order part Four (09:27)
11.10 Create Order part Five (13:45)
- return을 사용하고 싶다면 for of를 사용하자.
// items.forEach(async (item) => { // 안에서 return { ok: false }를 할 수 없다. => for of 사용 for (const item of items) {
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/690117907693827153be75cc9613ae760eb91caa
11.11 Create Order part Six (12:10)
- through table = in between table: ManyToMany 관계를 만들면 자동으로 만들어지는 테이블
- 근데 orderItems에 item.options해주는데 얘는 DishOption이 아니라 OrderItemOption인데... 흠~ 암튼 잘 모르겠다!
- order 개수대로 for문 돌고 필요한거 push 해주고 그런거
!
~ - 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/fb150c8d854f8077d146576f5436b3a93ddeaad9
11.12 getOrders part One (17:31)
- .flat(1) // 1 is default value. = depth
- 질문: relations로 orders를 가져왔는데, 그 안에 또 sub인 items를 가져오고 싶으면?
- 코드 (relations 복습~!)
const restaurants = await this.restaurants.find({ where: { owner: user, }, relations: ['orders'], });
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/119aa48e9352c2a14a57b8a19ad62c4b18dbcdd5
11.13 getOrders and getOrder (14:00)
- status 있을 때만 status 보내자! status: undefined 이면 안되니까.
orders = await this.orders.find({ where: { driver: user, ...(status && { status, }), }, });
- @RelationId 복습
@RelationId((order: Order) => order.driver) driverId: number;
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/768157360cdfe6941958f5511cf4b4dadd455378
11.14 Edit Order (13:34)
- update: save에 id 넣어줌 = 복습
await this.orders.save([ { id: orderId, status, }, ]);
- 최종코드: https://github.com/daheeahn/nuber-eats-backend/commit/f7fb84e8cc47afb6be00bca899324c4cfcab1d44
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" } }
- 이제 인스턴스를 생성할거다. 바로 PubSub!
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!
- JWT module 삭제...상태로 다시 만들기.
export class AppModule {}
- 그런데도 query, subscription 날려보면 Forbidden resource 에러메세지가 뜬다.
- 바로 Guard가 HTTP, WS를 위해 동작하고 있는 것이다.
- auth.guard.ts에서 gqlContext 로그를 찍어보자.
- query 날릴 때
- header에 x-jwt 있음
- subscription
{ req: undefined }
- 이 context는 어디서 온거지?
- query 날릴 때
- 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자리에 넣었었잖아! })
- resolve 이용!
- 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을 만들지 알아야한다.
- Owner: Pending Orders
'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 |