NestJs Chapter 7

yeopยท2022๋…„ 7์›” 21์ผ

Nest JS ์ •๋ฆฌ

๋ชฉ๋ก ๋ณด๊ธฐ
7/10

๐Ÿ“‘ Restaurant CRUD

๐Ÿ”ท Many-to-one / One-to-many

Many-to-one / One-to-many ๊ด€๊ณ„๋Š” A๊ฐ€ B์˜ ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๋ฅผ ํฌํ•จํ•˜์ง€๋งŒ B๋Š” A์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ํ•˜๋‚˜๋งŒ ํฌํ•จํ•˜๋Š” ๊ด€๊ณ„์ด๋‹ค.

  • @OneToMany(): ์ผ๋Œ€๋‹ค ๊ด€๊ณ„์—์„œ '๋‹ค'์— ์†ํ•  ๋•Œ ์‚ฌ์šฉ
    (DB์— ํ•ด๋‹น ์ปฌ๋Ÿผ์€ ์ €์žฅ๋˜์ง€ ์•Š์Œ)
  • @ManyToOne(): ์ผ๋Œ€๋‹ค ๊ด€๊ณ„์—์„œ '์ผ'์— ์†ํ•  ๋•Œ ์‚ฌ์šฉ
    (DB์— user๋ฉด userId๋กœ id๊ฐ’๋งŒ ์ €์žฅ๋จ)
  // Category Entity
  @Field((type) => [Restaurant])
  @OneToMany((type) => Restaurant, (restaurant) => restaurant.category)
  restaurants: Restaurant[];
  // Restaurant Entity
  @Field((type) => Category)
  @ManyToOne((type) => Category, (category) => category.restaurants)
  category: Category;

๐Ÿ”ท Unique Schema Name

์Šคํ‚ค๋งˆ์˜ ์ด๋ฆ„์€ ๊ณ ์œ  ํ•ด์•ผํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๊ฐ Entity์—์„œ @ObjectType ๊ณผ @InputType ๊ฐ€ ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ์Šคํ‚ค๋งˆ๊ฐ€ ๋œ๋‹ค. ์•„๋ž˜์˜ ์—๋Ÿฌ ํ•ด๊ฒฐ์„ ์œ„ํ•ด์„œ๋Š” @InputType ์— ๋”ฐ๋กœ ์ด๋ฆ„์„ ๋ถ€์—ฌํ•ด์•ผํ•œ๋‹ค.
Error: Schema must contain uniquely named types but contains multiple types named "Category".

@InputType('CategoryInputType', { isAbstract: true })
@ObjectType()
@Entity()
export class Category extends CoreEntity {
 	//.....
}

๐Ÿ”ท Database Relations Option

2๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”์„ ์—ฐ๊ฒฐํ–ˆ์„ ๋•Œ, ํ•œ ์ชฝ์˜ ํ…Œ์ด๋ธ” ์š”์†Œ๋ฅผ ์‚ญ์ œํ–ˆ์„ ๊ฒฝ์šฐ ์—ฐ๊ฒฐ๋œ ์ชฝ์˜ ํ…Œ์ด๋ธ”์˜ ์ƒํƒœ๋ฅผ ๊ฒฐ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • onDelete: "RESTRICT"|"CASCADE"|"SET NULL"
    Specifies how foreign key should behave when referenced object is deleted

https://orkhan.gitbook.io/typeorm/docs/relations#relation-options

@Field((type) => Category, { nullable: true })
@ManyToOne((type) => Category, (category) => category.restaurants, {
    nullable: true,
    onDelete: 'SET NULL',
})
category: Category;

๐Ÿ”ท Role Authorization / Authentication

๐Ÿ”นMetaData / Role Decorator

Resolver๋Š” ๊ฐ๊ฐ ๋‹ค๋ฅธ ๊ถŒํ•œ ์ฒด๊ณ„๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ์œ ์—ฐํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ถŒํ•œ ์ฒด๊ณ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ metadata๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค. Nest๋Š” @SetMetadata() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ํ†ตํ•ด Resolver์— ์ปค์Šคํ…€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ถ™์ด๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

  • ex) @SetMetadata('role', Role.Owner)
    key - ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜๋Š” ํ‚ค๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฐ’
    value - ํ‚ค์™€ ์—ฐ๊ฒฐ๋  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

https://docs.nestjs.com/guards#setting-roles-per-handler

route์—์„œ ์ง์ ‘ @SetMetadata()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ์Šต๊ด€์ด ์•„๋‹ˆ๋‹ค.
๋Œ€์‹  ์•„๋ž˜์™€ ๊ฐ™์ด ์ง์ ‘ ์ปค์Šคํ…€ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

//role decorator
import { SetMetadata } from '@nestjs/common';
import { UserRole } from 'src/users/entities/user.entity';

type AllowedRoles = keyof typeof UserRole | 'Any';
export const Role = (roles: AllowedRoles) => SetMetadata('roles', roles);

๐Ÿ”น APP_GUARD

๋‹ค์Œ๊ณผ ๊ฐ™์ด APP_GUARD๋ฅผ ์‚ฌ์šฉํ•  ์‹œ ๋ชจ๋“  resolver์— ๋Œ€ํ•ด Guard๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

//app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useValue: AuthGuard,
    },
  ],
})
export class AuthModule {}

https://docs.nestjs.com/guards#binding-guards

โ— AuthGuard์™€ Role Decorator(MetaData) ์—ฐ๊ฒฐ

  • Reflector๋ฅผ ์‚ฌ์šฉํ•ด MetaData๋ฅผ ๋ฐ›๋Š”๋‹ค.
  • Metadata๊ฐ€ ์„ค์ •๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด public(๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ)
  • Metadata๊ฐ€ ์„ค์ •๋˜์–ด์žˆ์œผ๋ฉด private(MetaData๋ฅผ ์ด์šฉํ•ด ํŠน์ • role๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ œํ•œ)
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(context: ExecutionContext) {
    const roles = this.reflector.get<AllowedRoles>(
      'roles',
      context.getHandler(),
    );
    if (!roles) {
      return true;
    }
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user: User = gqlContext['user'];
    if (!user) {
      return false;
    }
    if (roles.includes('Any')) {
      return true;
    }
    return roles.includes(user.role);
  }
}

๐Ÿ”ท Edit Restaurant CRUD

๐Ÿ”น @ RelationId

Field์— ํŠน์ • relation์˜ id๋ฅผ ๋กœ๋“œํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Restuarant ์—”ํ„ฐํ‹ฐ์— Many-to-one์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์ƒˆ ์†์„ฑ์„ @RelationId๋กœ ํ‘œ์‹œํ•˜์—ฌ ์ƒˆ Relation ID๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ many-to-many๋ฅผ ํฌํ•จํ•œ ๋ชจ๋“  ์ข…๋ฅ˜์˜ ๊ด€๊ณ„์—์„œ ์ž‘๋™ํ•œ๋‹ค. Relation ID๋Š” ์ถ”๊ฐ€/์ œ๊ฑฐ/๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š”๋‹ค.

  //Restaurant Entity

  @Field((type) => User)
  @ManyToOne((type) => User, (user) => user.restaurants, {
    onDelete: 'CASCADE',
  })
  owner: User;

  @RelationId((restaurant: Restaurant) => restaurant.owner)
  ownerId: number;

๐Ÿ”น Custom repositories

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ์œ„ํ•œ ๋ฉ”์†Œ๋“œ๋ฅผ ํฌํ•จํ•ด์•ผ ํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
์ผ๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ์ž ์ง€์ • ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ๋‹จ์ผ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด ์ƒ์„ฑ๋˜๊ณ  ํŠน์ • ์ฟผ๋ฆฌ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์•„๋ž˜์™€ ๊ฐ™์ด getOrCreate(name: string)์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด this.categories.getOrCreate(...)์™€ ๊ฐ™์ด ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

// @EntityRepository(Category)
@CustomRepository(Category)
export class CategoryRepository extends Repository<Category> {
  async getOrCreate(name: string): Promise<Category> {
    const categoryName = name.trim().toLowerCase();
    const categorySlug = categoryName.replace(/ /g, '-');
    let category = await this.findOne({ where: { slug: categorySlug } });
    if (!category) {
      category = await this.save(
        this.create({ slug: categorySlug, name: categoryName }),
      );
    }
    return category;
  }
}

TypeOrm 0.2.45๊นŒ์ง€๋Š” @EntityRepository๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ deprecated ๋˜๋ฉด์„œ ์ง์ ‘ decorator๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค.
https://stackoverflow.com/questions/71557301/how-to-workraound-this-typeorm-error-entityrepository-is-deprecated-use-repo
๋‹ค์šด๊ทธ๋ ˆ์ด๋“œํ•ด๋„ ์—๋Ÿฌ stackoverflow๋Œ€๋กœ ํ•ด๋„ ์—๋Ÿฌ 8์‹œ๊ฐ„ ์žก์•„๋จน์Œ...
์„ธ์ƒ์ด ๋‚  ์–ต๊นŒํ•˜๋„คใ…‹ใ…‹ใ…‹์ผ๋‹จ ์ง„๋„ ๋‚˜๊ฐ€๊ณ  ๋‹ค์Œ์— ๊ณ ์น˜์ž..

๐Ÿ”ท Categories Part

๐Ÿ”น@ ResolveField()

๋งค request๋งˆ๋‹ค ๊ณ„์‚ฐ๋œ field๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค.
(DB์—๋Š” ์กด์žฌํ•˜์ง€ ์•Š๊ณ  GraphQL ์Šคํ‚ค๋งˆ์—๋งŒ ์กด์žฌ)

@Resolver((of) => Category)
export class CategoryResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

  @ResolveField((type) => Int)
  restaurantCount(): number {
    return 80;
  }

  @Query((returns) => AllCategoriesOutput)
  allCategories(): Promise<AllCategoriesOutput> {
    return this.restaurantService.allCategories();
  }
}

๐Ÿ”ท Repository method Option

๐Ÿ”น Like

const [restaurants, totalResults] = await this.restaurants.findAndCount({
        where: {
          name: Like(`%${query}%`),
        },
      });

์œ„์˜ ์ฝ”๋“œ๋Š” ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

SELECT * FROM "post" WHERE "title" LIKE '%{query}%'

https://orkhan.gitbook.io/typeorm/docs/find-options#advanced-options
https://github.com/typeorm/typeorm/blob/master/docs/find-options.md#advanced-options

๐Ÿ”น ILike

Like๋Š” ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€๋งŒ ILike๋Š” ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๋Š”๋‹ค.

๐Ÿ”น Raw

const [restaurants, totalResults] = await this.restaurants.findAndCount({
        where: {
          name: Raw(name => `${name} ILIKE '%${query}%'`),
        },

๐Ÿ”นSQL - LIKE Clause

SQL LIKE ์ ˆ์€ wildcard ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ์œ ์‚ฌํ•œ ๊ฐ’๊ณผ ๋น„๊ตํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค. ํผ์„ผํŠธ ๊ธฐํ˜ธ(%)๋Š” 0, ํ•˜๋‚˜ ๋˜๋Š” ์—ฌ๋Ÿฌ ๋ฌธ์ž๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋ฐ‘์ค„(_)์€ ๋‹จ์ผ ์ˆซ์ž ๋˜๋Š” ๋ฌธ์ž๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐํ˜ธ๋Š” ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

EX )

WHERE SALARY LIKE '200%' : 200์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
WHERE SALARY LIKE '%200%': ์–ด๋А ์œ„์น˜๋“  200์ด ์žˆ๋Š” ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
WHERE SALARY LIKE '_00%': ๋‘ ๋ฒˆ์งธ ๋ฐ ์„ธ ๋ฒˆ์งธ ์œ„์น˜์— 00์ด ์žˆ๋Š” ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
WHERE SALARY LIKE '%2': 2๋กœ ๋๋‚˜๋Š” ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
https://www.tutorialspoint.com/sql/sql-like-clause.htm

โ“ Questions

  • express๋กœ ๋ฐฑ์—”๋“œ ๊ตฌ์ถ•ํ–ˆ์„ ๋•Œ๋Š” Many-to-one / One-to-many ์™€ ๊ฐ™์€ ๊ด€๊ณ„๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ์—ˆ๋Š”์ง€?

0๊ฐœ์˜ ๋Œ“๊ธ€