[ PART 10 ] Creating a Twitter clone with GraphQL, Typescript, and React ( comment & retweet )
Tags: showdev, javascript, career, graphql
Published: 2021-01-13 10:50:46

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)

We already have the possibility to add comment and retweet as we have a **parent_id** field in our **tweets** table as well as a **type** field of "tweet | retweet | comment". However, I just noticed ( from using twitter :D ) that we should not have the possibility to retweet multiple times the same tweet :D. So maybe it will be a good idea to check that in our **addTweet** method:

First of all, I added two enum Classes to match what I've done in the database schema:

*src/entities/Tweet.ts*

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

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

Then I will complete the validation rules when we add a tweet. After some struggle with the @ValidateIf() validator, I found out that was because with **Typegraphql**, the option **skipMissingProperties** is set to false. For now, let's change it to true to make my validation rules work.

*src/server.ts*

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

```

I could do it differently, for example, by forcing to have the **parent_id** and the **type** always present in the **AddTweetPayload**. But for now, let's do it this way. I can change it later if I have some issues. I will write some tests anyway to help to refactor if necessary ;).

So, let's take a look at the **AddTweetPayload** now:

*src/dto/AddTweetPayload.ts*

```typescript
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

```

If a *type* is sent, it should have a **parent_id** meaning that it's a *retweet* or a *comment*. In the same way, if I have a **parent_id** in the payload, the type should be either "comment" or "retweet". And to avoid to retweet a tweet that we have already retweeted, I will check that directly in the resolver. We can also check that the Tweet with the id of **parent_id** exists. 

*src/resolvers/TweetResolver.ts*

```typescript
@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)
    }
  }
```

Let's write some tests to see if I didn't break anything ;). As it's my first time using the **class-validator** library, I decided to write more tests to verify the different scenarios that could go wrong ;).

*src/tests/tweets.test.ts*

```typescript
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')
  })
```
![Tests results for retweets and comments](https://dev-to-uploads.s3.amazonaws.com/i/d7zjyi2d69doh3g20jvt.png)

Everything is green ;). Let's move to the next part. We should talk a little about **Github Workflows**.

See you in the next part ;).

Take care ;).