[ PART 7 ] Creating a Twitter clone with GraphQL, Typescript, and React ( add/delete a tweet )
Tags: javascript, showdev, career, challenge
Published: 2021-01-10 10:11:21

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

PS: I've got an error that I don't quite understand. Look at the section **Do you have an idea? ;)** on the delete tweet part ;).

## Adding a tweet

Hi everyone ;). Now let's look at how to insert a tweet.

*src/resolvers/TweetResolver*

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

    try {
      const [tweet] = await db('tweets')
        .insert({
          ...payload,
          user_id: userId,
        })
        .returning('*')

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

Nothing special here, don't forget that only an authenticated user can post a tweet and so we put the annotation **@Authorized**.

As for the **AddTweetPayload** class, here it is:

*src/dto/AddTweetPayload*

```typescript
import { IsNotEmpty, MinLength } from 'class-validator'
import { Field, InputType, Int } from 'type-graphql'

@InputType()
class AddTweetPayload {
  @Field()
  @IsNotEmpty()
  @MinLength(2)
  body: string

  @Field(() => Int, { nullable: true })
  parent_id?: number

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

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

export default AddTweetPayload

```

Only the body field is necessary since we have set default values for the other fields and the user will be retrieved directly via the context.

If I try the mutation, I get this: 
![Add tweet mutation playground result](https://dev-to-uploads.s3.amazonaws.com/i/grdpb16fdoraz46gljkn.png)

The only little problem here is that I'm going to have 3 SQL queries:
![Add tweet mutation sql queries](https://dev-to-uploads.s3.amazonaws.com/i/a54ieq0hds1osdzrwnke.png)

I get back my authenticated user in my **authChecker** method and then I retrieve him via the **userDataloader** that we set up in the previous part with the **@FieldResolver**. We could modify our **authChecker** function to use the **userDataloader** as well. On the other hand, we will have to be careful to clean our cache when the user is modified or deleted ( **userDataloader.clear(userId)** ). I put this as an example as we haven't encountered this problem yet. So my **authChecker** method would look like this:

*src/middlewares/authChecker.ts*

```typescript
export const authChecker: AuthChecker<MyContext, string> = async ({
  root,
  args,
  context,
  info,
}) => {
  const {
    db,
    req,
    dataloaders: { userDataloader }, // Get the dataloader from the context
  } = <MyContext>context

  try {
    const token = extractJwtToken(req)
    const {
      data: { id },
    }: any = jwt.verify(token, JWT_SECRET as string)
	
    // Modified part
    const user = await userDataloader.load(id)

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

    context.userId = user.id
    return true
  } catch (e) {
    throw e
  }
}
```

We're now going to write some tests to check what we've done ;). Note that I modified the entities **Tweet** and **User** to return a number ( for the id field ) instead of the type **ID** because it returned a String whereas I have integers in my case ;).

*src/tests/tweets.test.ts*

```typescript
test('it should insert a tweet', async () => {
    const user = await createUser()

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

    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: { body: 'First tweet' },
      },
    })

    const newTweet = await db('tweets')

    expect(newTweet.length).toEqual(1)

    expect(res.data.addTweet).not.toBeNull()
    expect(res.data.addTweet.body).toEqual('First tweet')
    expect(res.data.addTweet.user.id).toEqual(user.id)
  })

  test('it should not insert if the user is not authenticated', async () => {
    const { mutate } = await testClient()

    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: { body: 'First tweet' },
      },
    })

    const newTweet = await db('tweets')

    expect(newTweet.length).toEqual(0)

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

  test('it should not insert a tweet if the body is empty', async () => {
    const user = await createUser()

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

    const res = await mutate({
      mutation: ADD_TWEET,
      variables: {
        payload: { body: '' },
      },
    })

    const newTweet = await db('tweets')

    expect(newTweet.length).toEqual(0)

    expect(res.errors).not.toBeNull()
    expect(res.errors![0].message).toEqual('Argument Validation Error')
  })
```
![Add tweet tests result](https://dev-to-uploads.s3.amazonaws.com/i/nsbv399bz6mh7xb5wx4t.png)

## Deleting a tweet

*src/resolvers/TweetResolver*

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

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

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

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

I retrieve the tweet with the id AND with the connected user's id to be sure that only the author of the tweet can delete his tweets ;). I decided to return the number of rows affected by the deletion here.

Here are some tests to verify that the deletion is working properly:

### Do you have an idea? ;)

**I have a GraphQL error that I haven't resolved yet. *'Variable "$id" of type "Int!" used in position expecting type "Float!".'* The mutation wants me to pass a type *Float!* when I do have an *Int!* a priori. When I do a tweet.id *typeof* I have a type number. I will continue my investigations, but if you have an idea of how and why don't hesitate to enlighten me ;).**

Here is the mutation in the tests which is problematic:

*src/tests/queries/tweets.queries.ts*

```typescript
export const DELETE_TWEET = gql`
  mutation($id: Int!) { // I need to set the type to Float! to make it work
    deleteTweet(id: $id)
  }
`
```

Otherwise, here are the tests for deleting a tweet:

*src/tests/tweets.test.ts*

```typescript
it('should delete a user s tweet', async () => {
    const user = await createUser()
    const tweet = await createTweet(user, 'First tweet')

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

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

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

    expect(deletedTweet).toBeUndefined()

    expect(res.data.deleteTweet).toEqual(1)
  })

  it('should not delete a tweet that doesnt belong to the connected user', async () => {
    const user = await createUser()
    const another = await createUser('another', 'another@test.fr')
    const tweet = await createTweet(user, 'First tweet')

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

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

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

    expect(deletedTweet).not.toBeUndefined()
    expect(res.errors).not.toBeNull()
    expect(res.errors![0].message).toEqual('Tweet not found')
  })
```

Everything seems to work fine ;). See you in the next part!

Bye and take care! ;)