DEV Community

loading...
Cover image for [ PART 9 ] Creating a Twitter clone with GraphQL, Typescript, and React ( isLiked? )

[ PART 9 ] Creating a Twitter clone with GraphQL, Typescript, and React ( isLiked? )

ipscodingchallenge profile image ips-coding-challenge ・3 min read

Hi everyone! ;)

As a reminder, I try to do this challenge mostly to learn about GraphQL ;): Tweeter challenge

Github repository

Db diagram

In this post, we will see how to check if a tweet in our feed is already liked by the connected user. I had some "issues" while implementing this feature and even if it works, I wonder if there are better options to achieve the same result. Feel free to share how I could have done it if you know better ways.

First of all, let's add the isLiked field to our Tweet entity:

@Field()
isLiked: boolean
Enter fullscreen mode Exit fullscreen mode

I know I will need to create a dataloader but in this case, I will have to know about the connected user to check if the user liked the tweet. If I need the user, it means that I also need to add the @Authorized() annotation to the @FieldResolver(). At first, when I started this application, I wanted that only connected users should be able to access the tweets.

I'd stick with that idea, but I still wanted to see how I could deal with the fact that some properties shouldn't necessarily return an authentication error. This is the case for the isLiked property I think. When a user is connected, you have to check if the user has already liked this tweet but if I don't have a user, I just need to return false. But if I pass the annotation @Authorized() to my @FieldResolver(), it will throw an error. Fortunately, our authChecker method allows us to pass a second parameter called role. So here's what the new version of my authChecker will look like:

src/middleware/authChecker.ts

import { AuthChecker } from 'type-graphql'
import { MyContext } from '../types/types'
import { extractJwtToken } from '../utils/utils'
import { verify } from 'jsonwebtoken'
import { JWT_SECRET } from '../config/config'
import { AuthenticationError } from 'apollo-server'

export const authChecker: AuthChecker<MyContext, string> = async (
  { root, args, context, info },
  roles
) => {
  const {
    db,
    req,
    dataloaders: { userDataloader },
  } = <MyContext>context

  try {
    const token = extractJwtToken(req)
    const {
      data: { id },
    }: any = verify(token, JWT_SECRET as string)

    const user = await userDataloader.load(id)

    if (!user) {
      throw new AuthenticationError('User not found')
    }

    context.userId = user.id
    return true
  } catch (e) {
    if (roles.includes('ANONYMOUS')) {
      context.userId = null
      return true
    }
    throw e
  }
}

Enter fullscreen mode Exit fullscreen mode

I do a try/catch to avoid throwing the error if I allow the role "ANONYMOUS". For the moment, the only problem I see is that a "TokenExpired" error should trigger the error in order to be able to catch that in the Frontend to do what's appropriate. It should be enough to check the error type to handle this case ;).

So here's what the @FieldResolver() and the dataloader look like:

src/resolvers/TweetResolver.ts

@FieldResolver(() => Boolean)
@Authorized('ANONYMOUS')
async isLiked(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
    const {
        userId,
        dataloaders: { isLikedDataloader },
    } = ctx

    if (!userId) return false

    const isLiked = await isLikedDataloader.load({
        tweet_id: tweet.id,
        user_id: userId,
    })

    return isLiked !== undefined
}
Enter fullscreen mode Exit fullscreen mode

src/dataloaders/dataloaders.ts

isLikedDataloader: new DataLoader<any, any, unknown>(async (keys) => {
    const tweetIds = keys.map((k: any) => k.tweet_id)
    const userId = keys[0].user_id

    const likes = await db('likes')
      .whereIn('tweet_id', tweetIds)
      .andWhere('user_id', userId)
    return tweetIds.map((id) => likes.find((l) => l.tweet_id === id))
  }),
Enter fullscreen mode Exit fullscreen mode

As you can see, I pass an object for the keys of the "dataloader" since I need the user_id. Also, in the "authChecker" method, I set the userId to null if I was in "ANONYMOUS" mode. So, if I don't have a user logged in, I return false directly. Otherwise, I make my little query in the "dataloader" to be able to retrieve what I need ;).

Feed query with isLiked property and user connected

And without a connected user
Feed query with isLiked property and no user connected

This is how I handled this "problem". I'm sure there are better/scalable ways and I started to read about some possibilities. But for now, the idea is to resolve problems that I encountered, and not to overshadow Twitter :D.

Have a nice day and see you in the next part ;).

Discussion (2)

Collapse
banji220 profile image
Banji

Cool
Keep it Up
Such a good project boy
Happy Coding Man :)

Collapse
ipscodingchallenge profile image
Forem Open with the Forem app