[ PART 10 ] GraphQL, Typescript 및 React를 사용하여 Twitter 복제본 만들기( 댓글 및 리트윗 )

안녕하세요 여러분 ;).

참고로 저는 이 챌린지를 하고 있습니다 ;): Tweeter challenge

Db diagram

트윗 테이블에 parent_id 필드와 "tweet | retweet | comment"유형 필드가 있으므로 댓글과 리트윗을 추가할 가능성이 이미 있습니다. 그러나, 나는 (트위터를 사용하면서 :D) 우리가 같은 트윗을 여러 번 리트윗할 가능성이 없어야 한다는 것을 알아차렸습니다 :D. 따라서 addTweet 메소드에서 확인하는 것이 좋습니다.

우선 데이터베이스 스키마에서 수행한 작업과 일치하도록 두 개의 enum 클래스를 추가했습니다.

src/entities/Tweet.ts

export enum TweetTypeEnum {
  TWEET = 'tweet',
  RETWEET = 'retweet',
  COMMENT = 'comment',
}

export enum TweetVisibilityEnum {
  PUBLIC = 'public',
  FOLLOWERS = 'followers',
}


그런 다음 트윗을 추가할 때 유효성 검사 규칙을 완료합니다. @ValidateIf() 유효성 검사기와 약간의 어려움을 겪은 후 Typegraphql에서 skipMissingProperties 옵션이 false로 설정되어 있다는 사실을 알게 되었습니다. 지금은 유효성 검사 규칙이 작동하도록 true로 변경하겠습니다.

src/server.ts

export const schema = async () => {
  return await buildSchema({
    resolvers: [AuthResolver, TweetResolver, LikeResolver],
    authChecker: authChecker,
    validate: {
      skipMissingProperties: false, // set false instead of true
    },
  })
}



예를 들어, addTweetPayload에 항상 존재하는 parent_id와 유형을 갖도록 강제함으로써 다르게 할 수 있습니다. 하지만 지금은 이렇게 합시다. 나중에 문제가 있으면 변경할 수 있습니다. 어쨌든 필요한 경우 리팩터링하는 데 도움이 되도록 몇 가지 테스트를 작성할 것입니다 ;).

이제 AddTweetPayload를 살펴보겠습니다.

src/dto/AddTweetPayload.ts

import { IsIn, IsNotEmpty, MinLength, ValidateIf } from 'class-validator'
import { Field, InputType, Int } from 'type-graphql'
import { TweetTypeEnum } from '../entities/Tweet'

@InputType()
class AddTweetPayload {
  @Field()
  @IsNotEmpty()
  @MinLength(2)
  body: string

  @Field(() => Int, { nullable: true })
  @ValidateIf((o) => o.type !== undefined)
  @IsNotEmpty()
  parent_id?: number

  @Field(() => String, { nullable: true })
  @ValidateIf((o) => o.parent_id !== undefined)
  @IsIn([TweetTypeEnum.COMMENT, TweetTypeEnum.RETWEET])
  type?: TweetTypeEnum

  @Field(() => String, { nullable: true })
  visibility?: string
}

export default AddTweetPayload



유형이 전송되면 리트윗 또는 댓글임을 의미하는 parent_id가 있어야 합니다. 같은 방식으로 페이로드에 parent_id가 있는 경우 유형은 "댓글"또는 "리트윗"이어야 합니다. 그리고 우리가 이미 리트윗한 트윗을 리트윗하지 않도록 리졸버에서 직접 확인하겠습니다. id가 parent_id인 트윗이 존재하는지 확인할 수도 있습니다.

src/resolvers/TweetResolver.ts

@Mutation(() => Tweet)
  @Authorized()
  async addTweet(
    @Arg('payload') payload: AddTweetPayload,
    @Ctx() ctx: MyContext
  ) {
    const { db, userId } = ctx

    // Maybe I should add a mutation to handle the retweet?
    // For the comment, we can comment as much as we want so I could
    // still add the comment here.
    // Feel free to share your opinion ;)
    if (payload.type === TweetTypeEnum.RETWEET && payload.parent_id) {
      const [alreadyRetweeted] = await db('tweets').where({
        parent_id: payload.parent_id,
        type: TweetTypeEnum.RETWEET,
        user_id: userId,
      })

      if (alreadyRetweeted) {
        throw new ApolloError('You already retweeted that tweet')
      }
    }

    try {
      const [tweet] = await db('tweets')
        .insert({
          ...payload,
          user_id: userId,
        })
        .returning('*')

      return tweet
    } catch (e) {
      throw new ApolloError(e.message)
    }
  }


내가 아무것도 깨지 않았는지 확인하기 위해 몇 가지 테스트를 작성해 봅시다 ;). class-validator 라이브러리를 처음 사용하기 때문에 잘못될 수 있는 다양한 시나리오를 확인하기 위해 더 많은 테스트를 작성하기로 결정했습니다 ;).

src/tests/tweets.test.ts

it('should insert a comment', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          type: 'comment',
          parent_id: tweet.id,
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(2)

    expect(res.data.addTweet.body).toEqual('Bouh')
    expect(res.data.addTweet.type).toEqual('comment')
    expect(res.data.addTweet.parent_id).toEqual(tweet.id)
    expect(res.errors).toBeUndefined()
  })
  it('should insert a retweet', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          type: 'retweet',
          parent_id: tweet.id,
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(2)

    expect(res.data.addTweet.body).toEqual('Bouh')
    expect(res.data.addTweet.type).toEqual('retweet')
    expect(res.data.addTweet.parent_id).toEqual(tweet.id)
    expect(res.errors).toBeUndefined()
  })
  it('should not insert a comment if the type is provided but the parent_id is not provided', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          type: 'comment',
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(1)

    expect(res.errors).not.toBeUndefined()
    const {
      extensions: {
        exception: { validationErrors },
      },
    }: any = res.errors![0]

    expect((validationErrors[0] as ValidationError).constraints).toEqual({
      isNotEmpty: 'parent_id should not be empty',
    })
  })
  it('should not insert a comment if the parent_id is provided but the type is not provided', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          parent_id: tweet.id,
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(1)

    expect(res.errors).not.toBeUndefined()
    const {
      extensions: {
        exception: { validationErrors },
      },
    }: any = res.errors![0]

    expect((validationErrors[0] as ValidationError).constraints).toEqual({
      isIn: 'type must be one of the following values: comment,retweet',
    })
  })
  it('should not insert a retweet if the type is provided but not the parent_id', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          type: 'retweet',
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(1)

    expect(res.errors).not.toBeUndefined()
    const {
      extensions: {
        exception: { validationErrors },
      },
    }: any = res.errors![0]

    expect((validationErrors[0] as ValidationError).constraints).toEqual({
      isNotEmpty: 'parent_id should not be empty',
    })
  })
  it('should not insert a retweet if the parent_id is provided but not the type', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          parent_id: tweet.id,
        },
      },
    })

    const tweets = await db('tweets')

    expect(tweets.length).toEqual(1)

    expect(res.errors).not.toBeUndefined()
    const {
      extensions: {
        exception: { validationErrors },
      },
    }: any = res.errors![0]

    expect((validationErrors[0] as ValidationError).constraints).toEqual({
      isIn: 'type must be one of the following values: comment,retweet',
    })
  })
  it('should not insert a retweet if the user already retweeted the tweet', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)
    const retweet = await createTweet(
      user,
      'test',
      'retweet',
      'public',
      tweet.id
    )

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })
    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: {
          body: 'Bouh',
          type: 'retweet',
          parent_id: tweet.id,
        },
      },
    })

    expect(res.errors).not.toBeUndefined()
    expect(res.errors![0].message).toEqual('You already retweeted that tweet')
  })




모든 것이 녹색입니다;). 다음 부분으로 넘어가겠습니다. Github Workflows에 대해 조금 이야기해야 합니다.

다음 편에서 만나요 ;).

잘 지내세요 ;).

좋은 웹페이지 즐겨찾기