[ PART 12 ] Creating a Twitter clone with GraphQL, Typescript, and React ( CommentsCount, retweetsCount )
Tags: showdev, graphql, javascript, career
Published: 2021-01-15 11:28:20

Hi everyone ;).

As a reminder, I'm doing this challenge: [Tweeter challenge](https://devchallenges.io/challenges/rleoQc34THclWx1cFFKH)

[Github repository](https://github.com/ips-coding-challenge/challenge_twitter)

[Db diagram](https://dbdiagram.io/d/5f75c0243a78976d7b75f7be)



In [Part 10](https://dev.to/ipscodingchallenge/part-10-creating-a-twitter-clone-with-graphql-typescript-and-react-comment-retweet-kjj), I had some issues with validating some field conditionally. To fix the issue, I had to set this option to the server file:

*src/server.ts*

```typescript
export const schema = async () => {
  return await buildSchema({
    resolvers: [AuthResolver, TweetResolver, LikeResolver],
    authChecker: authChecker,
    validate: {
      skipMissingProperties: false, // This one
    },
  })
}
```

However, I found that there was another option, so I changed what I did previously. It's not a big thing, but I didn't like to change the option globally. Let's see what I modified:

*src/entities/AddTweetPayload*

```typescript
import {
  IsDefined,
  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)
  @IsDefined()
  parent_id?: number

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

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

export default AddTweetPayload

```

According to the documentation, the **IsDefined()** annotation ignore the property *skipMissingProperties*. That exactly what I needed ;). I rewrote some tests too because the error was not the same. I can finally remove the option from my server file:

*src/server.ts*

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



## CommentsCount && RetweetsCount

As we already add the **likesCount**, it will be easy to do the same for the comments and retweets.

*src/entities/Tweet.ts*

```typescript
@Field()
retweetsCount: number

@Field()
commentsCount: number
```

*src/dataloaders/dataloaders.ts*

```typescript
retweetsCountDataloader: new DataLoader<number, any, unknown>(async (ids) => {
    const counts = await db('tweets')
    .whereIn('parent_id', ids)
    .andWhere('type', TweetTypeEnum.RETWEET)
    .count('parent_id', { as: 'retweetsCount' })
    .select('parent_id')
    .groupBy('parent_id')

    return ids.map((id) => counts.find((c) => c.parent_id === id))
}),
commentsCountDataloader: new DataLoader<number, any, unknown>(async (ids) => {
    const counts = await db('tweets')
    .whereIn('parent_id', ids)
    .andWhere('type', TweetTypeEnum.COMMENT)
    .count('parent_id', { as: 'commentsCount' })
    .select('parent_id')
    .groupBy('parent_id')

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

*src/resolvers/TweetResolver.ts*

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

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

I also have to clear the cache if a comment/retweet is added or if a tweet is deleted.

*src/resolvers/TweetResolver.ts*

```typescript
@Mutation(() => Tweet)
  @Authorized()
  async addTweet(
    @Arg('payload') payload: AddTweetPayload,
    @Ctx() ctx: MyContext
  ) {
    const {
      db,
      userId,
      dataloaders: { retweetsCountDataloader, commentsCountDataloader },
    } = ctx
    const { body, type, parent_id } = payload

    // 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 (type === TweetTypeEnum.RETWEET && parent_id) {
      const [alreadyRetweeted] = await db('tweets').where({
        parent_id: parent_id,
        type: TweetTypeEnum.RETWEET,
        user_id: userId,
      })

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

    if (parent_id) {
      const [tweetExists] = await db('tweets').where('id', parent_id)
      if (!tweetExists) {
        throw new ApolloError('Tweet not found')
      }
    }

    try {
      const [tweet] = await db('tweets')
        .insert({
          ...payload,
          user_id: userId,
        })
        .returning('*')
	  
      // Needed to clear the cache
      if (type === TweetTypeEnum.RETWEET) {
        retweetsCountDataloader.clear(tweet.parent_id)
      } else if (type === TweetTypeEnum.COMMENT) {
        commentsCountDataloader.clear(tweet.parent_id)
      }

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

  @Mutation(() => Int)
  @Authorized()
  async deleteTweet(@Arg('id') id: number, @Ctx() ctx: MyContext) {
    const {
      db,
      userId,
      dataloaders: { retweetsCountDataloader, commentsCountDataloader },
    } = ctx

    try {
      const [tweet] = await db('tweets').where({
        id,
        user_id: userId,
      })

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

      // Needed to clear the cache  
      if (tweet.parent_id) {
        if (tweet.type === TweetTypeEnum.COMMENT) {
          commentsCountDataloader.clear(tweet.parent_id)
        } else if (tweet.type === TweetTypeEnum.RETWEET) {
          retweetsCountDataloader.clear(tweet.parent_id)
        }
      }

      // Return the number of affected rows
      return await db('tweets').where({ id, user_id: userId }).del()
    } catch (e) {
      throw new ApolloError(e.message)
    }
  }
```

It should work as expected ;)

## Comment Query

I will add another query to fetch the comments for a tweet.

*src/resolvers/TweetResolver.ts*

```typescript
@Query(() => [Tweet])
  async comments(@Arg('parent_id') parent_id: number, @Ctx() ctx: MyContext) {
    const { db } = ctx

    const comments = await db('tweets').where({
      parent_id,
      type: TweetTypeEnum.COMMENT,
    })

    return comments
  }
```

Nothing particular here. We should also need a way to retrieve the parent of a comment as a *Tweet*. 

*src/entities/Tweet.ts*

```typescript
@Field(() => Tweet, { nullable: true })
parent?: Tweet
```

And we will add a **dataloader** too:

*src/dataloaders/dataloaders*

```typescript
parentTweetDataloader: new DataLoader<number, Tweet, unknown>(async (ids) => {
    const parents = await db('tweets').whereIn('id', ids)

    return ids.map((id) => parents.find((p) => p.id === id))
  }),
```

We just need to add the **@FieldResolver**

*src/resolvers/TweetResolver.ts*

```typescript
@FieldResolver(() => Tweet, { nullable: true })
  async parent(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
    const {
      dataloaders: { parentTweetDataloader },
    } = ctx

    if (!tweet.parent_id) return null

    return await parentTweetDataloader.load(tweet.parent_id!)
  }
```

![Feed Query result](https://dev-to-uploads.s3.amazonaws.com/i/a7ijcm6qzzjtztd1pdri.png)

It will be all for today.

Ciao!

Have a nice day ;)