[ 8부 ] GraphQL, Typescript 및 React로 Twitter 복제본 만들기( 트윗에 좋아요 추가 )

안녕하세요 여러분 ;). 나는 당신이 잘 바랍니다.

참고로 저는 다음 챌린지를 시도합니다: Tweeter challenge

Db diagram

오늘 저는 트윗을 좋아할 가능성을 추가할 것을 제안합니다.

knex migrate:make create_likes_table -x ts


src/db/migrations/create_likes_table

import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTable('likes', (t) => {
    t.increments('id')
    t.integer('user_id').unsigned().notNullable()
    t.integer('tweet_id').unsigned().notNullable()

    t.unique(['user_id', 'tweet_id'])

    t.foreign('user_id').references('id').inTable('users').onDelete('CASCADE')
    t.foreign('tweet_id').references('id').inTable('tweets').onDelete('CASCADE')
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.raw('DROP TABLE likes CASCADE')
}




knex migrate:latest


트윗에 좋아요를 한 번만 표시할 수 있기 때문에 user_id 및 tweet_id 열과 관련된 고유 제약 조건을 추가합니다 ;).

src/resolvers/LikeResolver.ts

import { ApolloError } from 'apollo-server'
import {
  Arg,
  Authorized,
  Ctx,
  Int,
  Mutation,
  ObjectType,
  Resolver,
} from 'type-graphql'
import { MyContext } from '../types/types'

@Resolver()
class LikeResolver {
  @Mutation(() => String)
  @Authorized()
  async toggleLike(@Arg('tweet_id') tweet_id: number, @Ctx() ctx: MyContext) {
    const { db, userId } = ctx

    const [tweet] = await db('tweets').where('id', tweet_id)

    if (!tweet) {
      throw new ApolloError('Tweet not found')
    }

    const data = {
      user_id: userId,
      tweet_id: tweet_id,
    }

    try {
      const [alreadyLiked] = await db('likes').where(data)

      if (alreadyLiked) {
        // Delete the like and return
        await db('likes').where(data).del()
        return 'Like deleted'
      }

      await db('likes').insert(data)

      return 'Like added'
    } catch (e) {
      throw new ApolloError(e.message)
    }
  }
}
export default LikeResolver



"좋아요"추가 또는 제거를 관리하는 단일 메서드를 생성합니다. 그래서 나는 이미 "좋아요"가 있는지 확인하고 있다면 그것을 삭제합니다. 그렇지 않으면 추가합니다.

내 스키마 메서드에 LikeResolver도 추가해야 합니다.

src/server.ts

...
export const schema = async () => {
  return await buildSchema({
    resolvers: [AuthResolver, TweetResolver, LikeResolver],
    authChecker: authChecker,
  })
}


내 서버를 시작하면 모든 것이 제대로 작동합니다.


몇 가지 테스트를 작성해 보겠습니다.

src/tests/likes.test.ts

import db from '../db/connection'
import { generateToken } from '../utils/utils'
import { createLike, createTweet, createUser } from './helpers'
import { TOGGLE_LIKE } from './queries/likes.queries'
import { ADD_TWEET, FEED, DELETE_TWEET } from './queries/tweets.queries'
import { testClient } from './setup'

describe('Likes', () => {
  beforeEach(async () => {
    await db.migrate.rollback()
    await db.migrate.latest()
  })

  afterEach(async () => {
    await db.migrate.rollback()
  })

  it('should add a like', 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: TOGGLE_LIKE,
      variables: {
        tweet_id: tweet.id,
      },
    })

    const [like] = await db('likes').where({
      user_id: user.id,
      tweet_id: tweet.id,
    })

    expect(like).not.toBeUndefined()

    expect(res.data.toggleLike).toEqual('Like added')
    expect(res.errors).toBeUndefined()
  })

  it('should add delete a like', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)
    await createLike(user, tweet)

    const { mutate } = await testClient({
      req: {
        headers: {
          authorization: 'Bearer ' + generateToken(user),
        },
      },
    })

    const res = await mutate({
      mutation: TOGGLE_LIKE,
      variables: {
        tweet_id: tweet.id,
      },
    })

    const [deleted] = await db('likes').where({
      user_id: user.id,
      tweet_id: tweet.id,
    })

    expect(deleted).toBeUndefined()

    expect(res.data.toggleLike).toEqual('Like deleted')
    expect(res.errors).toBeUndefined()
  })

  it('should not authorized an anonymous user to like a tweet', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)

    const { mutate } = await testClient()

    const res = await mutate({
      mutation: TOGGLE_LIKE,
      variables: {
        tweet_id: tweet.id,
      },
    })

    const likes = await db('likes')
    expect(likes.length).toEqual(0)

    expect(res.errors![0].message).toEqual('Unauthorized')
  })

  it('should not authorized an anonymous user to delete a like', async () => {
    const user = await createUser()
    const tweet = await createTweet(user)
    const like = await createLike(user, tweet)

    const { mutate } = await testClient()

    const res = await mutate({
      mutation: TOGGLE_LIKE,
      variables: {
        tweet_id: tweet.id,
      },
    })

    const likes = await db('likes')
    expect(likes.length).toEqual(1)

    expect(res.errors![0].message).toEqual('Unauthorized')
  })
})





완료하기 전에 트윗의 좋아요 수를 확인하는 것이 좋습니다. 임의의 좋아요를 추가하기 위해 seed 파일을 수정했습니다. ;)에서 확인하겠습니다.

src/entities/Tweet.ts

@Field()
likesCount: number


좋아요에 @FieldResolver를 추가할 때 n+1 문제를 피하기 위해 "dataloader"가 필요합니다.

src/dataloaders/dataloaders.ts

import DataLoader from 'dataloader'
import db from '../db/connection'
import User from '../entities/User'

export const dataloaders = {
  userDataloader: new DataLoader<number, User, unknown>(async (ids) => {
    const users = await db('users').whereIn('id', ids)

    return ids.map((id) => users.find((u) => u.id === id))
  }),
  // Get the likesCount for each tweet
  likesCountDataloader: new DataLoader<number, any, unknown>(async (ids) => {
    const counts = await db('likes')
      .whereIn('tweet_id', ids)
      .count('tweet_id', { as: 'likesCount' })
      .select('tweet_id')
      .groupBy('tweet_id')

    return ids.map((id) => counts.find((c) => c.tweet_id === id))
  }),
}



이제 TweetResolver에 @FieldResolver를 추가할 수 있습니다.

src/resolvers/TweetResolver.ts

 @FieldResolver(() => Int)
  async likesCount(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
    const {
      dataloaders: { likesCountDataloader },
    } = ctx
    const count = await likesCountDataloader.load(tweet.id)
    return count?.likesCount || 0
  }




세 가지 SQL 쿼리가 있습니다. 하나는 트윗을 검색합니다. 연결된 사용자를 검색하는 또 다른 하나와 좋아요 수를 검색하는 마지막 항목.

그러나 문제가 있음을 알 수 있습니다. toggleLike 메서드를 여러 번 실행하고 피드 메서드를 새로고침하면 likesCount 속성이 업데이트되지 않는 것을 볼 수 있습니다. 이 문제를 방지하려면 "좋아요"를 추가하거나 제거할 때 캐시를 지워야 합니다.

데이터 로더 캐시 지우기



src/resolvers/LikeResolver.ts

@Mutation(() => String)
  @Authorized()
  async toggleLike(@Arg('tweet_id') tweet_id: number, @Ctx() ctx: MyContext) {
    const {
      db,
      userId,
      dataloaders: { likesCountDataloader }, // I get the dataloaders from the context
    } = ctx

    const [tweet] = await db('tweets').where('id', tweet_id)

    if (!tweet) {
      throw new ApolloError('Tweet not found')
    }

    const data = {
      user_id: userId,
      tweet_id: tweet_id,
    }

    try {
      const [alreadyLiked] = await db('likes').where(data)

      if (alreadyLiked) {
        // Delete the like and return
        await db('likes').where(data).del()

        likesCountDataloader.clear(tweet_id) // I clear the dataloader for this particular tweet_id

        return 'Like deleted'
      }

      await db('likes').insert(data)

      likesCountDataloader.clear(tweet_id) // I clear the dataloader for this particular tweet_id

      return 'Like added'
    } catch (e) {
      throw new ApolloError(e.message)
    }
  }


이제 예상대로 작동해야 합니다.

오늘은 여기까지입니다 ;).

안녕히 계세요! ;)

좋은 웹페이지 즐겨찾기