Introduction to the forum Data Model
An overview of the data model we’ll be using, and an exploration of it via GraphQL Playground
An overview of the data model we’ll be using, and an exploration of it via GraphQL Playground
@knewter
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:
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!
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@smoothterminal.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@smoothterminal.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.
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.
{ "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
- [Firestorm data](https://github.com/dailydrip/firestorm_data)
- [GraphQL Voyager](https://apis.guru/graphql-voyager/)
- [GraphQL Full introspection query](https://gist.github.com/franzejr/d0a178286d0e23d3ed50999288806068)
Comments for Introduction to the forum Data Model