[001.3] Introduction to the forum Data Model

An overview of the data model we’ll be using, and an exploration of it via GraphQL Playground

Subscribe now

Introduction to the forum Data Model [01.22.2019]

Today we’ll be building a forum, so we’ll need to model one in our data layer and inside of our GraphQL Schema. Then we can use GraphQL Playground to explore our data model via GraphQL. We will be implementing this model and schema on the backend in both our Elixir and JavaScript applications.

To better understand our data model, we are going to point GraphQL Playground at an implementation of the backend. This will allow us to both read the documentation as well as perform queries and mutations.

Our basic data model is:

  • A Forum has Categories.
  • A Category has Threads.
  • A Thread belongs to both a Category and a User.
  • A Thread has many Posts.
  • A Post belongs to both a Thread and a User.

Here we can see all the schema’s types, mutations, and queries. For example, if we click on a Category, we can see the fields it contains. These fields include its relationships to other types in the system. Clicking on a Post, we can see it has a Thread and a User, and we can click on each of these types and read their documentation as well. Pretty neat!

Let's look at a visualization of our schema using GraphQL Voyager. We use our full introspection query to initialize the visualization. We can walk through each query in the schema and see its return type and that type’s fields, in a Data Flow Diagram (DFD). That's cool!

User

Let's start by talking about our User. Users will need to authenticate with our API to create threads and posts. We authorize users by providing a mutation that takes an email and password as arguments, and returns a Bearer Token. The client then includes this token in the Authorization header of subsequent requests, and the backend uses the token to identify the authorized user.

Our User type has id, name, avatarUrl and username fields.

type User {
  id: ID!
  name: String!
  username: String!
  avatarUrl: String!
  posts: [Post!]!
  insertedAt: DateTime!
  updatedAt: DateTime!
}

To create a user, we will need to pass four arguments to our mutation: name, username, email and password.

mutation createUser{
  createUser(name:"Josh", username: "josh", email: "email@example.com", password: "password"){
    id
    insertedAt
  }
}

That creates a user! Now, we can authenticate the user, get the token, and use that in the authorization header.

mutation authentication{
  authenticate(email: "email@example.com", password: "password")
}
{
  "data": {
    "authenticate": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjNTU4ODYzMC1kZjMwLTRlNDctODUxYS05Nzg0OTc5ODQ3OGUiLCJpYXQiOjE1Mzk4NjYyNjYsImV4cCI6MTU0MDQ3MTA2Nn0.RRkRf70bWktTo0Iav__bbCEeL5JCBftqVmbIcFHY1iE"
  }
}

On GraphQL Playground we can set the HTTP Headers. Let's use this token in our authorization header:

{
  "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjNTU4ODYzMC1kZjMwLTRlNDctODUxYS05Nzg0OTc5ODQ3OGUiLCJpYXQiOjE1Mzk4NjYyNjYsImV4cCI6MTU0MDQ3MTA2Nn0.RRkRf70bWktTo0Iav__bbCEeL5JCBftqVmbIcFHY1iE"
}

With this header in place, our backend can determine which authorized user is making the request. We can then require a user when we create posts or threads, and associate that user with them. Later we’ll walk through the process of how the backend identifies the authorized user based on this token, but from a client’s point of view it’s opaque.

Categories

Let's create a Category using the createCategory mutation. We must be logged in to create a category.

createCategory takes a single argument, title. We’ll create a Category titled Category Title 1. The mutation returns a Category, and we’ll request the title and id fields from it.

mutation createCategory1{
  createCategory(title:"Category Title 1"){
    title
    id
  }
}

Here’s the Category type for reference. It has the fields id, title, slug, insertedAt, and updatedAt. The id type is a UUID.

type Category {
  id: ID!
  title: String!
  slug: String!
  threads: [Thread!]!
  insertedAt: DateTime!
  updatedAt: DateTime!
}

We paginate the categories, so there’s a PaginatedCategories type. The paginated categories type has an entries field which is a list of categories. Additionally, it has page, perPage, totalEntries and totalPages fields to use for pagination purposes.

type PaginatedCategories {
  entries: [Category]
  page: Int!
  perPage: Int!
  totalEntries: Int!
  totalPages: Int!
}```

Now, let's remove the authorization header and try to create a category.

```javascript
{
  "data": {
    "createCategory": null
  },
  "errors": [
    {
      "message": "You must be logged in to perform this action.",
      "statusCode": 401
    }
  ]
}

We got a message that we must be logged in and our status code is 401. We’ll add the header back.

Let's get categories. As categories are paginated, we need to pass the pagination argument with its page and perPage fields.

query categories {
  categories(pagination: { page: 1, perPage: 5 }) {
    entries {
      id
      title
      slug
      insertedAt
    }
    perPage
    page
    totalEntries
    totalPages
  }
}

We also have a query to get a specific category by passing a category id. Let's get one category and its threads.

query getCategory {
  category(id: "fd584b4f-6071-4f4f-8b33-3073214ff30e") {
    id
    title
    threads {
      id
    }
  }
}

Now that we have created a category and we know how to get all of the categories and a specific category, let's create a thread.

Threads

A Thread has the fields id, title, slug, category, and posts. You can see the category and posts fields reference other types in our schema, rather than scalars.

type Thread {
  id: ID!
  title: String!
  slug: String!
  category: Category!
  posts: [Post!]!
  insertedAt: DateTime!
  updatedAt: DateTime!
}

The createThread mutation requires the fields title, categoryId and the body for the first post. We are going to use the category we just created. Also, as a requirement, only authenticated users can create threads and posts. Since we’re sending a valid authorization header, we’ll be able to run the mutation.

mutation createThread{
  createThread(title: "My Thread", categoryId: "f3916bdd-8dd6-409d-9057-1c9c83ac592d", body: "My First post"){
    id
    title
  }
}

This mutation creates a thread, returning its id and title fields. We will use the thread id to create a post.

We can get the list of threads for a specific category id.

query getThreads {
  category(id: "5413db07-36de-49c2-816e-a0b7e0fcd4b4") {
    threads {
      id
      title
    }
  }
}

Posts

Our post has the fields id, body, thread, user, insertedAt, and updatedAt.

type Post {
  id: ID!
  body: String!
  thread: Thread!
  user: User!
  insertedAt: DateTime!
  updatedAt: DateTime!
}

To create a post, we also must be authenticated. We still have our authorization header set, so we’re golden. The threadId argument will be the id of the thread we just created.

mutation createPost{
  createPost(body: "Body of my post", threadId: "f188995d-8b71-48b2-beeb-36800f67b599"){
    body
  }
}

That created our post! We also have a query to get the posts from a specific thread. We just need to pass the thread id.

query getPosts {
  thread(id: "156b9e60-7e83-4a29-ae24-2b76a40c30dd") {
    id
    posts {
      id
      body
      insertedAt
      user {
        id
        name
      }
    }
  }
}

At a glance

In this lesson, we introduced our GraphQL Schema and used GraphQL Playground to explore it and execute some queries and mutations. In the next episodes we will be building apps using this schema. We will build the backend in both JavaScript and Elixir. We’ll also build clients in each of Vue, React Native, Elm, and Flutter.

Because GraphQL is schema focused, now that we have the schema we can build front-end or back-end applications for this schema. We’ll start on that next!

Resources