[ PART 13 ] Creating a Twitter clone with GraphQL, Typescript, and React ( followers )
Tags: showdev, javascript, challenge, graphql
Published: 2021-01-16 11:55:03

Hi everyone ;).

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

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

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

## Add the followers Table

```sh
knex migrate:make create_followers_table -x ts
```

*src/db/migrations/create_followers_table*

```typescript
import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTable('followers', (t) => {
    t.increments('id')
    t.integer('follower_id').notNullable()
    t.integer('following_id').notNullable()

    t.foreign('follower_id')
      .references('id')
      .inTable('users')
      .onDelete('CASCADE')
    t.foreign('following_id')
      .references('id')
      .inTable('users')
      .onDelete('CASCADE')
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.raw('DROP TABLE followers CASCADE')
}

```

```sh
knex migrate:latest
```



I will then create a Follower Resolver:

*src/resolvers/FollowerResolver.ts*

```typescript
import { ApolloError } from 'apollo-server'
import { Arg, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'
import { MyContext } from '../types/types'

@Resolver()
class FollowerResolver {
  @Mutation(() => String)
  @Authorized()
  async toggleFollow(
    @Arg('following_id') following_id: number,
    @Ctx() ctx: MyContext
  ) {
    const { db, userId } = ctx

    try {
      const userToFollow = await db('users').where('id', following_id)

      if (!userToFollow) {
        throw new ApolloError('User not found')
      }

      const [alreadyFollow] = await db('followers').where({
        follower_id: userId,
        following_id,
      })

      // Delete the follow
      if (alreadyFollow) {
        await db('followers')
          .where({
            follower_id: userId,
            following_id,
          })
          .del()

        return 'You are no longer following this user'
      }

      await db('followers').insert({
        follower_id: userId,
        following_id,
      })

      return 'User followed!'
    } catch (e) {
      console.log('e', e)
      throw e
    }
  }
}

export default FollowerResolver

```

We add the resolver to the server:

*src/server.ts*

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



Here are some tests that I wrote too:

*src/tests/followers.test.ts*

```typescript
import db from '../db/connection'
import { generateToken } from '../utils/utils'
import { createUser, followUser } from './helpers'
import { testClient } from './setup'
import { TOGGLE_FOLLOW } from './queries/followers.queries'

describe('Followers', () => {
  beforeEach(async () => {
    await db.migrate.rollback()
    await db.migrate.latest()
  })

  afterEach(async () => {
    await db.migrate.rollback()
  })

  it('should add a user to follow', async () => {
    const user = await createUser()
    const userToFollow = await createUser('new', 'new@test.fr')

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

    const res = await mutate({
      mutation: TOGGLE_FOLLOW,
      variables: {
        following_id: userToFollow.id,
      },
    })

    const [follower] = await db('followers').where({
      follower_id: user.id,
      following_id: userToFollow.id,
    })

    expect(follower).not.toBeUndefined()

    expect(res.data.toggleFollow).toEqual('User followed!')
  })

  it('should delete a user that a user follow', async () => {
    const user = await createUser()
    const userToFollow = await createUser('new', 'new@test.fr')
    await followUser(user, userToFollow)

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

    const res = await mutate({
      mutation: TOGGLE_FOLLOW,
      variables: {
        following_id: userToFollow.id,
      },
    })

    const [follower] = await db('followers').where({
      follower_id: user.id,
      following_id: userToFollow.id,
    })

    expect(follower).toBeUndefined()

    expect(res.data.toggleFollow).toEqual(
      'You are no longer following this user'
    )
  })
})

```



And that's pretty much it. For now, I don't have any queries for the followers because I don't know yet how I will handle that in the Frontend. 



Now that we can have users that we follow, let's modify our **feed** query.

*src/resolvers/TweetResolver.ts*

```typescript
@Query(() => [Tweet])
  @Authorized()
  async feed(@Ctx() ctx: MyContext) {
    const { db, userId } = ctx

    const followedUsers = await db('followers')
      .where({
        follower_id: userId,
      })
      .pluck('following_id')

    const tweets = await db('tweets')
      .whereIn('user_id', followedUsers)
      .orWhere('user_id', userId)
      .orderBy('id', 'desc')
      .limit(20)

    return tweets
  }
```

As I previously said, I will only allow connected users to access the application. Therefore, I add the **@Authorized()** annotation to the query. Then I just get the users followed by the connected user and I fetch the tweets from those users and from the connected user. I also fix the tests by adding the **authorization header** when needed.



I noticed too that I should have two more properties on the User according to the challenge details ;). I need the Bio and also a "banner image". Let's do that:

```sh
knex migrate:make add_bio_banner_users -x ts
```

```typescript
import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.alterTable('users', (t) => {
    t.string('bio').nullable()
    t.string('banner').nullable()
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.schema.alterTable('users', (t) => {
    t.dropColumn('bio')
    t.dropColumn('banner')
  })
}

```

```sh
knex migrate:latest
```

Add them to the **User** entity

```typescript
  @Field({ nullable: true })
  bio?: string

  @Field({ nullable: true })
  banner?: string
```

Also, I expose the user's email to everyone. We will need to change that later as we only need to show the user's email on its own profile. Let's just add an issue to not forget that :D.



I will stop here for today ;).

Bye and take care ;)!