### GraphQL

Spring 2019 。 Ric Huang
---
### Disclaimer
* 這份講義裡頭的範例與 tutorial 是參考這個 repo:[Ian Huang's Modern GraphQL Tutorial](https://github.com/ian13456/modern-graphql-tutorial?fbclid=IwAR3qAopoukGCv_Sx3clS05svCL5icno-EeFWS01i5E-Tm-Kb8Rp1eIZGuf4),請大家先 git clone 下來,等一下上課可以參考。
* 應作者要求,請大家喜歡的話就幫忙給個星星哦!
---
### What is "GraphQL"?([ref](https://en.wikipedia.org/wiki/GraphQL))
* An open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data.
* Developed internally by Facebook in 2012, and publicly released in 2015.
* In Nov 2018, project ownership is moved to the newly-established GraphQL Foundation, hosted by the non-profit Linux Foundation
----
### GraphQL 的好處
* **Faster** - with user-defined specification for data fetching,, GraphQL prevents clients from underfetching or overfetching, making network requests more efficient.
* **More Flexible** - user can define their own schemas and data types to share between frontend and backend.
* **Faster Production** - with schemas acting as a contract for data fetching between the frontend team and backend team, both teams can do their individual work without further communication.
----
An example would be:
```
Say you are creating an Instagram-like app. When a user opens
up the app, you want to show him the posts on his news feed,
so you make a network request to your backend and fetch all
the post data. As for the traditional RESTful API, you fetch
the post data including the comments and details about the
comments on the posts. However, that would be overfetching
since the user may not necessarily click into individual posts
to check comments. Here, GraphQL comes into play by letting
you specify what kind of data you want and how many comments
you want for each post. This not only limits the amount of
data needed to transfer through the internet, it also speeds
up the fetching efficiency and speed.
```
----
### Tools used in this tutorial
* [graphql-yoga](https://github.com/prisma/graphql-yoga) for the backend services
* [apollo](https://github.com/apollographql/react-apollo) for a simple front-end service
---
### Getting Started
```bash
git clone https://github.com/ian13456/modern-graphql-tutorial.git
cd modern-graphql-tutorial
npm install
```
----
### A quick experience on GraphQL
* The *database* in this example is loaded from: **backend/src/db.js**
```javascript
const users = [
{
id: '1',
name: 'Andrew',
email: 'andrew@example.com',
age: 27
},
{
id: '2',
name: 'Sarah',
email: 'sarah@example.com'
},
{
id: '3',
name: 'Mike',
email: 'mike@example.com'
}
]
const posts = [
{
id: '10',
title: 'GraphQL 101',
body: 'This is how to use GraphQL...',
published: true,
author: '1'
},
{
id: '11',
title: 'GraphQL 201',
body: 'This is an advanced GraphQL post...',
published: false,
author: '1'
},
{
id: '12',
title: 'Programming Music',
body: '',
published: true,
author: '2'
}
]
const comments = [
{
id: '102',
text: 'This worked well for me. Thanks!',
author: '3',
post: '10'
},
{
id: '103',
text: 'Glad you enjoyed it.',
author: '1',
post: '10'
},
{
id: '104',
text: 'This did no work.',
author: '2',
post: '11'
},
{
id: '105',
text: 'Nevermind. I got it to work.',
author: '1',
post: '12'
}
]
const db = {
users,
posts,
comments
}
export { db as default }
```
----
* **npm start** and open *http://localhost:4000*
* In this GraphQL playground, type in the sample query:
```javascript
query {
users(query: "a") {
id
name
age
}
}
```
* What do you see?
* Play around the query and compare with the DB
----
### 拆解 query syntax
```javascript
// DB request method: query, mutation, subscription
query {
// "resolver" name to perform query (arguments)
users(query: "a") {
// Fields to be queried
id
name
age
}
}
```
* The query structure is to define:
* DB action ('query')
* Target data ( {id, name, age} from 'users')
----
### Quick Summary

---
### GraphQL Schema
* To define what and how data is manipulated (e.g. queried) by GraphQL query
* Often named **schema.graphql**, sitting in the same directory as index.js.
* Added to server by 'typedefs' option
```javascript
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
...
}
```
* Three most important database request types: Query, Mutation, Subscription
----
### Example: "Query" in schema.graphql
```javascript
// In backend/src/schema.graphql
type Query {
users(query: String): [User!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
comments: [Comment!]!
}
```
* Basically, treat GraphQL schema as a "typedef". That is, "propertyName: typeOfData"
----
* "**type Query**" defines the outmost request method "query"
* "users" is the resolver defined in *backend/src/resolvers/Query.js*
* See other resolvers for type "Query"
```javascript
// In backend/src/schema.graphql
type Query {
users(query: String): [User!]!
posts(query: String): [Post!]!
comments: [Comment!]!
me: User!
post: Post!
}
```
* 'users', 'posts', 'comments'... are different resolvers
----
* ": [User!]!" in "users(query: String): [User!]!" specifies the expected returned data type
* "**User**" is a user-defined type
* **String**, **ID**, along with **Int**, **Float**, **Boolean** are the predefined scalar data types
* Note: scalar types basically mean that these values are scalars and do not contain any sub-fields below them
* **!** Exclamation mark after type names means non-nullable, meaning that the data sent back cannot be null or undefined. For example, \[String!\]! represents a non-nullable list containing values of Strings that cannot be null.
----
* Query string
```javascript
query {
users(query: "a") {
id
name
age
}
}
```
* GraphQL Schema
```javascript
// In backend/src/schema.graphql
type Query {
users(query: String): [User!]!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
comments: [Comment!]!
}
```
----
```javascript
{
"data": {
"users": [
{
"id": "1",
"name": "Andrew",
"age": 27
},
{
"id": "2",
"name": "Sarah",
"age": null
}
]
}
}
```
----
### The 'users' resolver
* Refer to data schema, where the argument for resolver 'users' is defined as (query: String)
```javascript
// backend/src/resolvers/Query.js
// We will cover these arguments later
users(parent, args, context, info) {
// See "context" in the "new GraphQLServer()" argument list
const { db } = context
// Check if the optional `query` of type String is passed in.
if (!args.query) {
return db.users
}
// Reaching here means `query` is defined -> filter the data passed back.
return db.users.filter((user) => {
return user.name.toLowerCase().includes(args.query.toLowerCase())
})
}
```
----
### Putting things together

---
### 回頭來看看 'backend/src/index.js' 的 code
```javascript
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context: {
db,
pubsub
}
})
// Serving server on port 4000
server.start({ port: process.env.PORT | 4000 }, () => {
console.log(`The server is up on port ${process.env.PORT | 4000}!`)
})
```
---
### Example: "Mutation" in schema.graphql
```javascript
// backend/src/schema.graphql
type Mutation {
createUser(data: CreateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
```
----
* Again, **createUser** is a resolver for Mutation
* **CreateUserInput** is an aggregation of data inputs for passing in data as an argument of 'createUser' resolver
* Expected to return a non-null data of type "User"
----
```javascript
mutation {
createUser(data: {
name: "Morgan"
age: 81
email: "morganfreeman@free.com"
}) {
id
name
}
}
```
* resulted in ---
```javascript
{
"data": {
"createUser": {
"id": "47afd124-0c06-4e2e-992f-0b837d4f0d5e",
"name": "Morgan"
}
}
}
```
----
### Resolver 'createUser'
```javascript
// backend/src/resolvers/Mutation.js
createUser(parent, args, context, info) {
const { db } = context
// Check if email is already taken in fake database
const emailTaken = db.users.some((user) => user.email === args.data.email)
if (emailTaken) {
throw new Error('Email taken')
}
// Create new user object
const user = {
id: uuidv4(),
...args.data
}
// Save (append) it to face database
db.users.push(user)
return user
}
```
---
### The "Subscription" request type
* As its name suggests, when a client "subscribes" a DB, it actually creates communication channel to listen to some changes of certain data in the DB. When the data is mutated, the corresponding resolver (where the data is mutated) can post the change and the server will notify all the clients that subscribe to this change.
* We use "pubsub" in the graphql-yoga service...
----
### 再看一次 'backend/src/index.js' 的 code
```javascript
import { GraphQLServer, PubSub } from 'graphql-yoga'
// ...import...lots...of...files...
const pubsub = new PubSub()
const server = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context: {
db,
pubsub
}
})
```
----
* 事實上,resolver 都會吃進四個參數 (parent, args, context, info)
* 其中 args 就是從 query string 傳進去的參數
* 而 context 定義了 GraphQL 在操作時外部的資源,在我們的 backend server 當中,我們傳入了 "db" 以及 "pubsub"
----
### Example: "Subscription" in schema.graphql
* Notified when **db.posts** is created/updated/deleted
```javascript
// backend/src/schema.graphql
type Subscription {
comment(postId: ID!): CommentSubscriptionPayload!
}
type CommentSubscriptionPayload {
mutation: MutationType!
data: Comment!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
enum MutationType {
CREATED
UPDATED
DELETED
}
```
----
### The resolver of Subscription
* Subscription 的 resolver 比較特別,因為它不只是處理 resolution 然後回傳 data 而已,它還要 create 一個 websocket 來通知 subscribers db.posts 有改變
```javascript
// backend/src/resolvers/Subscription.js
const Subscription = {
comment: { // create a channel listening to comment
subscribe(parent, { postId }, { db, pubsub }, info) {
const post = db.posts.find(post => post.id === postId && post.published)
if (!post) {
throw new Error('Post not found')
}
return pubsub.asyncIterator(`comment ${postId}`)
}
}
}
export { Subscription as default }
```
----
* **return pubsub.asyncIterator(`comment ${postId}`)** creates a subscription channel with `comment ${postId}` as its tag
* 然後在 Mutation.js 裡頭的 create/update/delete post 裏頭加上 pubsub.publish 的 websocket hooks!
```javascript
// backend/src/resolvers/Mutation.js
const Mutation = {
...
createPost(parent, args, { db, pubsub }, info) {
const userExists = db.users.some(user => user.id === args.data.author)
if (!userExists) {
throw new Error('User not found')
}
const post = {
id: uuidv4(),
...args.data
}
db.posts.unshift(post)
if (args.data.published) {
pubsub.publish('post', {
post: {
mutation: 'CREATED',
data: post
}
})
}
return post
},
...
}
```
----
### Try it on GraphQL playground!!
* Subscription request
```javascript
subscription {
comment (postId: 10) {
mutation
data {
text
author {
name
}
}
}
}
```
----
* Mutation request
```javascript
mutation {
createComment(data: {
text: "Hello sir! Nice Post!"
author: 1
post: 10
}) {
text
author {
name
}
}
}
```
----
* Returned result on subscription side
```javascript
{
"data": {
"comment": {
"mutation": "CREATED",
"data": {
"text": "Hello sir! Nice Post!",
"author": {
"name": "Andrew"
}
}
}
}
}
```
---
### Now we have create the GraphQL APIs on the server side, the next thing is how to generate DB requests from the client side!
---

----
* This front-end contains a form and a list of posts.
* The form allows you to use GraphQL mutations to create posts in the database, and the list of posts is fetched with a GraphQL query.
* This list is actually subscribed to any new posts, which means when the form creates a new post, the post gets saved in the database and sent back to the client spontaneously through a GraphQL subscription.
----
### Starting front-end app
```bash
cd frontend
npm install
npm start
```
---
* In order to connect to the backend API, you need to set up a GraphQL client like the Apollo client below:
```javascript
// frontend/src/index.js
import { ApolloClient, InMemoryCache } from 'apollo-boost'
import { ApolloProvider } from 'react-apollo'
import { split } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { WebSocketLink } from 'apollo-link-ws'
import { getMainDefinition } from 'apollo-utilities
// Create an http link:
const httpLink = new HttpLink({
uri: 'http://localhost:4000/'
})
// Create a WebSocket link:
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: { reconnect: true }
})
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
link,
cache: new InMemoryCache().restore({})
})
```
----
### The 'split' utility in Apollo
* To split types of GraphQL requests.
* We can then send the request to different GraphQL endpoints accordingly.
---
### File structures in 'frontend/src'
```bash
> src/
> public/index.html
> components/Post/Post.js // define view for Post form
> containers/App/App.js // define view for the app
> graphql
> index.js
> queries.js
> mutation.js
> subscriptions.js
> index.js
```
----
### GraphQL DB requests
* Gathered in "graphql/index.js"
```javascript
export { POSTS_QUERY } from './queries'
export { CREATE_POST_MUTATION } from './mutations'
export { POSTS_SUBSCRIPTION } from './subscriptions'
```
----
### Define GraphQL DB requests by **react-apollo**
* Using **gql** template string
```javascript
// frontend/src/graphql/queries.js
import { gql } from 'apollo-boost'
export const POSTS_QUERY = gql`
query {
posts {
title
body
author {
name
}
published
}
}
`
```
----
```javascript
import { gql } from 'apollo-boost'
export const CREATE_POST_MUTATION = gql`
mutation createPost(
$title: String!
$body: String!
$published: Boolean!
$authorId: ID!
) {
createPost(
data: {
title: $title
body: $body
published: $published
author: $authorId
}
) {
title
body
author {
name
}
published
}
}
`
```
----
```javascript
// frontend/src/graphql/subscriptions.js
export const POSTS_SUBSCRIPTION = gql`
subscription {
post {
mutation
data {
title
body
author {
name
}
published
}
}
}
`
```
---

----
### GraphQL Mutations in App.js
```javascript
<Mutation mutation={CREATE_POST_MUTATION}> // predefined class components in 'react-apollo'
{createPost => {
this.createPost = createPost
return (
<Form onSubmit={this.handleFormSubmit}>
<FormGroup row>
// for Title
</FormGroup>
<FormGroup>
// for Body
</FormGroup>
<Button type="submit" color="primary">
Post!
</Button>
</Form>
)
}}
</Mutation>
```
----
### Closer look at \<Mutation>
* **this.createPost = createPost** saves the *createPost* function generated by Mutation so it can be used later in **handleFormSubmit**
----
* **handleFormSubmit** fires mutation request to DB everytime the form is submitted
```javascript
// frontend/src/container/App/App.js
handleFormSubmit = e => {
e.preventDefault()
const { formTitle, formBody } = this.state
if (!formTitle || !formBody) return
this.createPost({
variables: {
title: formTitle,
body: formBody,
published: true,
authorId: 2
}
})
this.setState({
formTitle: '',
formBody: ''
})
}
```
----
### GraphQL queries in App.js
```javascript
<Query query={POSTS_QUERY}> // predefined class components in 'react-apollo'
{({ loading, error, data, subscribeToMore }) => {
if (loading) return <p>Loading...</p>
if (error) return <p>Error :(((</p>
const posts = data.posts.map((post, id) => (
<Post data={post} key={id} />
))
if (!unsubscribe)
unsubscribe = subscribeToMore({
document: POSTS_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newPost = subscriptionData.data.post.data
return {
...prev,
posts: [newPost, ...prev.posts]
}
}
})
return <div>{posts}</div>
}}
</Query>
```
----
### Closer look at \<Query>
* The function within the Query tag is defined to be the process you execute once the GraphQL request is fired.
* `data` represents the data received;
* `loading` is a boolean indicating whether the query is pending or not.
* Once the data is received, you can then return whatever you want to render with the data.
----
* The **subscribeToMore** function is defined in 'react-apollo' and is what tells the GraphQL client to listen to any updates.
* Once an update is sent from backend through wsLink to the client, the 'updateQuery' function in subscribeToMore gets executed.
* It basically takes the cached posts from the query, *prev*, append the freshly received post onto it, and put the *prev* cache back in place. (reason to the unsubscribe variable is explained here.)
---
### Homework #3 --- Basic Requirements
1. On the left side of "Post!", add a "pull-down" menu on top of "Title", from which you can select author's name to make a new post. The list of authors, of course, should be loaded from "db.js"
2. Change the view of the right side to list of authors with number of posts. Whenever a new post is published, the number followed by the corresponding author should be incremented automatically.
----
### Homework #3 --- Basic Requirements
3. The posts of each author should be by default folded. When you click on an author, its posts will be expanded. Click again and the it will be folded.
----
### Homework #3 --- Advanced Requirements
1. Linked to a real DB (Mongo, MySQL, or anything)
2. Implement a "like" system. You can make it anonymous (like Medium) or named (like FB)
3. Add "authentication"
4. Improve UI/outlook
----
### Homework #3 --- Rules and Deadline
* Deadline: 9pm, Sunday, 06/09
* Create a directory "Homework03" under "WebProg2019"
* In "Homework03", follow the directory structure as in "[Modern GraphQL Tutorial](https://github.com/ian13456/modern-graphql-tutorial)".
* Add more files or directories as you wish, if necessary
---
### That's it!! Enjoy the Homework~
{"metaMigratedAt":"2023-06-14T21:58:59.004Z","metaMigratedFrom":"YAML","title":"GraphQL","breaks":true,"slideOptions":"{\"theme\":\"beige\",\"transition\":\"fade\",\"slidenumber\":true}","contributors":"[{\"id\":\"752a44cb-2596-4186-8de2-038ab32eec6b\",\"add\":21803,\"del\":1427}]"}