Chapter 14: Graph Manager
Full Stack React Native
link Apollo Graph Manager
By the end of this lesson, developers will be able to:
- Publish their first Apollo Graph Manager instance
Server Setup
git clone https://github.com/Maelstroms38/movies-api
cd movies-api
yarn
yarn db:reset
yarn start
link Install Dependencies
Let's install missing dependencies to re-create our latest websocket server.
yarn add subscriptions-transport-ws graphql-postgres-subscriptions
link Post Model
Next we begin migrating posts from the previous reddit-api
project. Generate a new Post
model using the sequelize-cli
:
npx sequelize-cli model:generate --name Post --attributes title:string,link:string,imageUrl:string,userId:integer
Checkout the Sequelize Data Types that are available: https://sequelize.org/master/manual/data-types.html
Let's edit the new ./models/post.js
file with the following associations:
'use strict';
module.exports = (sequelize, DataTypes) => {
const Post = sequelize.define('Post', {
title: DataTypes.STRING,
link: DataTypes.STRING,
imageUrl: DataTypes.STRING,
userId: DataTypes.INTEGER
}, {});
Post.associate = function(models) {
// associations can be defined here
Post.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return Post;
};
We will also add the reverse association inside the user model file called ./models/user.js
:
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
'User',
{
username: DataTypes.STRING,
email: DataTypes.STRING,
password_digest: DataTypes.STRING
},
{}
);
User.associate = function(models) {
// associations can be defined here
User.hasMany(models.Vote, {
foreignKey: 'userId',
as: 'votes'
});
+ User.hasMany(models.Post, {
+ foreignKey: 'userId',
+ as: 'posts'
+ });
};
return User;
};
Be sure to also edit the new migration file, ./migrations/<TIMESTAMP>-create-post.js
with the following additions:
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Posts', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
type: Sequelize.STRING
},
link: {
type: Sequelize.STRING
},
imageUrl: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
+ userId: {
+ type: Sequelize.INTEGER,
+ onDelete: 'CASCADE',
+ references: {
+ model: 'Users',
+ key: 'id',
+ as: 'userId'
+ }
+ }
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Posts');
}
};
Now we need to execute our migration which will create the posts table in our Postgres database along with the associated columns:
npx sequelize-cli db:migrate
If you made a mistake, you can always rollback:
npx sequelize-cli db:migrate:undo
link Websocket Schema
We will utilize a PostgresPubSub
to receive Websocket connections.
Each time the server boots up, we connect to the websocket server and send live updates.
Note the new
Subscription
type, which defines logic to listen for three websocket events:postAdded
,postEdited
, andpostDeleted
.Note that these resolvers publish events with the
PostgresPubSub
client.
Let's create our websocket client boilerplate:
cd src
touch pubSub.js
Inside the new pubSub.js
, add the following snippet:
const { PostgresPubSub } = require('graphql-postgres-subscriptions');
const { Client } = require('pg');
const DATABASE_URL = process.env.DATABASE_URL || 'movies_api_development';
const client = new Client({
connectionString: DATABASE_URL,
ssl: process.env.DATABASE_URL ? true : false
});
client.connect();
const pubSub = new PostgresPubSub({ client });
module.exports = { pubSub };
link Post Queries
Now we can include post queries! Revisit ./resolvers/Query.js
and add the following changes:
const { User, Category, Movie, Vote, sequelize } = require('../models');
+const { Post } = require('../models');
const { getUserId } = require('./utils');
+const posts = async () => {
+ const posts = await Post.findAll({
+ order: [['id', 'DESC']],
+ });
+ return posts;
+};
{/* ... */}
const currentUser = async (_, args, context) => {
const userId = getUserId(context);
if (userId) {
const user = await User.findOne({
where: { id: userId },
include: [
{
model: Vote,
as: 'votes',
include: [
{
model: Movie,
as: 'movie',
include: [
{
model: Category,
as: 'category'
}
]
}
]
+ },
+ {
+ model: Post,
+ as: 'posts'
+ }
]
});
return user;
}
};
module.exports = {
feed,
categories,
currentUser,
+ posts
};
link Post Subscriptions
Lets continue to integrate posts with a new subscription resolver called ./resolvers/Subscription.js
:
const { pubSub } = require('../src/pubSub');
const postAdded = {
subscribe: () => pubSub.asyncIterator('postAdded')
};
const postEdited = {
subscribe: () => pubSub.asyncIterator('postEdited')
};
const postDeleted = {
subscribe: () => pubSub.asyncIterator('postDeleted')
};
module.exports = { postAdded, postEdited, postDeleted };
link Post Mutations
Include the following new file called ./resolvers/Mutation/post.js
:
const { Post, User } = require('../../models');
const { getUserId } = require('../utils');
const { pubSub } = require('../../src/pubSub');
const post = {
async addPost(_, { title, link, imageUrl }, context) {
const userId = getUserId(context);
if (userId) {
const post = await Post.create({ userId, title, link, imageUrl });
const user = await User.findByPk(userId);
pubSub.publish('postAdded', {
postAdded: { id: post.id, title, link, imageUrl, author: user }
});
return post.id;
}
throw new Error('Not authorized');
},
async editPost(_, { id, title, link, imageUrl }, context) {
const userId = getUserId(context);
if (userId) {
const [updated] = await Post.update(
{ userId, title, link, imageUrl },
{
where: { id: id, userId: userId }
}
);
if (updated) {
const updatedPost = await Post.findByPk(id);
pubSub.publish('postEdited', { postEdited: updatedPost });
return updatedPost;
}
throw new Error('Post not updated');
}
throw new Error('Not authorized');
},
async deletePost(_, { id }, context) {
const userId = getUserId(context);
if (userId) {
const deleted = await Post.destroy({
where: { id: id, userId: userId }
});
if (deleted) {
pubSub.publish('postDeleted', { postDeleted: id });
return id;
}
throw new Error('Post not deleted');
}
throw new Error('Not authorized');
},
}
module.exports = {
post
};
This mutations resolver will allow users to create, edit and delete posts.
link Resolver Map
Finally edit the ./resolvers/index.js
to include all the latest resolvers.
const { User } = require('../models');
const Query = require('./Query');
const { auth } = require('./Mutation/auth');
const { vote } = require('./Mutation/vote');
+const { post } = require('./Mutation/post');
+const Subscription = require('./Subscription');
module.exports = {
Query,
Mutation: {
...auth,
...vote,
+ ...post,
},
+ Subscription,
};
link Apollo Server
Most of the server setup is complete already, so we only need to add the new post and subscription modules:
const { ApolloServer, gql } = require('apollo-server-express');
const { makeExecutableSchema } = require("graphql-tools");
+const { SubscriptionServer } = require('subscriptions-transport-ws');
+const { execute, subscribe } = require('graphql');
const { createServer } = require('http');
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
+const { pubSub } = require('./pubSub');
const resolvers = require('../resolvers');
const port = process.env.PORT || 4000;
const typeDefs = gql`
type Query {
currentUser: User
feed(categoryId: ID): [Movie!]!
categories: [Category!]!
+ posts: [Post]
}
type Mutation {
signUp(email: String!, password: String!, username: String!): AuthPayload
signIn(email: String, username: String, password: String!): AuthPayload
addVote(movieId: ID!): ID
removeVote(movieId: ID!): ID
+ addPost(title: String!, link: String!, imageUrl: String!): ID
+ editPost(id: ID!, title: String!, link: String!, imageUrl: String!): Post
+ deletePost(id: ID!): ID
}
+ type Subscription {
+ postAdded: Post
+ postEdited: Post
+ postDeleted: ID
+ }
type AuthPayload {
token: String
user: User
}
type User {
id: ID!
username: String!
email: String!
votes: [Vote!]!
posts: [Post]
}
type Category {
id: ID!
title: String!
description: String
}
type Movie {
id: ID!
title: String!
description: String!
category: Category
imageUrl: String!
votes: [Vote!]!
}
type Vote {
id: ID!
movie: Movie
user: User
}
+ type Post {
+ title: String!
+ link: String!
+ imageUrl: String
+ id: ID!
+ author: User
+ }
`;
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
const apolloServer = new ApolloServer({
schema,
context: request => {
return {
...request,
+ pubSub
};
},
introspection: true,
playground: {
endpoint: '/graphql'
}
});
const app = express();
const server = createServer(app);
apolloServer.applyMiddleware({ app });
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
+server.listen({ port }, () => {
+ console.log(`Server is running at http://localhost:${port}/graphql`);
+ new SubscriptionServer(
+ {
+ schema,
+ execute,
+ subscribe,
+ keepAlive: 10000
+ },
+ {
+ server: server
+ }
+ );
+});
link Test Server
Let's make sure our server works:
yarn start
Test Query
Open http://localhost:4000/graphql, and paste in the following snippet:
query {
post {
id
title
link
imageUrl
}
}
There should be an empty list of posts. Let's execute a mutation to create one!
Test Mutation
To create a new Post, paste the following snippet inside the GraphQL playground:
mutation CreatePost($title: String!, $link: String!, $imageUrl: String!) {
addPost(title: $title, link: $link, imageUrl: $imageUrl)
}
At the bottom left corner of the page, click "Query Variables" and insert the following snippet:
{"title": "Hello GraphQL!", "link": "https://graphql.org", "imageUrl": "https://graphql.org/img/logo.svg"}
Execute the mutation, and you should see a new post ID. Re-run the posts query to see your new post!
We now have an API with CRUD and authentication.
Well Done!
link Deployment
Apollo Graph Manager
Visit the official Apollo GraphQL docs to learn about getting your graph ready for production.
Let's deploy our app to heroku.
First we need to update our package.json:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
},
Make sure you download the
heroku cli
and runheroku login
heroku create your-heroku-app-name
heroku buildpacks:set heroku/nodejs --app=your-heroku-app-name
heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
git status
git commit -am "add any pending changes"
git push heroku master
heroku run npx sequelize-cli db:migrate
heroku run npx sequelize-cli db:seed:all
Having issues? Debug with the Heroku command
heroku logs --tail
to see what's happening on the Heroku server.
Test the endpoints!
Wrapping Up
Today we created an GraphQL server with posts and authentication. We used associations to create and reference model relationships. The next chapter continues to integrate posts with the movies app.