You are on page 1of 117

2

3
4
5
6
query {

course

query {
course
}

{
"data": {
"course": "GraphQL"
}
}

7
query {
course
me
}

8
query {
course
me {
id
name
email
}
}

{
"data": {
"course": "GraphQL",
"me": {
"id": "affb67e1-0d3b-40ae-b58d-7935a5506012",
"name": "Andrew Mead",
"email": "andrew@example.com"
}
}
}

User

9
query {
users {
id
name
}
}

{
"data": {
"users": [
{
"id": "affb67e1-0d3b-40ae-b58d-7935a5506012",
"name": "Andrew"
},
{
"id": "c2d96c35-e28a-4bfd-b075-3427bc09426a",
"name": "Sarah"
},
{
"id": "8b457570-06fa-4195-a712-d24bbc1b5294",
"name": "Michael"
}
]
}
}

10
babel-cli babel-preset-env

npm install babel-cli@6.26.0 babel-preset-env@1.7.0

.babelrc
babel-preset-env

{
"presets": [
"env"
]
}

{
"scripts": {
"start": "babel-node src/index.js"
}
}

npm run start

11
export

// utilities.js
const add = (a, b) => a + b
const subtract = (a, b) => a - b

export { add, subtract }

import

// index.js
import { add, subtract } from './utilities'

console.log(add(32, 1)) // Will print: 33


console.log(subtractadd(32, 1)) // Will print: 31

add subtract
square

12
// utilities.js
const add = (a, b) => a + b
const subtract = (a, b) => a - b
const square = (x) => x * x

export { add, subtract, square as default }

square otherSquare
add

// index.js
import otherSquare, { add } from './utilities'

console.log(add(32, 1)) // Will print: 33


console.log(otherSquare(10)) // Will print: 100

graphql-yoga

13
npm install graphql-yoga@1.14.10

graphql-yoga

import { GraphQLServer } from 'graphql-yoga'

// Type definitions (schema)


const typeDefs = `
type Query {
hello: String!
}
`

// Resolvers
const resolvers = {
Query: {
hello() {
return 'This is my first query!'
}
}
}

const server = new GraphQLServer({


typeDefs,
resolvers
})

server.start(() => {
console.log('The server is up!')
})

typeDefs

Query

Query

14
typeDefs
resolvers
typeDefs resolvers Query
Query hello hello

graphql-yoga
localhost:4000

query {
hello
}

15
{
"data": {
"hello": "This is my first query!"
}
}

type Query {
title: String!
price: Float!
releaseYear: Int
rating: Float
inStock: Boolean!
}

16
const resolvers = {
Query: {
title() {
return 'The War of Art'
},
price() {
return 12.99
},
releaseYear() {
return null
},
rating() {
return 5
},
inStock() {
return true
}
}
}

rating Float price Float!


! !

rating null
price price

17
npm install nodemon@1.17.5 --save-dev

{
"scripts": {
"start": "nodemon src/index.js --exec babel-node"
}
}

typeDefs

type Product {

id

18
type Product {
id: ID!
title: String!
price: Float!
releaseYear: Int
rating: Float
inStock: Boolean!
}

product

type Query {
product: Product!
}

product Product! !
User
product

const resolvers = {
Query: {
product() {
return {
id: '123',
title: 'Watch',
price: 39.99,
rating: 4.8,
inStock: false
}
}
}
}

19
query {
product {
id
title
rating
}
}

{
"data": {
"product": {
"id": "123",
"title": "Watch",
"rating": 4.8
}
}
}

20
greeting

type Query {
greeting(name: String): String!
}

typeDefs greeting

"args"

const resolvers = {
Query: {
greeting(parent, args, ctx, info) {
if (args.name) {
return `Hello, ${args.name}!`
} else {
return 'Hello!'
}
}
}
}

name String
String!

type Query {
greeting(name: String!): String!
}

name
"Andrew" name

21
query {
greeting(name: "Andrew")
}

{
"data": {
"product": "Hello, Andrew!"
}
}

grades
[Int!]!

type Query {
grades: [Int!]!
}

[]!

Int!

grades

22
const resolvers = {
Query: {
grades() {
return [
99,
74,
100,
3
]
}
}
}

query {
grades
}

{
"data": {
"grades": [99, 74, 100, 3]
}
}

users
User

23
type Query {
users: [User!]!
}

type User {
id: ID!
name: String!
age: Int
}

User id name
age

const resolvers = {
Query: {
users() {
return [{
id: '1',
name: 'Jamie'
}, {
id: '2',
name: 'Andrew',
age: 27
}, {
id: '3',
name: 'Katie'
}]
}
}
}

id

24
query {
users {
id
}
}

query {
users {
id
name
age
}
}

25
{
"data": {
"users": [
{
"id": "1",
"name": "Jamie",
"age": null
},
{
"id": "2",
"name": "Andrew",
"age": 27
},
{
"id": "3",
"name": "Katie",
age: null
}
]
}
}

User Post author

26
type User {
id: ID!
name: String!
email: String!
age: Int
}

type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
}

author
Post

parent
parent.author

const resolvers = {
// Query object hidden for brevity
Post: {
author(parent, args, ctx, info) {
return users.find((user) => {
return user.id === parent.author
})
}
}
}

id
title id title
author Post

27
query {
posts {
id
title
author {
name
}
}
}

posts User
[Post!]!

type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
}

type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
}

parent

28
const resolvers = {
// Query and Post objects hidden for brevity
User: {
posts(parent, args, ctx, info) {
return posts.filter((post) => {
return post.author === parent.id
})
}
}
}

posts

id title

query {
users {
id
name
posts {
id
title
}
}
}

29
Query
Mutation

createUser
name email
age
User

type Mutation {
createUser(name: String!, email: String!, age: Int): User!
}

Mutation resolvers createUser


createUser

30
import uuidv4 from 'uuid/v4'

const resolvers = {
// Other properties hidden for brevity
Mutation: {
createUser(parent, args, ctx, info) {
const emailTaken = users.some((user) => user.email ===
args.email)

if (emailTaken) {
throw new Error('Email taken')
}

const user = {
id: uuidv4(),
name: args.name,
email: args.email,
age: args.age
}

users.push(user)

return user
}
}
}

mutation
createUser name email
User

31
mutation {
createUser(name: "Andrew", email: "testing@example.com"){
id
name
email
age
}
}

{
"data": {
"createUser": {
"id": "8257f14f-80b0-4313-ad35-9047e3b5f851",
"name": "Andrew",
"email": "testing2@example.com",
"age": null
}
}
}

createUser

32
{
"data": null,
"errors": [
{
"message": "Email taken",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createUser"
]
}
]
}

title body published


author author

type Mutation {
createPost(title: String!, body: String!, published: Boolean!,
author: ID!): Post!
}

createPost

posts

33
const resolvers = {
// Other properties hidden for brevity
Mutation: {
createPost(parent, args, ctx, info) {
const userExists = users.some((user) => user.id === args.author)

if (!userExists) {
throw new Error('User not found')
}

const post = {
id: uuidv4(),
title: args.title,
body: args.body,
published: args.published,
author: args.author
}

posts.push(post)

return post
}
}
}

createPost
"2"

34
mutation {
createPost(
title:"My new post",
body:"",
published:false,
author:"2"
){
id
title
body
published
author {
name
}
comments {
id
}
}
}

{
"data": {
"createPost": {
"id": "18735d13-6b96-4eba-9b85-81da86853231",
"title": "My new post",
"body": "",
"published": false,
"author": {
"name": "Sarah"
},
"comments": []
}
}
}

35
npm install babel-plugin-transform-object-rest-spread@6.26.0

.babelrc plugins

{
"presets": [
"env"
],
"plugins": [
"transform-object-rest-spread"
]
}

one two one name age


two age location two one
...one two
age location name

36
const one = {
name: 'Andrew',
age: 27
}

const two = {
age: 25,
location: 'Philadelphia',
...one
}

console.log(two)

// Will print:
// {
// age: 27,
// location: "Philadelphia",
// name: "Andrew"
// }

createUser
name email age
data

37
type Mutation {
createUser(data: CreateUserInput!): User!
}

input CreateUserInput {
name: String!
email: String!
age: Int
}

createUser args.data

createUser(parent, args, ctx, info) {


// Can access name, email, and age on args.data
},

createUser
data
CreateUserInput

mutation {
createUser(
data:{
name:"Jess",
email:"jess@example.com"
}
){
id
}
}

38
deleteUser

deleteUser id

type Mutation {
deleteUser(id: ID!): User!
}

39
const resolvers = {
// Other properties hidden for brevity
Mutation: {
deleteUser(parent, args, ctx, info) {
const userIndex = users.findIndex((user) => user.id === args.id)

if (userIndex === -1) {


throw new Error('User not found')
}

const deletedUsers = users.splice(userIndex, 1)

posts = posts.filter((post) => {


const match = post.author === args.id

if (match) {
comments = comments.filter((comment) => comment.post !==
post.id)
}

return !match
})
comments = comments.filter((comment) => comment.author !==
args.id)

return deletedUsers[0]
}
}
}

index.js

40
ctx

db

const server = new GraphQLServer({


typeDefs: './src/schema.graphql',
resolvers,
context: {
db
}
})

users
db db.users

const resolvers = {
// Other properties hidden for brevity
Query: {
users(parent, args, { db }, info) {
if (!args.query) {
return db.users
}

return db.users.filter((user) => {


return
user.name.toLowerCase().includes(args.query.toLowerCase())
})
},
}
}

41
updateUser id
data
UpdateUserInput

type Mutation {
updateUser(id: ID!, data: UpdateUserInput!): User!
}

input UpdateUserInput {
name: String
email: String
age: Int
}

deleteUser updateUser
data

42
updateUser(parent, args, { db }, info) {
const { id, data } = args
const user = db.users.find((user) => user.id === id)

if (!user) {
throw new Error('User not found')
}

if (typeof data.email === 'string') {


const emailTaken = db.users.some((user) => user.email ===
data.email)

if (emailTaken) {
throw new Error('Email taken')
}

user.email = data.email
}

if (typeof data.name === 'string') {


user.name = data.name
}

if (typeof data.age !== 'undefined') {


user.age = data.age
}

return user
},

43
PubSub graphql-
yoga
PubSub context

44
import { GraphQLServer, PubSub } from 'graphql-yoga'
import db from './db'
import Query from './resolvers/Query'
import Mutation from './resolvers/Mutation'
import Subscription from './resolvers/Subscription'
import User from './resolvers/User'
import Post from './resolvers/Post'
import Comment from './resolvers/Comment'

const pubsub = new PubSub()

const server = new GraphQLServer({


typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context: {
db,
pubsub
}
})

server.start(() => {
console.log('The server is up!')
})

Subscription
count

45
type Subscription {
count: Int!
}

Subscription count
count subscribe count

pubsub.asyncIterator
pubsub.publish pubsub.asyncIterator
pubsub.publish

const Subscription = {
count: {
subscribe(parent, args, { pubsub }, info) {
let count = 0

setInterval(() => {
count++
pubsub.publish('count', {
count
})
}, 1000)

return pubsub.asyncIterator('count')
}
}
}

export { Subscription as default }

count

46
subscription {
count
}

{
"data": {
"count": 1
}
}

{
"data": {
"count": 2
}
}

{
"data": {
"count": 3
}
}

comment
postId

47
type Subscription {
comment(postId: ID!): Comment!
}

comment
postId

postId 1 comment 1

const Subscription = {
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 }

pubsub.publish
pubsub.publish createComment

48
const Mutation = {
createComment(parent, args, { db, pubsub }, info) {
const userExists = db.users.some((user) => user.id ===
args.data.author)
const postExists = db.posts.some((post) => post.id === args.data.post
&& post.published)

if (!userExists || !postExists) {
throw new Error('Unable to find user and post')
}

const comment = {
id: uuidv4(),
...args.data
}

db.comments.push(comment)
pubsub.publish(`comment ${args.data.post}`, { comment })

return comment
}
}

11

subscription {
comment(postId:"11"){
id
text
author{
id
name
}
}
}

49
{
"data": {
"comment": {
"id": "f6925dbb-8899-4be5-9ab6-365e698931c2",
"text": "My new comment",
"author": {
"id": "1",
"name": "Andrew"
}
}
}
}

data
mutation mutation
"CREATED" "UPDATED" "DELETED"

50
type Subscription {
post: PostSubscriptionPayload!
}

type PostSubscriptionPayload {
mutation: String!
data: Post!
}

pubsub.publish
data mutation
publish

const Mutation = {
deletePost(parent, args, { db, pubsub }, info) {
const postIndex = db.posts.findIndex((post) => post.id === args.id)

if (postIndex === -1) {


throw new Error('Post not found')
}

const [post] = db.posts.splice(postIndex, 1)

db.comments = db.comments.filter((comment) => comment.post !==


args.id)

if (post.published) {
pubsub.publish('post', {
post: {
mutation: 'DELETED',
data: post
}
})
}

return post
}
}

51
subscription {
post{
mutation
data {
id
title
body
author{
id
name
}
}
}
}

mutation

{
"data": {
"post": {
"mutation": "UPDATED",
"data": {
"id": "10",
"title": "GraphQL 101",
"body": "Something new...",
"author": {
"id": "1",
"name": "Andrew"
}
}
}
}
}

52
PowerState on off asleep

enum PowerState {
on
off
asleep
}

PowerState

mutation mutation
CREATED UPDATED DELETED

53
enum MutationType {
CREATED
UPDATED
DELETED
}

type PostSubscriptionPayload {
mutation: MutationType!
data: Post!
}

type CommentSubscriptionPayload {
mutation: MutationType!
data: Comment!
}

54
55
npm i -g prisma@1.12.0

prisma init prisma init

prisma init prisma

prisma init

prisma init

56
cd prisma

docker-compose up -d

prisma deploy

ssl true
docker-compose.yml

datamodel.graphql

type User {
id: ID! @unique
name: String!
}

User
createUser users

57
mutation {
createUser(
data:{
name:"Vikram"
}
){
id
name
}
}

{
"data": {
"createUser": {
"id": "cjjsq2rqf001d0822g2if54em",
"name": "Vikram"
}
}
}

datamodel.graphql

58
type User {
id: ID! @unique
name: String!
email: String! @unique
posts: [Post!]!
}

type Post {
id: ID! @unique
title: String!
body: String!
published: Boolean!
author: User!
}

datamodel.graphql
prisma deploy

59
mutation {
createPost(
data:{
title:"Prisma post",
body:"",
published:false,
author:{
connect:{
id:"cjjucbpfg004i0822onkb9z2l"
}
}
}
){
id
title
body
published
author{
id
name
}
}
}

60
{
"data": {
"createPost": {
"author": {
"id": "cjjucbpfg004i0822onkb9z2l",
"name": "Vikram"
},
"body": "",
"published": false,
"id": "cjjud0s7200580822gywi3e7y",
"title": "Prisma post"
}
}
}

prisma-binding
prisma-binding
graphql-cli

npm install prisma-binding@2.1.1 graphql-cli@2.16.4

prisma-binding
graphql get-schema graphql-cli

.graphqlconfig

61
{
"projects": {
"prisma": {
"schemaPath": "src/generated/prisma.graphql",
"extensions": {
"endpoints": {
"default": "http://localhost:4466"
}
}
}
}
}

scripts package.json

{
"scripts": {
"get-schema": "graphql get-schema -p prisma"
}
}

npm run get-schema prisma-binding

// prisma.js
import { Prisma } from 'prisma-binding'

const prisma = new Prisma({


typeDefs: 'src/generated/prisma.graphql',
endpoint: 'localhost:4466'
})

62
datamodel.graphql

prisma.query users comments


users comments
prisma.query

prisma.query.users(null, '{ id name posts { id title } }').then((data) => {


console.log(JSON.stringify(data, undefined, 2))
})

prisma.query.comments(null, '{ id text author { id name } }').then((data) =>


{
console.log(JSON.stringify(data, undefined, 2))
})

prisma.mutation prisma.query
prisma.mutation
deletePost
prisma.mutation.deletePost

createPost

63
prisma.mutation.createPost({
data: {
title: "GraphQL 101",
body: "",
published: false,
author: {
connect: {
id: "cjjybkwx5006h0822n32vw7dj"
}
}
}
}, '{ id title body published }').then((data) => {
console.log(data)
})

createPostForUser

prisma.mutation.createPost
prisma.query.use

64
const createPostForUser = async (authorId, data) => {
const post = await prisma.mutation.createPost({
data: {
...data,
author: {
connect: {
id: authorId
}
}
}
}, '{ id }')
const user = await prisma.query.user({
where: {
id: authorId
}
}, '{ id name email posts { id title published } }')
return user
}

createPostForUser

createPostForUser('cjjucl3yu004x0822dq5tipuz', {
title: 'Great books to read',
body: 'The War of Art',
published: true
}).then((user) => {
console.log(JSON.stringify(user, undefined, 2))
})

prisma prisma.exists
User Post
datamodel.graphql User Post prisma.exists

updatePostForUser
prisma.exists.Post

65
const updatePostForUser = async (postId, data) => {
const postExists = await prisma.exists.Post({ id: postId })

if (!postExists) {
throw new Error('Post not found')
}

const post = await prisma.mutation.updatePost({


where: {
id: postId
},
data
}, '{ author { id name email posts { id title published } } }')

return post.author
}

updatePostForUser catch

updatePostForUser("power", { published: true }).then((user) => {


console.log(JSON.stringify(user, undefined, 2))
}).catch((error) => {
console.log(error.message)
})

datamodel.graphl
unique relation

relation

66
onDelete CASCASE

onDelete SET_NULL

type Job {
id: ID! @unique
title: String!
qualifications: String!
salary: Int!
applicants: [Applicant!]! @relation(name: "ApplicantToJob",
onDelete: CASCADE)
}

type Applicant {
id: ID! @unique
name: String!
email: String! @unique
resume: String!
job: Job! @relation(name: "ApplicantToJob", onDelete: SET_NULL)
}

67
prisma-binding

import { Prisma } from 'prisma-binding'

const prisma = new Prisma({


typeDefs: 'src/generated/prisma.graphql',
endpoint: 'http://localhost:4466'
})

export { prisma as default }

prisma.query
prisma.mutation prisma.subscription prisma.exists

users prisma.query.users

68
const Query = {
users(parent, args, { prisma }, info) {
return prisma.query.users(null, info)
}
}

prisma-binding

undefined

info

users query

69
const Query = {
users(parent, args, { prisma }, info) {
const opArgs = {}

if (args.query) {
opArgs.where = {
OR: [{
name_contains: args.query
}, {
email_contains: args.query
}]
}
}

return prisma.query.users(opArgs, info)


}
}

"Andrew"

query {
users (
query:"Andrew"
) {
id
name
email
}
}

70
query {
users {
id
name
email
posts {
id
title
}
}
}

User.posts User.comments
Post.author Post.comments
Comment.author Comment.post

const User = {
posts(parent, args, { db }, info) {
return db.posts.filter((post) => {
return post.author === parent.id
})
},
comments(parent, args, { db }, info) {
return db.comments.filter((comment) => {
return comment.author === parent.id
})
}
}

export { User as default }

info prisma-binding info

User.js Comment.js Post.js

71
const User = {

export { User as default }

prisma-binding
prisma.query
prisma.exists prisma.mutation createUser

const Mutation = {
async createUser(parent, args, { prisma }, info) {
const emailTaken = await prisma.exists.User({ email: args.data.email
})

if (emailTaken) {
throw new Error('Email taken')
}

return prisma.mutation.createUser({ data: args.data }, info)


}
}

prisma.mutation.createUser info

72
pubsub.publish

pubsub.publish

pubsub.publish

prisma.subscription.comment
where

const Subscription = {
comment: {
subscribe(parent, { postId }, { prisma }, info){
return prisma.subscription.comment({
where: {
node: {
post: {
id: postId
}
}
}
}, info)
}
}
}

73
type CommentSubscriptionPayload {
mutation: MutationType!
node: Comment
}

prisma.yml

endpoint: http://localhost:4466
datamodel: datamodel.graphql
secret: thisismysupersecrettext

prisma deploy

secret prisma-binding

74
import { Prisma } from 'prisma-binding'

const prisma = new Prisma({


typeDefs: 'src/generated/prisma.graphql',
endpoint: 'http://localhost:4466',
secret: 'thisismysupersecrettext'
})

export { prisma as default }

prisma token

{
"Authorization":"Bearer YourTokenHere"
}

npm run get-schema get-schema


http://localhost:4466

npm run get-schema prisma


extensions get-schema

75
{
"projects": {
"prisma": {
"schemaPath": "src/generated/prisma.graphql",
"extensions": {
"prisma": "prisma/prisma.yml",
"endpoints": {
"default": "http://localhost:4466"
}
}
}
}
}

get-schema package.json

graphql get-schema -p prisma

createUser
schema.graphql

createUser

76
import bcrypt from 'bcryptjs'

const Mutation = {
async createUser(parent, args, { prisma }, info) {
if (args.data.password.length < 8) {
throw new Error('Password must be 8 characters or longer.')
}

const password = await bcrypt.hash(args.data.password, 10)

return prisma.mutation.createUser({
data: {
...args.data,
password
}
}, info)
}
}

createUser

77
import jwt from 'jsonwebtoken'

const Mutation = {
async createUser(parent, args, { prisma }, info) {
if (args.data.password.length < 8) {
throw new Error('Password must be 8 characters or longer.')
}

const password = await bcrypt.hash(args.data.password, 10)


const user = await prisma.mutation.createUser({
data: {
...args.data,
password
}
})

return {
user,
token: jwt.sign({ userId: user.id }, 'thisisasecret')
}
}
}

78
{
"Authorization": "Bearer
eyJhbGciaiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjamtqczMBcGYwMDQ3MDg3MzJt
NHJoaWFjIiwiaWF0IjoxNTMzNjU2NzE2fQ.MHSey8h1xeIfUmMvV75Vhmzvbx7R7jR65ZWr--
r2oVY"
}

context

const server = new GraphQLServer({


typeDefs: './src/schema.graphql',
resolvers: {
Query,
Mutation,
Subscription,
User,
Post,
Comment
},
context(request) {
return {
db,
pubsub,
prisma,
request
}
}
})

request
createPost

request getUserId getUserId

79
getUserId

const Mutation = {
createPost(parent, args, { prisma, request }, info) {
const userId = getUserId(request)

return prisma.mutation.createPost({
data: {
title: args.data.title,
body: args.data.body,
published: args.data.published,
author: {
connect: {
id: userId
}
}
}
}, info)
}
}

getUserId

import jwt from 'jsonwebtoken'

const getUserId = (request) => {


const header = request.request.headers.authorization

if (!header) {
throw new Error('Authentication required')
}

const token = header.replace('Bearer ', '')


const decoded = jwt.verify(token, 'thisisasecret')

return decoded.userId
}

export { getUserId as default }

80
deletePost
getUserId

const Mutation = {
async deletePost(parent, args, { prisma, request }, info) {
const userId = getUserId(request)
const postExists = await prisma.exists.Post({
id: args.id,
author: {
id: userId
}
})

if (!postExists) {
throw new Error('Unable to delete post')
}

return prisma.mutation.deletePost({
where: {
id: args.id
}
}, info)
}
}

81
post

getUserId

null

const getUserId = (request, requireAuth = true) => {


const header = request.request.headers.authorization

if (header) {
const token = header.replace('Bearer ', '')
const decoded = jwt.verify(token, 'thisisasecret')
return decoded.userId
}

if (requireAuth) {
throw new Error('Authentication required')
}

return null
}

post
getUserId false
requireAuth userId
userId null

prisma.query.posts

82
const Mutation = {
async post(parent, args, { prisma, request }, info) {
const userId = getUserId(request, false)

const posts = await prisma.query.posts({


where: {
id: args.id,
OR: [{
published: true
}, {
author: {
id: userId
}
}]
}
}, info)

if (posts.length === 0) {
throw new Error('Post not found')
}

return posts[0]
}
}

schema.graphql
String! String

User.email

83
getUserId requireAuth false

null

import getUserId from '../utils/getUserId'

const User = {
email(parent, args, { request }, info) {
const userId = getUserId(request, false)

if (userId && userId === parent.id) {


return parent.email
} else {
return null
}
}
}

export { User as default }

userFields
User User
userFields

users

84
query {
users {
...userFields
posts{
id
title
published
}
}
}

fragment userFields on User {


id
name
email
}

85
{
"data": {
"users": [
{
"id": "cjkjs28fg003x0873dqzy26y8",
"name": "Jess",
"email": null,
"posts": []
},
{
"id": "cjkl8qxky005z0873eix4o03n",
"name": "Andrew",
"email": null,
"posts": [
{
"id": "cjkl8rfiz00630873lz1zv2ho",
"title": "Updated post by Andrew",
"published": true
}
]
}
]
}
}

extractFragmentReplacements prisma-binding

fragmentReplacements Prisma GraphQLServer


fragmentReplacements
extractFragmentReplacements

86
import getUserId from '../utils/getUserId'

const User = {
email: {
fragment: 'fragment userId on User { id }',
resolve(parent, args, { request }, info) {
const userId = getUserId(request, false)

if (userId && userId === parent.id) {


return parent.email
} else {
return null
}
}
}
}

export { User as default }

getUserId
getUserId

87
const getUserId = (request, requireAuth = true) => {
const header = request.request ? request.request.headers.authorization :
request.connection.context.Authorization

// The rest of the function is the same and has been removed for brevity
}

getUserId
myPost

const Subscription = {
myPost: {
subscribe(parent, args, { prisma, request }, info) {
const userId = getUserId(request)

return prisma.subscription.post({
where: {
node: {
author: {
id: userId
}
}
}
}, info)
}
}
}

jwt.sign
expiresIn

88
jwt.sign({ userId }, 'thisisasecret', { expiresIn: '7 days' })

jwt.verify

first skip first


first 10 skip

first 10 skip 0
first 10
skip 10

89
first skip

type Query {
users(query: String, first: Int, skip: Int): [User!]!
}

first skip

const Query = {
users(parent, args, { prisma }, info) {
const opArgs = {
first: args.first,
skip: args.skip
}

if (args.query) {
opArgs.where = {
OR: [{
name_contains: args.query
}]
}
}

return prisma.query.users(opArgs, info)


}
}

90
query {
users(first:3, skip:6){
id
name
}
}

after after

first 50 after
after after
first 50 after
'somerandomid'

after

91
type Query {
users(query: String, first: Int, skip: Int, after: String):
[User!]!
}

const Query = {
users(parent, args, { prisma }, info) {
const opArgs = {
first: args.first,
skip: args.skip,
after: args.after
}

if (args.query) {
opArgs.where = {
OR: [{
name_contains: args.query
}]
}
}

return prisma.query.users(opArgs, info)


}
}

id updatedAt createdAt updatedAt


createdAt
DateTime!

92
type Comment {
id: ID! @unique
text: String!
author: User! @relation(name: "CommentToUser", onDelete: SET_NULL)
post: Post!@relation(name: "CommentToPost", onDelete: SET_NULL)
updatedAt: DateTime!
createdAt: DateTime!
}

schema.graphql
String!

type Comment {
id: ID!
text: String!
author: User!
post: Post!
updatedAt: String!
createdAt: String!
}

createdAt updatedAt

query {
comments{
id
text
updatedAt
createdAt
}
}

id text

93
{
"data": {
"comments": [
{
"id": "cjklaufyx009h0873rhadqtpz",
"text": "Jess Comment 3",
"updatedAt": "2018-08-08T15:39:30.881Z",
"createdAt": "2018-08-08T15:39:30.881Z"
},
{
"id": "cjklaugv5009n087301y9ybs2",
"text": "This is updated with auth",
"updatedAt": "2018-08-08T15:42:57.661Z",
"createdAt": "2018-08-08T15:39:31.953Z"
}
]
}
}

orderBy

orderBy

users orderBy
UserOrderByInput

94
# import UserOrderByInput from './generated/prisma.graphql'

type Query {
users(query: String, first: Int, skip: Int, after: String,
orderBy: UserOrderByInput): [User!]!
}

if (args.query) {
opArgs.where = {
OR: [{
name_contains: args.query
}]
}
}

return prisma.query.users(opArgs, info)


}

orderBy createdAt_DESC

95
query {
users(orderBy:createdAt_DESC){
id
name
email
updatedAt
createdAt
}
}

96
prisma.yml

http://localhost:4466

prisma.yml
config dev.env prod.env
dev.env

PRISMA_ENDPOINT=http://localhost:4466

prisma.yml

endpoint: ${env:PRISMA_ENDPOINT}
datamodel: datamodel.graphql
secret: thisismysupersecrettext

prisma deploy

prisma deploy -e ../config/dev.env

97
prisma login

prod.env dev.env

prisma deploy -e ../config/prod.env

prod.env

prisma.yml
prod.env

PRISMA_ENDPOINT=PUT-YOUR-PRODUCTION-URL-HERE

endpoint: ${env:PRISMA_ENDPOINT}
datamodel: datamodel.graphql
secret: thisismysupersecrettext

98
heroku login

PORT 4000

server.start({ port: process.env.PORT || 4000 }, () => {


console.log('The server is up!')
})

prisma.js
http://localhost:4466 PRISMA_ENDPOINT

const prisma = new Prisma({


typeDefs: 'src/generated/prisma.graphql',
endpoint: process.env.PRISMA_ENDPOINT,
secret: 'thisismysupersecrettext',
fragmentReplacements
})

PRISMA_ENDPOINT
env-cmd

npm install env-cmd@8.0.2

dev
dev.env

99
env-cmd ./config/dev.env nodemon src/index.js --ext js,graphql --exec babel-
node

package.json
heroku-postbuild start

package.json

{
"scripts": {
"start": "node dist/index.js",
"heroku-postbuild": "babel src --out-dir dist --copy-files",
}
}

babel-node babel

npm install @babel/polyfill@7.0.0

src/index.js

100
import '@babel/polyfill/noConflict'

node_modules

config
.gitignore

node_modules/
config/

git init

git add

git add .

git commit -am "Init commit"

101
heroku create

git push heroku master

config
test.env default
test

102
PRISMA_ENDPOINT=http://localhost:4466/default/test
PRISMA_SECRET=pi389xjam2b3pjsd0
JWT_SECRET=23oijdsm23809sdf

prisma

prisma deploy -e ../config/test.env

jest

npm install jest@23.5.0

{
"scripts": {
"test": "jest --watch"
}
}

.test.js tests

user.test.js tests test

103
test('Should create a new user', () => {

})

test

isValidPassword

const isValidPassword = (password) => {


return password.length >= 8 &&
!password.toLowerCase().includes('password')
}

export { isValidPassword }

isValidPassword
false

104
import { isValidPassword } from '../src/utils/user.js'

test('Should reject password shorter than 8 characters', () => {


const isValid = isValidPassword('abc123')

expect(isValid).toBe(false)
})

npm install parcel-bundler@1.9.7

package.json

{
"scripts": {
"start": "parcel src/index.html"
},
"devDependencies": {
"parcel-bundler": "^1.9.7"
}
}

npm run start npm start

105
npm install apollo-boost@0.1.16

import ApolloBoost, { gql } from 'apollo-boost'

const client = new ApolloBoost({


uri: 'http://localhost:4000'
})

106
const getUsers = gql`
query {
users {
id
name
}
}
`

client.query({
query: getUsers
}).then((response) => {
let html = ''

response.data.users.forEach((user) => {
html += `
<div>
<h3>${user.name}</h3>
</div>
`
})

document.getElementById('users').innerHTML = html
})

env-cmd

107
npm install env-cmd@8.0.2

package.json
globalSetup
globalTeardown

{
"scripts": {
"test": "env-cmd ./config/test.env jest --watch"
},
"jest": {
"globalSetup": "./tests/jest/globalSetup.js",
"globalTeardown": "./tests/jest/globalTeardown.js"
}
}

require('babel-register')
require('@babel/polyfill')
const server = require('../../src/server').default

module.exports = async () => {


// Do something to start the app up
global.httpServer = await server.start({ port: 4000 })
}

module.exports = async () => {


// Do something to tear the app down
await global.httpServer.close()
}

108
test('Should create a new user', async () => {
const createUser = gql`
mutation {
createUser(
data: {
name: "Andrew",
email: "andrew@example.com",
password: "MyPass123"
}
){
token,
user {
id
}
}
}
`

const response = await client.mutate({


mutation: createUser
})

const exists = await prisma.exists.User({ id:


response.data.createUser.user.id })
expect(exists).toBe(true)
})

109
beforeAll beforeEach afterAll afterEach
beforeEach

beforeEach(async () => {
// Wipe all test data and data that test cases may have added
await prisma.mutation.deleteManyUsers()

// Add test data base in


const user = await prisma.mutation.createUser({
data: {
name: 'Jen',
email: 'jen@example.com',
password: bcrypt.hashSync('Red098!@#$')
}
})
})

110
test('Should expose published posts', async () => {
const getPosts = gql`
query {
posts {
id
title
body
published
}
}
`
const response = await client.query({ query: getPosts })

expect(response.data.posts.length).toBe(1)
expect(response.data.posts[0].published).toBe(true)
})

toThrow

toThrow expect

test('Should throw an error', async () => {


expect(() => {
throw new Error()
}).toThrow()
})

toThrow
expect toThrow

111
test('Should not login with bad credentials', async () => {
const login = gql`
mutation {
login(
data: {
email: "jen@example.com",
password: "red098!@#$"
}
){
token
}
}
`

await expect(
client.mutate({ mutation: login })
).rejects.toThrow()
})

request

getClient

112
import ApolloBoost from 'apollo-boost'

const getClient = (jwt) => {


return new ApolloBoost({
uri: 'http://localhost:4000',
request(operation) {
if (jwt) {
operation.setContext({
headers: {
Authorization: `Bearer ${jwt}`
}
})
}
}
})
}

export { getClient as default }

test('My test case', async () => {


const client = getClient(userOne.jwt)
// Send off an operation that requires authentication
})

createUser

113
$
$data $data data
createUser

const createUser = gql`


mutation($data:CreateUserInput!) {
createUser(
data: $data
){
token,
user {
id
name
email
}
}
}
`

const variables = {
data: {
name: 'Andrew',
email: 'andrew@example.com',
password: 'MyPass123'
}
}

const response = await client.mutate({


mutation: createUser,
variables
})

114
getClient.js

done
done

test('Should subscribe to comments for a post', async (done) => {


const variables = {
postId: postOne.post.id
}
client.subscribe({ query: subscribeToComments, variables }).subscribe({
next(response) {
expect(response.data.comment.mutation).toBe('DELETED')
done()
}
})

await prisma.mutation.deleteComment({ where: { id: commentOne.comment.id


}})
})

115
PRISMA_ENDPOINT

116
prisma prisma
deploy -e
npm install
npm run test
npm run dev
heroku create
heroku config:set KEY=VALUE
prod.env
git push heorku master

117

You might also like