- Basic Babel VSCode
- Creating Your Own GraphQL API
- Prisma
- Deploying To Heroku
- Testing - Jest Framework
npm init
npm i babel-cli
npm i babel-preset-env
npm i graphql-yoga
mkdir src src/demo
touch src/index.js src/demo/myModule.js src/demo/math.js .babelrc
-
Add
babel-node src/index.js
in the scripts array"scripts": { "start": "babel-node src/index.js", "test": "echo \"Error: no test specified\" && exit 1" },
-
Or use nodemon to run the server
"scripts": { "start": "nodemon src/index.js --exec babel-node", "test": "echo \"Error: no test specified\" && exit 1" },
-
In
.babelrc
{ "presets": ["env"] }
-
In
src/demo/myModule.js
-
It will contain contain some code that's necessary for
index.js
to run- function, methods, variables...
-
Named export - Has a name. Have as many as needed
-
Default export - Has no name. You can only have one
const message = 'Some message from myModule.js'; const name = 'Roger Takeshita'; const location = 'Toronto'; const getGreeting = (name) => `Welcome to the course ${name}`; export { message, name, getGreeting, location as default };
-
-
In
src/demo/math.js
const add = (number1, number2) => number1 + number2; const subtract = (number1, number2) => number1 - number2; export { subtract, add as default };
-
In
src/index.js
import myCurrentLocation, { message, name, getGreeting } from './demo/myModule'; import sum, { subtract } from './demo/math'; console.log(message); console.log(name); console.log(myCurrentLocation); console.log(getGreeting('Thaisa')); console.log(sum(1, 1)); console.log(subtract(1, 1));
npm run start
- GraphQL has 5 scalar types:
- String
- Boolean
- Int
- Float
- ID
-
Import the
{GraphQLServer}
fromgraphql-yoga
-
Then we have to define:
Type definitions
- Schema (Data Structures)Resolvers
- Define functions that run for each of the operations that can be performed on our API
-
in
src/index.js
import { GraphQLServerLambda, GraphQLServer } from 'graphql-yoga'; //! Demo user data const users = [ { id: '1', name: 'Roger', email: 'roger@gmail.com', age: 33 }, { id: '2', name: 'Yumi', email: 'ymi@gmail.com' }, { id: '3', name: 'Mike', email: 'mike@gmail.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: false, 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: '11' } ]; //! Type definitions (Schema) //! ! Always get type back const typeDefs = ` type Query { users(query: String): [User!]! posts(query: String): [Post!]! me: User! post: Post! comments: [Comment!]! } type User { id: ID! name: String! email: String! age: Int posts: [Post!]! comments: [Comment!]! } type Post { id: ID! title: String! body: String! published: Boolean! author: User! comments: [Comment!]! } type Comment { id: ID! text: String! author: User! post: Post! } `; //! Resolvers const resolvers = { Query: { users(parent, args, ctx, info) { if (!args.query) return users; return users.filter((user) => { return user.name.toLowerCase().includes(args.query.toLowerCase()); }); }, me() { return { id: '123456', name: 'Mike', email: 'mike@gmail.com', age: null }; }, post() { return { id: '789', title: 'Harry Potter', body: 'Lorem text', published: false }; }, posts(parent, args, ctx, info) { if (!args.query) return posts; return posts.filter((post) => { const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase()); const isBodyMatch = post.body.toLowerCase().includes(args.query.toLowerCase()); return isTitleMatch || isBodyMatch; }); }, comments(parent, args, ctx, info) { return comments; } }, Post: { author(parent, args, ctx, info) { return users.find((user) => { return user.id === parent.author; }); }, comments(parent, args, ctx, info) { return comments.filter((comment) => { return comment.post === parent.id; }); } }, User: { posts(parent, args, ctx, info) { return posts.filter((post) => { return post.author === parent.id; }); }, comments(parent, args, ctx, info) { return comments.filter((comment) => { return comment.author === parent.id; }); } }, Comment: { author(parent, args, ctx, info) { return users.find((user) => { return user.id === parent.author; }); }, post(parent, args, ctx, info) { return posts.find((post) => { return post.id === parent.post; }); } } }; //! Create the server //+ The server expects an object as argument with: //+ - typeDefs //+ - resolvers const server = new GraphQLServer({ typeDefs, resolvers }); //! Start the server server.start(() => { console.log('The Server is Running'); });
-
install prisma globally
npm i prisma -g npm i graphql-cli
-
Download the following tools
-
Create a PostgreSQL database on Heroku
- Login to your heroku account
- Click on
New > Create new app
- Give a name to your app (in my case
roger-takeshita-dev-server
) - Click on
Create app
- Give a name to your app (in my case
- Click on your brand new project (
roger-takeshita-dev-server
)- Click on
Resources
- Find an
Add-ons
calledHeroku Postgres
- Select a plan (in this case
Hobby Dev - Free
) - Click on
Provision
- Select a plan (in this case
- Find an
- Click on new add-on
Heroku Postgres
(it will open a new tab)- Click on
Settings
- Click on
View Credentials
- Heroku will display all the information necessary for us to connect to the database
- Use those information to login with your
pgAdmin
- Click on
- Click on
- Click on
- Click on
Please note that these credentials are not permanent. Heroku rotates credentials periodically and updates applications where this database is attached. Host ec2-52-70-15-120.compute-1.amazonaws.com Database d7jasf7ogfo91 User tqiffasclofnar Port 5432 Password 0fc0851041e3fe1a7c20efagasdfas92f05fasfas2a4ed66ea8738ac1b0e4c75b URI postgres://tqiffasclofnar:0fc0851041e3fe1a7c20efagasdfas92f05fasfas2a4ed66ea8738ac1b0e4c75b@ec2-52-70-15-120.compute-1.amazonaws.com:5432/d7jasf7ogfo91 Heroku CLI heroku pg:psql postgresql-clean-98586 --app roger-takeshita-dev-server
- Login to your heroku account
npm i prisma-binding
-
It gives us a set of node.js methods that we can use to interact with our prisma graphical
- Example:
prisma.query.user(...) prisma.mutation.createUser(...) prisma.mutation.updateUser(...) prisma.mutation.deleteUser(...)
-
create a new file called
prisma.js
on the root of the project -
graphql-cli
gives us a couple of different commands for performing common tasks. And one of the common task is fetching the schema form a given API. -
we need to create a graphql configuration file
- on the root of our project, create a
.graphqlconfig
(it's json configuration file)- There we need to inform two things
- Where the schema lives
- Where where it should be saved
- There we need to inform two things
mkdir src/generated touch .graphqlconfig src/prisma.js
- on the root of our project, create a
-
in
.graphqlconfig
{ "projects": { "prisma": { "schemaPath": "src/generated/prisma.graphql", "extensions": { "endpoints": { "default": "http://localhost:4466" } } } } }
-
in
package.json
- Add a new line
"get-schema"
we call graphql we invoke the methodget-schema
pass the-p
(project) and prisma (project name)
"scripts": { "start": "nodemon src/index.js --ext js,graphql --exec babel-node", "test": "echo \"Error: no test specified\" && exit 1", "get-schema": "graphql get-schema -p prisma" },
- Add a new line
-
then to generate the
prisma.graphql
inside the folder that we created- Run
npm run get-schema
- Run
- After running the command
npm run get-schema
prisma will automatically create 3 files insidesrc/generated
folderdatamodel.prisma
- Our Schemadocker-compose.yml
- Our database configuration- remove the line
Schema
- remove the line
prisma.yml
- Our endpoint
-
We have two available methods on delete
- By default is set to
SET_NULL
this means, if we delete an user, but this user has posts related to this user ID, prisma will set asnull
the id - The other option is
CASCADE
, it we delete an user, this will delete all the chain related to this user
type User { id: ID! @id @unique name: String! email: String! @unique posts: [Post!]! @relation(name: "PostToUser", onDelete: CASCADE) comments: [Comment!]! @relation(name: "CommentToUser", onDelete: CASCADE) updatedAt: DateTime @updatedAt createdAt: DateTime @createdAt } type Post { id: ID! @id @unique title: String! body: String! published: Boolean! author: User! @relation(name: "PostToUser", onDelete: SET_NULL) comments: [Comment!]! @relation(name: "CommentToPost", onDelete: CASCADE) } type Comment { id: ID! @id @unique text: String! post: Post! @relation(name: "CommentToPost", onDelete: SET_NULL) author: User! @relation(name: "CommentToUser", onDelete: SET_NULL) }
- By default is set to
-
Create a file called
database
in our source directorytouch src/database.js
-
Inside this file we are going to add dummy data
const users = [ { id: '1', name: 'Roger', email: 'roger@gmail.com', age: 33 }, { id: '2', name: 'Yumi', email: 'ymi@gmail.com' }, { id: '3', name: 'Mike', email: 'mike@gmail.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: false, 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 };
-
Create a
prisma.js
insidesrc
folder -
In
src/prisma.js
- We have all the methods to CRUD our data base using Prisma Bindings
import { Prisma } from 'prisma-binding'; //! Create the connection //+ Then we pass an object as argument to connect prisma endpoint //- We have to provide two things: //? typeDefs, we need to provide type definitions for the endpoint, so prisma binding //? library can generate all the methods that we need //? endpoint, specifies the URL where prisma graphical API lives const prisma = new Prisma({ typeDefs: 'src/generated/prisma.graphql', endpoint: 'http://localhost:4466' }); //! PRISMA BINDINGS //! prisma.query, prisma.mutation, prisma.subscription, prism.exists //+ 1st argument options (operation args, selection set) - returns a promise //- JSON.stringify ( data, replacer, indentation ) // prisma.query // .users(null, '{ id name posts { id title } }') // .then((data) => console.log(JSON.stringify(data, undefined, 4))); // prisma.query // .comments(null, '{ id text author { id name } }') // .then((data) => console.log(JSON.stringify(data, undefined, 4))); //! PRISMA MUTATIONS //+ Create Post //- Async/Await Create Post - Always return a promise const createPostForUser = async (authorId, data) => { const userExists = await prisma.exists.User({ id: authorId }); if (!userExists) throw new Error('User not found'); const post = await prisma.mutation.createPost( { data: { ...data, author: { connect: { id: authorId } } } }, '{ author { id name email posts { id title published } } }' ); return post; }; //+ Update Post //- Async/Await Update Post - Always return a promise const updatePostFroUser = 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( { data, where: { id: postId } }, '{ author { id name email posts { id title published } } }' ); return post; }; updatePostFroUser('cka4ak88o03gk0721jh3lu4w5', { title: 'UPDATED POST ' + new Date(), body: 'UPDATED BODY ' + new Date(), published: false }) .then((user) => console.log(JSON.stringify(user, undefined, 4))) .catch((error) => console.log(error.message));
- prisma.query.users(null, second_argument )
- second_argument can be:
- nothing / null / undefined - Never ask for relational data
- string - selection set as string
- object - info object (contains all the original information)
- We need to refactor our prisma.js into multiple files to keep organized
- Convert our
Promises
intoAsync/Await
-
Create folder and files
mkdir src/resolvers touch src/resolvers/Comments.js src/resolvers/Mutation.js src/resolvers/Post.js src/resolvers/Query.js src/resolvers/Subscription.js src/resolvers/User.js
-
in
Comment.js
const Comment = { author(parent, args, { db: { users } }, info) { return users.find((user) => user.id === parent.author); }, post(parent, args, { db: { posts } }, info) { return posts.find((post) => post.id === parent.post); } }; export { Comment as default };
-
in
Post.js
const Post = { author(parent, args, { db: { users } }, info) { return users.find((user) => user.id === parent.author); }, comments(parent, args, { db: { comments } }, info) { return comments.filter((comment) => comment.post === parent.id); } }; export { Post as default };
-
in
Mutation.js
import uuidv4 from 'uuid/v4'; const Mutations = { createUser(parent, { data }, { db: { users } }, info) { const emailTaken = users.some((user) => user.email === data.email); if (emailTaken) throw new Error('Email taken'); const user = { id: uuidv4(), ...data }; users.push(user); return user; }, deleteUser(parent, { id }, { db: { users, posts } }, info) { const userIndex = users.findIndex((user) => user.id === id); if (userIndex === -1) throw new Error('User not found'); const deletedUsers = users.splice(userIndex, 1); posts = posts.filter((post) => { const match = post.author === id; if (match) comments = comments.filter((comment) => comment.post !== post.id); return !match; }); comments = comments.filter((comment) => comment.author !== id); return deletedUsers[0]; }, updateUser(parent, { id, data }, { db: { users } }, info) { const user = users.find((user) => user.id === id); if (!user) throw new Error('User not found'); if (typeof data.email === 'string') { const emailTaken = users.some((user) => user.email === data.email); if (emailTaken) throw new Error('Email in use'); user.email = data.email; } if (typeof data.name === 'string') user.name = data.name; if (typeof data.age !== 'undefined') user.age = data.age; return user; }, createPost(parent, { data }, { db: { users, posts }, pubsub }, info) { const userExists = users.some((user) => user.id === data.author); if (!userExists) throw new Error('User not found'); const post = { id: uuidv4(), ...data }; posts.push(post); if (data.published) pubsub.publish(`POST`, { post: { mutation: 'CREATED', data: post } }); return post; }, deletePost(parent, { id }, { db: { posts, comments }, pubsub }, info) { const postIndex = posts.findIndex((post) => post.id === id); if (postIndex === -1) throw new Error('Post not found'); const [post] = posts.splice(postIndex, 1); comments = comments.filter((comment) => comment.post !== id); if (post.published) { pubsub.publish('POST', { post: { mutation: 'DELETED', data: post } }); } return post; }, updatePost(parent, { id, data }, { db: { posts }, pubsub }) { const post = posts.find((post) => post.id === id); const originalPost = { ...post }; if (!post) throw new Error('Post not found'); if (typeof data.title === 'string') post.title = data.title; if (typeof data.body === 'string') post.body = data.body; if (typeof data.published === 'boolean') { post.published = data.published; if (originalPost.published && !post.published) { pubsub.publish('POST', { post: { mutation: 'DELETED', data: originalPost } }); } else if (!originalPost.published && post.published) { pubsub.publish('POST', { post: { mutation: 'CREATED', data: post } }); } } else if (post.published) { pubsub.publish('POST', { post: { mutation: 'UPDATED', data: post } }); } return post; }, createComment(parent, { data }, { db: { users, posts, comments }, pubsub }, info) { const userExists = users.some((user) => user.id === data.author); const postExists = posts.some((post) => post.id === data.post); if (!postExists || !userExists) throw new Error('Unable to find user or post'); const comment = { id: uuidv4(), ...data }; comments.push(comment); pubsub.publish(`COMMENT ${data.post}`, { comment: { mutation: 'CREATED', data: comment } }); return comment; }, deleteComment(parent, { id }, { db: { comments }, pubsub }, info) { const commentIndex = comments.findIndex((comment) => comment.id === id); if (commentIndex === -1) throw new Error('Comment not found'); const [deletedComment] = comments.splice(commentIndex, 1); pubsub.publish(`COMMENT ${deletedComment.post}`, { comment: { mutation: 'DELETED', data: deletedComment } }); return deletedComment; }, updateComment(parent, { id, data }, { db: { comments }, pubsub }) { const commentExist = comments.find((comment) => comment.id === id); if (!commentExist) throw new Error('Comment not found'); if (typeof data.text === 'string') commentExist.text = data.text; pubsub.publish(`COMMENT ${commentExist.post}`, { comment: { mutation: 'UPDATED', data: commentExist } }); return commentExist; } }; export { Mutations as default };
-
in
Query.js
const Query = { users(parent, args, { db: { users } }, info) { if (!args.query) return users; return users.filter((user) => { return user.name.toLowerCase().includes(args.query.toLowerCase()); }); }, posts(parent, args, { db: { posts } }, info) { if (!args.query) return posts; return posts.filter((post) => { const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase()); const isBodyMatch = post.body.toLowerCase().includes(args.query.toLowerCase()); return isTitleMatch || isBodyMatch; }); }, comments(parent, args, { db: { comments } }, info) { return comments; }, me() { return { id: '123', name: 'Thaisa', email: 'thaisa@gmail.com' }; }, post() { return { id: '123', title: 'GraphQL 101', body: '', published: false }; } }; export { Query as default };
-
in
Subscription.js
const Subscription = { comment: { subscribe(parent, { postId }, { db: { posts }, pubsub }, info) { const post = posts.find((post) => post.id == postId && post.published); if (!post) throw new Error('Post not found'); return pubsub.asyncIterator(`COMMENT ${postId}`); } }, post: { subscribe(parent, args, { pubsub }, info) { return pubsub.asyncIterator(`POST`); } } }; export { Subscription as default };
-
in
User.js
const User = { posts(parent, args, { db: { posts } }, info) { return posts.filter((post) => post.author === parent.id); }, comments(parent, args, { db: { comments } }, info) { return comments.filter((comment) => comment.author === parent.id); } }; export { User as default };
-
In
src/index.js
- We are going to import and connect everything
- import resolvers, database, prisma
import { GraphQLServer, PubSub } from 'graphql-yoga'; import db from './database'; import Query from './resolvers/Query'; import Mutation from './resolvers/Mutation'; import User from './resolvers/User'; import Post from './resolvers/Post'; import Comment from './resolvers/Comment'; import Subscription from './resolvers/Subscription'; import './prisma'; //! Subscriptions const pubsub = new PubSub(); //! Create the server //+ The server expects an object as argument with: //+ - typeDefs //+ - resolvers const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers: { Query, Mutation, User, Post, Comment, Subscription }, context: { db, pubsub } }); //! Start the server server.start(() => { console.log('The Server is Running'); });
- We are going to import and connect everything
-
in
src/resolvers/index.js
-
Import
GraphQLServer
fromgraphql-yoga
- To create our GraphQL server
-
Import
PubSub
fromgraphql-yoga
- To create subscriptions
- In the future prisma will handle the subscription for us
-
Import our local database (in the future we are going to connect with Prisma and Heroku/PostgreSQL)
-
Import all our resolvers files
- Query, Mutation, User, Post, Comment, Subscription
-
Import out prisma connection file
-
then to connect Prisma with node.js, we just have to import in the beginning the file
import { GraphQLServer, PubSub } from 'graphql-yoga'; import db from './database'; import Query from './resolvers/Query'; import Mutation from './resolvers/Mutation'; import User from './resolvers/User'; import Post from './resolvers/Post'; import Comment from './resolvers/Comment'; import Subscription from './resolvers/Subscription'; import './prisma'; //! Subscriptions const pubsub = new PubSub(); //! Create the server //+ The server expects an object as argument with: //+ - typeDefs //+ - resolvers const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers: { Query, Mutation, User, Post, Comment, Subscription }, context: { db, pubsub } }); //! Start the server server.start(() => { console.log('The Server is Running'); });
-
-
Create a new file inside
src/resolvers
calledSubscription.js
-
This file will be responsible for sending updated data to the users that subscribed for a specific post or comment
-
In the future we are going to use
prisma
to do all the heavy lifting for use, but for now let's manually do itconst Subscription = { comment: { subscribe(parent, { postId }, { db: { posts }, pubsub }, info) { const post = posts.find((post) => post.id == postId && post.published); if (!post) throw new Error('Post not found'); return pubsub.asyncIterator(`COMMENT ${postId}`); } }, post: { subscribe(parent, args, { pubsub }, info) { return pubsub.asyncIterator(`POST`); } } }; export { Subscription as default };
-
Created a new prisma project inside an existing project - Duplicate the prisma folder - Delete the
docker-compose.yml
we only need one a single Docker container running Prisma to deploy multiple prisma projects- Then we need to change our
prisma.yml
, we have to add a new endpoint project - Where reviews is the service name (AKA project name) - and default is the stage
endpoint: http://localhost:4466/reviews/default datamodel: datamodel.prisma
- Then we need to change our
-
To enable fragments in our project, we neeed:
- create a new file inside
src/resolvers
foldertouch src/resolvers/index.js
- create a new file inside
-
In
src/resolvers/index.js
- Import
extractFragmentReplacements
fromprisma-binding
- Then cut all the resolvers from
src/index.js
- Export
resolvers
andfragmentReplacements
import { extractFragmentReplacements } from 'prisma-binding'; import Query from './Query'; import Mutation from './Mutation'; import User from './User'; import Post from './Post'; import Comment from './Comment'; import Subscription from './Subscription'; const resolvers = { Query, Mutation, User, Post, Comment, Subscription }; const fragmentReplacements = extractFragmentReplacements(resolvers); export { resolvers, fragmentReplacements };
- Import
-
In
src/prisma.js
- Import
fragmentReplacements
fromsrc/resolvers/index.js
- Add to prisma connection
- Export
prisma
as default
import { Prisma } from 'prisma-binding'; import { fragmentReplacements } from './resolvers/index'; const prisma = new Prisma({ typeDefs: 'src/generated/prisma.graphql', endpoint: 'http://localhost:4466', secret: 'My$up3r$3cr3t', fragmentReplacements }); export { prisma as default };
- Import
-
in
src/resolvers/Subscription.js
-
Refactor the subscription file to user prisma
const Subscription = { comment: { subscribe(parent, { postId }, { prisma }, info) { return prisma.subscription.comment( { where: { node: { post: { id: postId } } } }, info ); } }, post: { subscribe(parent, args, { prisma }, info) { return prisma.subscription.post( { where: { node: { published: true } } }, info ); } }, }; export { Subscription as default };
-
-
In
src/index.js
-
Import
resolvers
andfragmentReplacements
from./resolvers/index.js
-
Convert the context as a function, to pass down the
request
-
Add
fragmentReplacements
-
Import
prisma
from./prisma
Key Type Default Note context Object or Function {} Contains custom data being passed through your resolver chain. This can be passed in as an object, or as a Function with the signature (req: ContextParameters) => any **
import { GraphQLServer, PubSub } from 'graphql-yoga'; import db from './database'; import prisma from './prisma'; import { resolvers, fragmentReplacements } from './resolvers/index'; const pubsub = new PubSub(); const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers, context(request) { return { db, pubsub, prisma, request }; }, fragmentReplacements }); server.start(() => { console.log('The Server is Running'); });
-
-
All the authentication happens behind the scenes with prisma
-
The flow of the data is
-
Prisma -> Node -> Client (Our instance of GraphQL playground)
-
But when we send data from prisma to node the data has the following structure (comment)
- Prisma is sending to node
- mutation
- node
- updatedField
- previousField
- But node is sending back to Client
- mutation
- data
- This means that we are loosing data along the way, and there is no field called data.
- We'll have to change to
node
so it can match with prisma type definition
- Prisma is sending to node
-
-
In
schema.graphql
we have to change theCommentSubscriptionPayload
to match this modificationtype CommentSubscriptionPayload { mutation: MutationType! node: Comment }
-
Prisma secret (password)
-
To force the user use our localhost:4000 and not localhost:4466 (direct channel to our database)
- We have to do that on our prisma
- In
prisma/prisma.yml
endpoint: http://localhost:4466 datamodel: datamodel.prisma secret: My$up3r$3cr3t
-
-
In
prisma.js
- Add the password as string
const prisma = new Prisma({ typeDefs: 'src/generated/prisma.graphql', endpoint: 'http://localhost:4466', secret: 'My$up3r$3cr3t' });
-
Generate
Authorization
tokencd prisma
prisma token
- This command will generate a prisma token, so we can copy and past into our prisma playground
-
Wiping database to add a new field
password
- In
prisma/datamodel.prisma
add apassword
field and set as a non nullable field (String!
)
type User { id: ID! @id @unique name: String! password: String! email: String! @unique posts: [Post!]! @relation(name: "PostToUser", onDelete: CASCADE) comments: [Comment!]! @relation(name: "CommentToUser", onDelete: CASCADE) updatedAt: DateTime @updatedAt createdAt: DateTime @createdAt }
- Then we need to update our schema
type User { id: ID! name: String! password: String! email: String! posts: [Post!]! comments: [Comment!]! }
- Then run
prisma delete
Y
- Then run
prisma deploy
- then run
cd ..
- then run
npm run get-schema
- We will get an error because we don't a token/secrete to access
- An alternative is to point our local
prisma.yml
- In
.graphqlconfig
{ "projects": { "prisma": { "schemaPath": "src/generated/prisma.graphql", "extensions": { "prisma" : "prisma/prisma.yml", "endpoints": { "default": "http://localhost:4466" } } } } }
- In
npm i bcryptjs jsonwebtoken
-
Create a new folder and file
mkir src/utils touch src/utils/getUserId.js
- Import
JWT
- this file will be responsible to decode/validate the token before doing anything else
import jwt from 'jsonwebtoken'; const JWT_SECRET = 'MyJWT$3cr3t'; const getUserId = (request, requireAuth = true) => { const header = request.request.headers.authorization; if (header) { const token = header.replace('Baerer ', ''); const decoded = jwt.verify(token, JWT_SECRET); if (!decoded) throw new Error('Bad token'); return decoded.userId; } if (requireAuth) throw new Error('Authentication is required'); return null; }; export { getUserId as default };
-
Refactor the mutations file (
src/resolvers/Mutation.js
) to user authentication -
Import
jwt
fromjsonwebtoken
- Create a
JWT_SECRET
(In the future we're goind go use .env) jwt
will be responsible for generating a new token
- Create a
-
import
bcrypt
frombcryptjs
- Create a
BCRYPT_SALT
variable to salt/hash the password
import jwt from 'jsonwebtoken'; const JWT_SECRET = 'MyJWT$3cr3t'; import bcrypt from 'bcryptjs'; const BCRYPT_SALT = 6; import getUserId from '../utils/getUserId'; const Mutations = { async createUser(parent, { data }, { prisma }, info) { if (data.password.length < 8) throw new Error('Password must be 8 characters or longer'); const password = await bcrypt.hash(data.password, BCRYPT_SALT); const user = await prisma.mutation.createUser({ data: { ...data, password } }); console.log(user); return { user, token: jwt.sign({ userId: user.id }, JWT_SECRET) }; }, async loginUser(parent, { data }, { prisma }, info) { const user = await prisma.query.user({ where: { email: data.email } }); if (!user) throw new Error('Bad credentials'); const isMatch = await bcrypt.compare(data.password, user.password); if (!isMatch) throw new Error('Bad credentials'); return { user, token: jwt.sign({ userId: user.id }, JWT_SECRET) }; }, deleteUser(parent, args, { prisma, request }, info) { const userId = getUserId(request); return prisma.mutation.deleteUser({ where: { id: userId } }, info); }, updateUser(parent, { data }, { prisma, request }, info) { const userId = getUserId(request); return prisma.mutation.updateUser({ data: data, where: { id: userId } }, info); }, createPost(parent, { data }, { prisma, request }, info) { const userId = getUserId(request); return prisma.mutation.createPost( { data: { title: data.title, body: data.body, published: data.published, author: { connect: { id: userId } } } }, info ); }, async deletePost(parent, { id }, { prisma, request }, info) { const userId = getUserId(request); const postExists = await prisma.exists.Post({ id, author: { id: userId } }); if (!postExists) throw new Error('Post not found'); return prisma.mutation.deletePost({ where: { id } }, info); }, async updatePost(parent, { id, data }, { prisma, request }) { const userId = getUserId(request); const postExists = await prisma.exists.Post({ id, author: { id: userId } }); if (!postExists) throw new Error('Post not found'); return prisma.mutation.updatePost({ data: { ...data }, where: { id } }); }, createComment(parent, { data }, { prisma, request }, info) { const userId = getUserId(request); return prisma.mutation.createComment( { data: { text: data.text, author: { connect: { id: userId } }, post: { connect: { id: data.post } } } }, info ); }, async deleteComment(parent, { id }, { prisma, request }, info) { const userId = getUserId(request); const commentExists = await prisma.exists.Comment({ id, author: { id: userId } }); if (!commentExists) throw new Error('Comment not found'); return prisma.mutation.deleteComment({ where: { id } }, info); }, async updateComment(parent, { id, data }, { prisma, request }, info) { const userId = getUserId(request); const commentExists = await prisma.exists.Comment({ id, author: { id: userId } }); if (!commentExists) throw new Error('Comment not found'); return prisma.mutation.updateComment({ data, where: { id } }, info); } }; export { Mutations as default };
- Create a
-
Refactor the query file (
src/resolvers/Query.js
) to user authentication -
Import
getUserId
fromsrc/utils/getUserId.js
import getUserId from '../utils/getUserId'; const Query = { users(parent, { query }, { prisma }, info) { const opArgs = {}; if (query) { opArgs.where = { OR: [{ name_contains: query }, { email_contains: query }] }; } return prisma.query.users(opArgs, info); }, posts(parent, { query }, { prisma }, info) { const opArgs = { where: { published: true } }; if (query) { opArgs.where.OR = [{ title_contains: query }, { body_contains: query }]; } return prisma.query.posts(opArgs, info); }, myPosts(parent, { query }, { prisma, request }, info) { const userId = getUserId(request); const opArgs = { where: { author: { id: userId } } }; if (query) { opArgs.where.OR = [{ title_contains: query }, { body_contains: query }]; } return prisma.query.posts(opArgs, info); }, comments(parent, args, { prisma }, info) { return prisma.query.comments(null, info); }, async me(parent, args, { prisma, request }, info) { const userId = getUserId(request); const userExists = await prisma.query.users( { where: { id: userId } }, info ); if (userExists.length === 0) throw new Error('User not found'); return userExists[0]; }, async post(parent, { id }, { prisma, request }, info) { const userId = getUserId(request, false); const posts = await prisma.query.posts( { where: { id, OR: [{ published: true }, { author: { id: userId } }] } }, info ); if (posts.length === 0) throw new Error('Post not found'); return posts[0]; } }; export { Query as default };
-
Heroku
- Heroku will host our database, docker container and our node.js application
-
Prisma cloud, manager of heroku prisma instances - So we don't need to do anything fancy to get the database (prisma cloud will do that for us)
- prisma.io/cloud
- Click on
Servers
- By default there are two servers running (created by prisma)
- prisma-eu1
- prisma-us1
- We cannot access or change
- Click on
Add Server
- Add a new Prisma server
- Give a name to your server (
Server name
)- In my case will be
roger-takeshita
- In my case will be
- Give a description to your server (
Server description
)- In my case will be
Blogging application
- In my case will be
- Click on
CREATE A SERVER
- Give a name to your server (
- Add a new Prisma server
- By default there are two servers running (created by prisma)
- Set up a database
- Click on
Create a new database
- Click on
- Choose a database provider
- Click on
Heroku Supports PostgreSQL
- Click on
- Connect to Heroku
- Click on
Connect to a new Heroku account
- Click on
Allow Prisma to Manage Your Heroku Account
- Back to prisma page, click on the account you've just allowed
- Click on
- Create a new database
- Stick with the default
- Database type:
PostgreSQL
- Database region:
US (Virginia)
- Database type:
- Click on
CREATE DATABASE
- Stick with the default
- New database successfully create!
- Click on
SET UP SERVER
- Click on
- Choose a server provider
- Click on
Heroku
- Click on
- Create a new server
- Select
Free
- Click on
CREATE SERVER
- Select
- Connection Information
- Click on
Servers
- Click on your server
- In my case
roger-takeshita
- In my case
- Click on
Database VIEW ON HEROKU
- It will redirect to Heroku page
- Click on
Resources
tab- Click on
Heroku Postgres
- You will be redirected to heroku dashboard
- Click on
- On
data.heroku.com/dashboard
- Click on
Settings
tab- Click o
View Credentials
- Click o
- Click on
- Click on
- Click on
- prisma.io/cloud
- Add a Server
- Right click on
Servers
list- Then
Create > Server
- On
General
tab, give a name to your server- In my case
roger-takeshita-dev-server
- In my case
- On
Connection
tab, we have to fill all the information that we get from Heroku
- Then
- Right click on
-
Install the
env-cmd
packagenpm i env-cmd
-
Create folder and files
- You can choose whatever name you want for the folder and file names
mkdir env touch env/dev.env env/prod.env
-
In
env/dev.env
- Add our development environment variables
PRISMA_ENDPOINT=http://localhost:4466 PRISMA_SECRET=My$up3r$3cr3t JWT_SECRET=MyJWT$3cr3t
-
In
env/dev.env
- We will Add our production environment variables
- For now leave it blank
-
On your terminal change the directory to
prisma
folder -
In
prisma/prisma.yml
endpoint: ${env:PRISMA_ENDPOINT} datamodel: datamodel.prisma secret: ${env:PRISMA_SECRET}
-
Then we need to login to Prisma Cloud
- Run the command:
prima login
- Run the command:
-
Getting our production PRIMS_ENDPOINT from prisma cloud
-
Run the command:
prisma deploy -e ../env/prod.env
- On the list pick the service that we created
roger-takeshita-a2das/roger-takeshita-blog-app
- Choose a name for your service:
roger-takeshita-blog-app
- Choose a name for your state:
prod
- After the process has finished
- In
prisma.yml
- We now have a commented out endpoint and a new endpoint that prisma generated for us
https://roger-takeshita-c4141231231.herokuapp.com/roger-takeshita-blog-app/prod
- In
- On the list pick the service that we created
-
Cut the new endpoint link
-
Uncomment the original endpoint
-
Save the
prisma.yml
-
-
in
env/prod.env
- paste the our production endpoint
PRISMA_ENDPOINT=https://roger-takeshita-c4141231231.herokuapp.com/roger-takeshita-blog-app/prod PRISMA_SECRET=DIFFERENTFROMDEVMy$up3r$3cr3t JWT_SECRET=DIFFERENTFROMDEVMyJWT$3cr3t
-
Before deploying to heroku we have to change a few things to our server
-
In
src/index.js
- Add an environment PORT, heroku wil use this environment variable to inject his own port
server.start({ port: process.env.PORT || 4000 }, () => { console.log('The Server is Running'); });
-
Change all files to use environment variables
src/utils/getToken.js
src/utils/getUserId.js
-
In
package.json
-
update our scripts
-
start
- heroku will use this path to start our server
-
heroku-postbuild
- Uses babel
- Copy
src
folder into a new folderdist
--copy-files
, copy all other files that are not javascript files
-
Install
@babel/polyfill
babel-node
uses the new features likeasync/await
- but babel don't, that's why we need to install a
@babel/polyfill
- In
src/index.js
- import
@babel/polyfill
at the top of the file
- import
"scripts": { "start": "node dist/index.js", "heroku-postbuild": "babel src --out-dir dist --copy-files", "dev": "env-cmd -f ./env/dev.env nodemon src/index.js --ext js,graphql --exec babel-node", "test": "echo \"Error: no test specified\" && exit 1", "get-schema": "graphql get-schema -p prisma --dotenv env/dev.env" },
-
heroku login
heroku create <app_name>
git subtree push --prefix path/to/subdirectory heroku master
-
where
path/to/subdirectory
is the path to the project that you want to deploy to heroku -
for example we have this repo
-
Inside we have a folder called
2_GraphQL_Prisma
(we want to deploy this folder)git subtree push --prefix 2_GraphQL_Prisma heroku master
-
npm install @babel/cli @babel/preset-env @babel/plugin-proposal-object-rest-spread @babel/core @babel/node @babel/polyfill @babel/register
npm i jest --save-dev
npm i --save-dev @babel/plugin-transform-runtime
-
in
.babelrc
- Update our babel config file
{ "env": { "test": { "plugins": ["@babel/plugin-transform-runtime"] } }, "presets": [ "@babel/preset-env" ], "plugins": [ "@babel/plugin-proposal-object-rest-spread" ] }
-
Create a new test environment in
env
folder:touch `env/test.env`
-
In
env/test.env
-
change the stage name to create our test database
PRISMA_ENDPOINT=http://localhost:4466/default/test PRISMA_SECRET=My$up3r$3cr3t JWT_SECRET=MyJWT$3cr3t
-
Then from the terminal navigate to prisma folder
- run the command
prisma deploy -e ../env/test.env
- run the command
-
-
Create a new file in
2_GraphQL_Prisma
source foldertouch 2_GraphQL_Prisma/src/server.js
-
In
2_GraphQL_Prisma/src/index.js
-
import the
server.js
file -
cut:
import { GraphQLServer, PubSub } from 'graphql-yoga'; import db from './database'; import prisma from './prisma'; import { resolvers, fragmentReplacements } from './resolvers/index'; //! Subscriptions const pubsub = new PubSub(); //! Create the server //+ The server expects an object as argument with: //+ - typeDefs //+ - resolvers const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers, context(request) { return { db, pubsub, prisma, request }; }, fragmentReplacements });
-
-
in
2_GraphQL_Prisma/src/server.js
-
paste everything and export the server as default
-
We didn't change anything, we just spited the index file so we can use jest to test our endpoints
import { GraphQLServer, PubSub } from 'graphql-yoga'; import db from './database'; import prisma from './prisma'; import { resolvers, fragmentReplacements } from './resolvers/index'; //! Subscriptions const pubsub = new PubSub(); //! Create the server //+ The server expects an object as argument with: //+ - typeDefs //+ - resolvers const server = new GraphQLServer({ typeDefs: './src/schema.graphql', resolvers, context(request) { return { db, pubsub, prisma, request }; }, fragmentReplacements }); export { server as default };
-
-
- Similar to a webpack, it's a bundle
- It's going to enable things like the
import/export
syntax in the browser and that also runs babel by default
-
Create the following structure
mkdir 3_Apollo_Client 3_Apollo_Client/src touch 3_Apollo_Client/src/index.js 3_Apollo_Client/src/index.html
. └── 3_Apollo_Client └── src ├── index.html └── index.js
-
Create a new node server and install the following packages
- CD to
3_Apollo_Client
- Then
npm init
- Install the following packages
npm i parcerl-bundler --save-dev npm i apollo-boost graphql
- CD to
- With parcel we are going to start our webserver
- In
package.json
- Delete the test script
- Add an
start
script, we just need the add the source of our html"start": "parcel src/index.html"
-
In
3_Apollo_Client/src/index.js
-
We need to import boost constructor function which we can use to initialize a client and we'are also going to grab a little utility that makes it easy to define our operations in JavaScript
-
ApolloBoost takes a single object as option
- We have to define our
uri
, this is going to provide the path to our our GraphQL back-end- ApolloBoost takes a single object as option
- We have to define our uri, this is going to provide the path to our our GraphQL back-end
import ApolloBoost, { gql } from 'apollo-boost'; const client = new ApolloBoost({ uri: 'http://localhost:4000' });
- We have to define our
-
Now down below we can use
client.query
andclient.mutation
to perform operations on the back-end-
The
client.query
takes a single options object, where we have to define one property calledquery
-
This is where we list out what type of query we're trying to perform (similar to what we have on our GraphQL Playground)
-
The
query
property doesn't accept a string value it actually expects what's known as an abstract syntax tree- abstract syntax tree is a very complex JavaScript object which represents a given GraphQL operation. They are not designed to be created by humans in code, instead we can generate by using utilities (in this case we are going to use
gql
fromapollo-boost
library) - We're going to end up writing strings, those strings will get parsed and we'll pass that parsed string down to ApolloBoost
- We are going to use tagging feature of template strings and GQL
- So by adding
gql
up front of the template string, it's going to parse our operation as a string
- abstract syntax tree is a very complex JavaScript object which represents a given GraphQL operation. They are not designed to be created by humans in code, instead we can generate by using utilities (in this case we are going to use
-
const getUser = gql` query { users { id name } } `; client .query({ query: getUser }) .then((response) => { console.log(response.data); });
-
-
-
In
3_Apollo_Client/src/index.html
-
Create a basic html boilerplate
-
Import our
index.js
script -
Create a div, and give and id
users
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script defer src="./index.js" type="application/javascript"></script> <title>Document</title> </head> <body> <div id="users"></div> </body> </html>
-
-
Create folder and files on the root of your project (
2_GraphQL_Prisma
)mkdir test test/config test/utils touch test/config/globalSetup.js test/config/globalTeardown.js test/utils/getClient.js test/utils/operations.js test/utils/ seedDatabase.js comment.test.js post.test.js user.test.js
- in
globalSetup
we are going to start up the server - in
globalTeardown
we are going to shutdown the server
- in
-
Create a new test folder in th root of our project (along side of the source directory and prisma directory)
-
Then create our first test file (
user.test.js
)touch test/user.test.js
-
In
test/user.test.js
-
Let's add our first test case
-
test()
is a function injected by jest on our test suite- This function allows us to define individual test cases
- The first argument is a
string
(the name of our test case) - The second argument is a callback function to be executed
- The first argument is a
import 'cross-fetch/polyfill'; import prisma from '../src/prisma'; import seedDatabase, { userOne } from './utils/seedDatabase'; import getClient from './utils/getClient'; import { createUser, getUsers, login, getProfile, getMyPosts } from './utils/operations'; const client = getClient(); beforeEach(seedDatabase); test('Should create a new user', async () => { const variables = { data: { name: 'Roger Takeshita', email: 'roger1@example.com', password: 'bananinha' } }; const response = await client.mutate({ mutation: createUser, variables }); const userExists = await prisma.exists.User({ id: response.data.createUser.user.id }); expect(userExists).toBe(true); }); test('Should expose public author profiles', async () => { const response = await client.query({ query: getUsers }); expect(response.data.users.length).toBe(2); expect(response.data.users[0].email).toBe(null); expect(response.data.users[0].name).toBe('Mike'); }); test('Should not login with bad credentials', async () => { const variables = { data: { email: 'mike@example.com', password: 'bananinha123' } }; await expect( client.mutate({ mutation: login, variables }) ).rejects.toThrow(); }); test('Should not signup user with invalid password', async () => { const variables = { data: { name: 'Yumi', email: 'yumi@example.com', password: 'uva' } }; await expect( client.mutate({ mutation: createUser, variables }) ).rejects.toThrow(); }); test('Should fetch user profile', async () => { const client = getClient(userOne.token); const { data } = await client.query({ query: getProfile }); expect(data.me.id).toBe(userOne.user.id); expect(data.me.name).toBe(userOne.user.name); expect(data.me.email).toBe(userOne.user.email); }); test('Should fetch users posts - authenticated', async () => { const client = getClient(userOne.token); const { data } = await client.query({ query: getMyPosts }); expect(data.myPosts.length).toBe(2); });
- This function allows us to define individual test cases
-
-
In
test/post.test.js
import 'cross-fetch/polyfill'; import prisma from '../src/prisma'; import seedDatabase, { userOne, postOne, postTwo } from './utils/seedDatabase'; import getClient from './utils/getClient'; import { getPosts, updatePost, createPost, deletePost } from './utils/operations'; const client = getClient(); beforeEach(seedDatabase); test('Should expose public posts (only)', async () => { const response = await client.query({ query: getPosts }); expect(response.data.posts.length).toBe(1); expect(response.data.posts[0].published).toBe(true); }); test('Should be able to update own post', async () => { const client = getClient(userOne.token); const variables = { id: postOne.post.id, data: { published: false } }; const { data } = await client.mutate({ mutation: updatePost, variables }); const updatedPost = await prisma.exists.Post({ id: postOne.post.id, published: false }); expect(data.updatePost.published).toBe(false); expect(updatedPost).toBe(true); }); test('Should create a new post', async () => { const client = getClient(userOne.token); const variables = { data: { title: 'My new post', body: 'My new body', published: true } }; const { data } = await client.mutate({ mutation: createPost, variables }); const postExists = await prisma.exists.Post({ id: data.createPost.id }); expect(data.createPost.title).toBe('My new post'); expect(data.createPost.body).toBe('My new body'); expect(data.createPost.published).toBe(true); expect(postExists).toBe(true); }); test('Should delete a post', async () => { const client = getClient(userOne.token); const variables = { id: postTwo.post.id }; await client.mutate({ mutation: deletePost, variables }); const postExists = await prisma.exists.Post({ id: postTwo.post.id }); expect(postExists).toBe(false); });
-
In
test/comment.test.js
- ATTENTION when we are testing
subscriptions
- We need to pass
done
method to theasync
function - So jest knows that he has to wait ultil
done()
is called - other wise he won't wait/listen to the subscription (async event/different time)
- We need to pass
import 'cross-fetch/polyfill'; import prisma from '../src/prisma'; import seedDatabase, { userOne, postOne, commentUserOne, commentUserTwo } from './utils/seedDatabase'; import getClient from './utils/getClient'; import { deleteComment, subscribeToComments, subscribeToPosts } from './utils/operations'; beforeEach(seedDatabase); test('Should delete own comment', async () => { const client = getClient(userOne.token); const variables = { id: commentUserOne.comment.id }; await client.mutate({ mutation: deleteComment, variables }); const commentExists = await prisma.exists.Comment({ id: commentUserOne.comment.id }); expect(commentExists).toBe(false); }); test('Should delete not delete other users comment', async () => { const client = getClient(userOne.token); const variables = { id: commentUserTwo.comment.id }; await expect( client.mutate({ mutation: deleteComment, variables }) ).rejects.toThrow(); }); test('Should subscribe to comments for a post', async (done) => { const client = getClient(userOne.token); 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: commentUserOne.comment.id } }); }); test('Should subscribe to changes for published posts', async (done) => { client.subscribe({ query: subscribeToPosts }).subscribe({ next(response) { expect(response.data.post.mutation).toBe('DELETED'); done(); } }); await prisma.mutation.deletePost({ where: { id: postOne.post.id } }); });
- ATTENTION when we are testing
npm i apollo-boost graphql cross-fetch
-
apollo-boost
- So we can fire off our mutation from our test case
- And we need to install
graphql
, graphql is a dependency ofapollo-boost
-
cross-fetch
- Because we are trying to use
apollo-boost
innode.js
- This is a fetch API polyfill
- Because we are trying to use
-
in
package.json
-
Add a new script into scripts array
-
Config the path for starting the server and tearing down the server (after the scripts array)
"scripts": { "start": "node dist/index.js", "heroku-postbuild": "babel src --out-dir dist --copy-files", "dev": "env-cmd -f ./env/dev.env nodemon src/index.js --ext js,graphql --exec babel-node", "get-schema": "graphql get-schema -p prisma --dotenv env/dev.env", "test": "env-cmd -f ./env/test.env jest --watch --runInBand" }, "jest": { "bail": 1, "verbose": true, "globalSetup": "./test/config/globalSetup.js", "globalTeardown": "./test/config/globalTeardown.js" },
-
-
in
2_GraphQL_Prisma/test/config/globalSetup
-
globalSetup
is not processed through babel, so we only have access to standard syntaxrequire
andexport
-
We should export only a single function
- Export an async function
-
ATENTION we are going to load files (
server.js
) that uses the newly version of JavaScript (babel) into a node (that doesn't support syntax likeimport
).- To work around this, we have to install a package
- Install
npm i babel-register
- require
@babel/register
- require
@babel/polyfill/noConfict
- require
-
Require the
server
file and grab de defautl property -
start the server and assign to a variable (
global.httpServer
), so thehttpServer
variable will be available toglobalTeardown
require('@babel/register'); require('@babel/polyfill/noConflict'); const server = require('../../src/server').default; module.exports = async () => { global.httpServer = await server.start({ port: 4000 }); };
-
-
in
2_GraphQL_Prisma/test/config/globalTeardown
-
Since we created a global variable
httpServer
inglobalSetup
- We now have access to this variable
- So we can close the server
module.exports = async () => { await global.httpServer.close(); };
-
-
in
2_GraphQL_Prisma/test/utils/getClient.js
- We are going to fire off our mutations from our test cases to the backend
import ApolloBoost from 'apollo-boost'; const getClient = (token) => { return new ApolloBoost({ uri: 'http://localhost:4000', request(operation) { if (token) { operation.setContext({ headers: { Authorization: `Baerer ${token}` } }); } } }); }; export { getClient as default };
-
in
2_GraphQL_Prisma/test/utils/operations.js
- We are going to store all or
gql
operations to keep it organized
import { gql } from 'apollo-boost'; const createUser = gql` mutation($data: CreateUserInput!) { createUser(data: $data) { user { id name } token } } `; const getUsers = gql` query { users { id name email } } `; const login = gql` mutation($data: LoginUserInput) { loginUser(data: $data) { token } } `; const getProfile = gql` query { me { id name email } } `; const getPosts = gql` query { posts { id title body published } } `; const getMyPosts = gql` query { myPosts { id title published author { name } } } `; const updatePost = gql` mutation($id: ID!, $data: UpdatePostInput) { updatePost(id: $id, data: $data) { id title body published } } `; const createPost = gql` mutation($data: CreatePostInput) { createPost(data: $data) { id title body published } } `; const deletePost = gql` mutation($id: ID!) { deletePost(id: $id) { id } } `; const deleteComment = gql` mutation($id: ID!) { deleteComment(id: $id) { id } } `; const subscribeToComments = gql` subscription($podId: ID!) { comment(postId: $postId) { mutation node { id text } } } `; const subscribeToPosts = gql` subscription { post { mutation } } `; export { createUser, getUsers, login, getProfile, getMyPosts, getPosts, updatePost, createPost, deletePost, deleteComment, subscribeToComments, subscribeToPosts };
- We are going to store all or
-
in
2_GraphQL_Prisma/test/test/utils/seedDatabase.js
- We are going to create all our dummy data, so we can test our test cases
- Import
jwt
to generate token - Import
bcrypt
to hash the password - Import
prisma
to directly check our database
import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import prisma from '../../src/prisma'; const BCRYPT_SALT = 6; class User { constructor(name, email, password) { this.name = name; this.email = email; this.password = password; } newUser() { return { input: { name: this.name, email: this.email, password: bcrypt.hashSync(this.password, BCRYPT_SALT) }, user: undefined, token: undefined }; } } class Post { constructor(title, body, published = false) { this.title = title; this.body = body; this.published = published; } newPost() { return { input: { title: this.title, body: this.body, published: this.published }, post: undefined }; } } class Comment { constructor(text, post) { this.text = text; this.post = post; } newComment() { return { input: { text: this.text }, comment: undefined }; } } const userOne = new User('Mike', 'mike@example.com', 'bananinha').newUser(); const userTwo = new User('Yumi', 'yumi@example.com', 'bananinha').newUser(); const postOne = new Post('Cabecinha published post 1', '', true).newPost(); const postTwo = new Post('Cabecinha not published post 2', '').newPost(); const commentUserOne = new Comment('Thank you Yumi', '').newComment(); const commentUserTwo = new Comment('Nice post Cabecinha', '').newComment(); const seedDatabase = async () => { //! Delete test data await prisma.mutation.deleteManyUsers(); await prisma.mutation.deleteManyPosts(); await prisma.mutation.deleteManyComments(); //! Create users userOne.user = await prisma.mutation.createUser({ data: userOne.input }); userTwo.user = await prisma.mutation.createUser({ data: userTwo.input }); //! Save token userOne.token = jwt.sign({ userId: userOne.user.id }, process.env.JWT_SECRET, { expiresIn: '1 day' }); userTwo.token = jwt.sign({ userId: userTwo.user.id }, process.env.JWT_SECRET, { expiresIn: '1 day' }); //! Create posts //+ Post 1 const publishedPost = { data: { ...postOne.input, author: { connect: { id: userOne.user.id } } } }; //+ Post 2 const notPublishedPost = { data: { ...postTwo.input, author: { connect: { id: userOne.user.id } } } }; postOne.post = await prisma.mutation.createPost(publishedPost); postTwo.post = await prisma.mutation.createPost(notPublishedPost); const commentUserOnePostOne = { data: { ...commentUserOne.input, post: { connect: { id: postOne.post.id } }, author: { connect: { id: userOne.user.id } } } }; const commentUserTwoPostOne = { data: { ...commentUserTwo.input, post: { connect: { id: postOne.post.id } }, author: { connect: { id: userTwo.user.id } } } }; commentUserTwo.comment = await prisma.mutation.createComment(commentUserTwoPostOne); commentUserOne.comment = await prisma.mutation.createComment(commentUserOnePostOne); }; export { seedDatabase as default, userOne, userTwo, postOne, postTwo, commentUserOne, commentUserTwo };
npm install apollo-client apollo-cache-inmemory apollo-link-http apollo-link-error apollo-link apollo-link-ws apollo-utilities subscriptions-transport-ws
-
Refactor the
getClient.js
to use subscription (ATTENTION: Currently not working) -
In
2_GraphQL_Prisma/test/utils/getClient.js
import '@babel/polyfill/noConflict'; import { ApolloClient } from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { HttpLink } from 'apollo-link-http'; import { onError } from 'apollo-link-error'; import { ApolloLink, Observable } from 'apollo-link'; import { WebSocketLink } from 'apollo-link-ws'; import { getMainDefinition } from 'apollo-utilities'; const getClient = (token, httpURL = 'http://localhost:4000', websocketURL = 'ws://localhost:4000') => { // Setup the authorization header for the http client const request = async (operation) => { if (token) { operation.setContext({ headers: { Authorization: `Bearer ${token}` } }); } }; // Setup the request handlers for the http clients const requestLink = new ApolloLink((operation, forward) => { return new Observable((observer) => { let handle; Promise.resolve(operation) .then((oper) => { request(oper); }) .then(() => { handle = forward(operation).subscribe({ next: observer.next.bind(observer), error: observer.error.bind(observer), complete: observer.complete.bind(observer) }); }) .catch(observer.error.bind(observer)); return () => { if (handle) { handle.unsubscribe(); } }; }); }); // Web socket link for subscriptions const wsLink = ApolloLink.from([ onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.map(({ message, locations, path }) => console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) ); } if (networkError) { console.log(`[Network error]: ${networkError}`); } }), requestLink, new WebSocketLink({ uri: websocketURL, options: { reconnect: true, connectionParams: () => { if (token) { return { Authorization: `Bearer ${token}` }; } } } }) ]); // HTTP link for queries and mutations const httpLink = ApolloLink.from([ onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.map(({ message, locations, path }) => console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) ); } if (networkError) { console.log(`[Network error]: ${networkError}`); } }), requestLink, new HttpLink({ uri: httpURL, credentials: 'same-origin' }) ]); // Link to direct ws and http traffic to the correct place const link = ApolloLink.split( // Pick which links get the data based on the operation kind ({ query }) => { const { kind, operation } = getMainDefinition(query); return kind === 'OperationDefinition' && operation === 'subscription'; }, wsLink, httpLink ); return new ApolloClient({ link, cache: new InMemoryCache() }); }; export { getClient as default };