[ PART 13 ] Create a Twitter clone with GraphQL, Typescript, and React ( followers )
Hi everyone ;).
As a reminder, I'm doing this Tweeter challenge
Add the followers Table
knex migrate:make create_followers_table -x ts
src/db/migrations/create_followers_table
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')
}
knex migrate:latest
I will then create a Follower Resolver:
src/resolvers/FollowerResolver.ts
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
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
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
@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 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:
knex migrate:make add_bio_banner_users -x ts
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')
})
}
knex migrate:latest
Add them to the User entity
@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 profile. Let's just add an issue to not forget that :D.
I will stop here for today ;).
Bye and take care ;)!