Building a DevJoke Application with GraphQL

Update: On April 16th 2021, Slash GraphQL was officially renamed Dgraph Cloud. All other information below still applies.

Introduction

#DevJoke - is a place where you find the jokes for your geeky, developer brain. Like and share your favourite jokes with the world. Building the app over Slash GraphQL allowed us to focus on application logic rather than on managing the database.

The app takes its inspiration from Shruti Kapoor’s DevJoke and tries to provide a more engaging bundle of jokes to the developer community. We thank her for all the support and feedback provided.

This post provides an overview of the app, how we built it to provide authorization at the database level, and leveraged the power of DQL in GraphQL through custom resolvers. After reading this, you will be able to design an app, handle authentication/authorization with Auth0, and get some understanding for storing images.

Before we dive deeper into the technical details, let’s list the tech stack that we used:

  1. Frontend - React, Material UI
  2. Backend - Slash GraphQL as database, Auth0 for creating JWT and create user hook, AWS-S3 for storing images

Designing a Schema

In our app, anyone can see the jokes, search, sort, and share them. Once you are logged in, you can create jokes and like them. The content is moderated to provide you the best jokes in the feed and improved search. The moderators can approve, reject and edit description and tags for the newly created jokes and the jokes flagged by the community.

This leads to a very simple schema with 3 types: User, Post, and Tag.


type User {
    username: String! @id
    name: String
    posts: [Post] @hasInverse(field: createdby)
    likedPost: [Post]
    flaggedPost: [Post]
}
type Tag {
    name: String! @id @search(by: [fulltext])
    posts: [Post] @hasInverse(field: tags)
}
type Post {
    id: ID!
    text: String @search(by: [fulltext])
    tags: [Tag]
    createdby: User!
    timeStamp: DateTime @search
    isApproved: Boolean @search
    likes: [User] @hasInverse(field: likedPost)
    flags: [User] @hasInverse(field: flaggedPost)
    numFlags: Int @search
    img: String
}

All the posts created by a user is stored in List. It supports the relation with Post type through the @hasInverse directive on the field createdby. We are providing a full-text search over the post’s text/description.

Dgraph automatically generates a CRUD API from the types defined in schema. We wanted to query through the posts which have search text as well as any of the tags selected for search. Since, it is not directly possible through auto-generated CRUD API and requires unnecessary data to be fetched and client side logic applied over that. Hence, we leveraged the power of custom resolvers provide by Slash GraphQL and created a custom DQL query queryPostByTextAndTags.


type Query{
  queryPostByTextAndTags(tagString: String!, postTextString: String!): [Post] @custom(dql: """
  query q($tagString: string, $postTextString: string ){
      var(func: type(Tag)) @filter(anyoftext(Tag.name,$tagString)){
        Tag.posts{
          PostIds as uid
        }
      }
      queryPostByTextAndTags(func :uid(PostIds)) @filter(anyoftext(Post.text,$postTextString) and eq(Post.isApproved,true) and lt(Post.numFlags,2)){
        text: Post.text
        createdby: Post.createdby {
          username: User.username
        }
        ...
      }
    }
    """)
}

Now, if you run the following GraphQL query, it would fetch you the posts which have the text weird language as well as tagged as Java joke. Note that mapping between text and Post.text has to be created in the custom query through aliases.

query {
  queryPostByTextAndTags(tagString: "Java", postTextString:"weird language"){
    text
    createdby {
      username
    }
    ...
  }
}

Authentication and Authorization

In Slash GraphQL, you can specify the authorization rules for different types in your schema using @auth directive. We used this to enforce checks on mutations. For example, a user is authorized to create only her posts, and only admin can delete the posts.

type Post @auth(
  add: { rule: """
    query ($USER: String!) {
      queryPost {
        createdby(filter: { username: {eq: $USER} }){
          username
        }
      }
    } """
  }
  delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
){
    ...
}

Authentication in Slash GraphQL works using a signed JWT. DevJoke uses Auth0 for authentication. If you wish to clone and run the application, you would have to set up Auth0 for authentication.

You can set up Auth0 for DevJokes following these steps:

  1. Create a Single Page Application in Auth0 Dashboard
  2. Create “rule” in Auth0 dashboard to add claim to token with field as USER and ROLE (see this)
  3. Create “Add User” rule to Auth0 which creates a database entry for a user. (see this).

Since points 1 and 2 have been covered in the docs, we will discuss point 3 here. We will also shed some light on point 2 as it differs slightly due to the application’s nature.

Setting up Role Based Access Control

const moderators = ["[email protected]", "[email protected]"];
const role = moderators.includes(user.email) ? "ADMIN":"USER";
context.idToken[namespace] = { 'USER': user.email, 'ROLE': role };

The admin and user roles are used to distinguish between various CRUD operations. This essentially provides the authentication at the database level through custom claims to the token.

Creating Hooks for AddUser

Whenever a user logs in, the “Add User” rule makes a GraphQL mutation to the slash endpoint to add user data to the database. Auth0 provides an easy way to log in via different platforms. We are currently using Google and Github as preferred sign-up methods, these being the hotspot of the developer community.

type User @auth(
  add: {or: [{ rule: """
    query ($USER: String!){
      queryUser(filter: {username: {eq: $USER}}) {
        name
      }
    }
    """
    }
    { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
  ]}
  delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
){
    ...
}

Storing Images

Storing an image in a database is not recommended as these large blobs are never operated upon. These blobs are used as it is or replaced; they are never modified. This made us use the AWS-S3 bucket as a place to store the images and keep only the URL in the database (img field of Post type). We set up AWS; creating a S3 bucket, defining a lambda function and exposing it through API gateway. You can find the instruction for the same here.

All the Auth0 and AWS configuration related snippets are present in the Github repository here and here respectively. Don’t forget to update the credentials in the related file.

Having set up the Auth0 and AWS, we are ready to launch the application locally and post the jokes.

The code is up on Github.

Conclusion

In this blog, we introduced the #DevJoke app, discussed the schema and particularly the authorization rules, and how we leveraged the power of custom resolvers provided by Slash GraphQL. What next? Try deploying DevJoke using One-Click deploy on Slash GraphQL!