[ PART 9 ] Create a Twitter clone with GraphQL, Typescript, and React ( isLiked? )
Hi everyone! ;)
As a reminder, I try to do this challenge mostly to learn about GraphQL ;): Tweeter challenge
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
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 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
}
}
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 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
}
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))
}),
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 ;).
And without a connected user
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 ;).