avatar
Published on

NestJS 프레임워크 - Entity & Resolver

Authors
  • avatar
    Name
    Haneul
    Twitter

NestJS 프레임워크 - Entity & Resolver

Entity Setting (relation)

eager

eager 데코레이터가 설정되어 있으면, N:1로 관계가 설정된 podcast의 데이터를 따로 설정 없이(이를테면, find 함수의 relation 사용)
db에서 쉽게 가져올 수 있습니다.

import { ObjectType, Field } from '@nestjs/graphql'
import { IsString } from 'class-validator'
import { Column, Entity, ManyToOne } from 'typeorm'
import { CoreEntity } from './core.entity'
import { Podcast } from './podcast.entity'

@Entity()
@ObjectType()
export class Episode extends CoreEntity {
  @Column()
  @Field((type) => String)
  @IsString()
  title: string

  @Column()
  @Field((type) => String)
  @IsString()
  category: string

  @ManyToOne(() => Podcast, (podcast) => podcast.episodes, {
    onDelete: 'CASCADE',
    eager: true,
  })
  @Field((type) => Podcast)
  podcast: Podcast
}

review는 작성자(User)와 대상(Podcast)가 필요하고
각각 리뷰와 1:N 관계가 필요하므로 위와 같이 entity를 작성할 수 있습니다.

@OneToMany(() => Episode, (episode) => episode.podcast)
  @Field((type) => [Episode])
  episodes: Episode[];

  @OneToMany(() => Review, (review) => review.podcast)
  @Field((type) => [Review])
  reviews: Review[];

podcast에서 episode와 review 1:N 관계 설정이 필요

@OneToMany(() => Review, (review) => review.creator, { eager: true })
  @Field((type) => [Review])
  reviews: Review[];

  @ManyToMany(() => Episode, { eager: true })
  @Field((type) => [Episode])
  @JoinTable()
  playedEpisodes: Episode[];

  @ManyToMany(() => Podcast, { eager: true })
  @Field(() => [Podcast])
  @JoinTable()
  subsriptions: Podcast[];

review, markEpisodeAsPlayed, subscribeToPodcast 구현을 위한 relation


Resolver

take, skip을 이용하여 pagination을 구현한 부분이 보이실 겁니다.
또한 주의하셔야할 부분이 하나 있는데, where 옵션을 보시면 title: Like를 이용한 것이 보이실 건데,
sqlite에는 case insensitive한 ILIKE가 없습니다.
그래서 typeorm의 Like를 사용하셔도 case insensitive하게 find를 수행할 수 있으며
또는, 위 코드의 주석문으로 처리한 **Raw(sql 문법사용할 수 있는 유틸함수)**를 이용하시면 됩니다.

async searchPodcasts({
    titleQuery,
    page
  }: SearchPodcastsInput): Promise<SearchPodcastsOutput> {
    try {
      const [podcasts, totalCount] = await this.podcastRepository.findAndCount({
        // where: { title: Raw((title) => `${title} LIKE ${titleQuery}`) },
        where: { title: Like(`%${titleQuery}%`) },
        take: 50,
        skip: (page - 1) * 50
      });
      if (!podcasts) {
        return { ok: false, error: "Could not find podcasts" };
      }
      return {
        ok: true,
        podcasts,
        totalCount,
        totalPages: Math.ceil(totalCount / 50)
      };
    } catch (err) {
      console.log(err);
      return this.InternalServerErrorOutput;
    }
  }

createReview mutation은 episode를 podcast에 추가할 때와 똑같이 구현해주면 됩니다.

async createReview(
    creator: User,
    { title, text, podcastId }: CreateReviewInput
  ): Promise<CreateReviewOutput> {
    try {
      const { ok, error: podcastFindErr, podcast } = await this.getPodcast(
        podcastId
      );
      if (!ok || podcastFindErr) {
        return { ok: false, error: podcastFindErr };
      }
      const review = this.reviewRepository.create({ title, text });
      review.podcast = podcast;
      review.creator = creator;
      const { id } = await this.reviewRepository.save(review);
      return { ok: true, id };
    } catch {
      return this.InternalServerErrorOutput;
    }
  }

toggleSubscribe으로 subscribe을 toggle할 수 있는 방식입니다.
이미 구독이 있으면 구독에서 삭제하는 부분은 filter 함수를 이용하여 배열에서 삭제하도록 구현되어 있습니다.
구독에 포함시키는 부분은 spread operator를 이용하여 구현되어 있습니다.

some
filter
spread operator

async toggleSubscribe(
    user: User,
    { podcastId }: ToggleSubscribeInput
  ): Promise<ToggleSubscribeOutput> {
    try {
      const podcast = await this.podcasts.findOne({ id: podcastId });
      if (!podcast) {
        return { ok: false, error: "Podcast not found" };
      }
      if (user.subsriptions.some((sub) => sub.id === podcast.id)) {
        user.subsriptions = user.subsriptions.filter(
          (sub) => sub.id !== podcast.id
        );
      } else {
        console.log("foo");
        user.subsriptions = [...user.subsriptions, podcast];
      }
      await this.users.save(user);
      return { ok: true };
    } catch {
      return this.InternalServerErrorOutput;
    }
  }

playedEpisodes는 entity에 episode와 1:n 관계로 묶인 episode 배열입니다.
해당 episode를 episodesEpisodes에 추가한 것입니다.

아래 코드와 달리 해당 에피소드를 이미 시청한 경우에는 추가시키지 않는 로직을 만드는 것도 괜찮아 보입니다.
includes나, every 등을 이용하면 구현할 수 있습니다.

includes
every

async markEpisodeAsPlayed(
    user: User,
    { id: episodeId }: MarkEpisodeAsPlayedInput
  ): Promise<MarkEpisodeAsPlayedOutput> {
    try {
      const episode = await this.episodes.findOne({ id: episodeId });
      if (!episode) {
        return { ok: false, error: "Episode not found" };
      }
      user.playedEpisodes = [...user.playedEpisodes, episode];
      await this.users.save(user);
      return { ok: true };
    } catch {
      return this.InternalServerErrorOutput;
    }
  }

참고 : nestJS 공식문서