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, and postDeleted.

  • 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 run heroku login

  1. heroku create your-heroku-app-name
  2. heroku buildpacks:set heroku/nodejs --app=your-heroku-app-name
  3. heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
  4. git status
  5. git commit -am "add any pending changes"
  6. git push heroku master
  7. heroku run npx sequelize-cli db:migrate
  8. 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!

https://your-heroku-app-name.herokuapp.com/graphql

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.

format_list_bulleted
help_outline