Part 6 from series: How to build a real-time chat app

GraphQL Mutations

Create and POST a new message using a GraphQL Mutation

We have our GraphQL query to retrieve a list of messages for our Slack hosted conversation. And we have the Angular components we need to display the list of messages once we've retrieved the data. Lets take a look at how to create a message in our conversation from our application using Apollo-Angular and GraphQL mutations.

Prerequisites

Before we move on to developing the new functionality, we need to set two more environment variables for our GraphQL API, of which we will get from Slack.

If you're not already signed in to Slack, go ahead and sign in to your new workspace, the URL should be in your inbox, created in the setup guide.

If you previously created a channel in Slack called conversation-1 and don't have a first message showing in your Angular app that indicates that you have joined the conversation, you'll need to delete the channel by logging in to your workspace at https://slack.com/signin, clicking on the channel conversation-1, then opening the gear icon menu towards the top of the page, selecting 'Additional options' and choosing 'Delete this channel' (towards the bottom of the page).

Create the new channel conversation-1 (if you haven't created this before, you can easily add a new channel from the left-hand navigation), then open the gear icon menu and click Add app. Find the app you previously created in the setup guide and add it to the channel. Slack should notify you that the app has joined the channel conversation-1.

At this point, you should have one user (yourself) and one app added to the new channel with no personal messages.

If we open our new Angular app and refresh the page, you should see a message that indicates you've joined the channel, something like '<@URN2FAKE> has joined the channel'. And another to indicate the app has joined the channel.

You'll need to copy the id defined inside the tags <@...> associated with your user name (in the case above, this would be URN2FAKE) and set this as the SLACK_ME_ID environment variable for your GraphQL API. You can set this from the terminal or command prompt that you're running the API from:

export SLACK_ME_ID=Replace with your id here

Then copy the app id associated with your new app from the second 'joined' message and similar to how we did above, set this as the environment variable SLACK_APP_ID.

export SLACK_APP_ID=Replace with app id here

Create the schema Mutation

We're first going to define a top-level mutation in our GraphQL schema. From your project folder, open the file <project folder>/web-api/index.js and add the following code to the typeDefs variable in the middle of the file:

File path icon web-api/index.js
//Existing code...

const typeDefs = gql`
  type Query {
    #Existing code...
  }

  # Add the top level mutation
  type Mutation {
    createMessage(text: String): Message
  }

  #Existing code...
`;

This will define a mutation in our schema called createMessage that takes a text string parameter. This will hold the contents of a message sent by an anonymous user, which we'll use to send to Slack.

Extending the conversations data source

Before we create our resolver, we will need to extend our conversations API data source that we'll need in our new resolver. We will need to add three new methods for interacting with the Slack API to start a conversation.

Open the file api/slack-integration/data-sources/conversations.js and at the end of the class, add the createConversation method:

File path icon web-api/api/slack-integration/data-sources/conversations.js
// Existing code...

module.exports = class ConversationsAPI extends DataSource {
  // Existing code...

  /**
   * Create a Slack channel
   */
  createConversation(name, userIds) { // <-- New method
    const { webAPI } = connection.getConnection();

    return webAPI.conversations.create({ name, user_ids: userIds })
      .then(res => res.channel);
  }
}

The createConversation method will create a Slack channel named with the value name and add users to the channel from the array of userIds.

The next method we will create will be for inviting users to a channel. Although order is not significant, add this after the last method.

File path icon web-api/api/slack-integration/data-sources/conversations.js
module.exports = class ConversationsAPI extends DataSource {
  // Existing code...

  createConversation(name, userIds) {
    // Existing code...
  }

  /**
   * Invite users to a Slack channel
   */
  inviteUser({ channelId, userIds }) { // <-- New method
    const { webAPI } = connection.getConnection();

    return webAPI.conversations.invite({ channel: channelId, users: userIds.join(',') })
      .then(res => res.channel);
  }
}

This method takes a channelId and list of user ids that are used to join the channel.

We have our methods for creating a conversation channel and inviting users to the channel. The last method we will add to our DataSource will be used to actually post a message to the new channel. We can add this to the end of our class:

File path icon web-api/api/slack-integration/data-sources/conversations.js
module.exports = class ConversationsAPI extends DataSource {
  // Existing code...

  createConversation(name, userIds) {
    // Existing code...
  }

  inviteUser({ channelId, userIds }) {
    // Existing code...
  }

  /**
   * Create a message in a channel
   */
  chatPostMessage(channelId, text) { // <-- New method
    const { webAPI } = connection.getConnection();

    return webAPI.chat.postMessage({ channel: channelId, text })
      .then(res => res.message);
  }
}

Again, we are using the shared Slack connection to call chat.postMessage with a channel id and text option then return the message from the response. The webAPI methods all return asynchronous Promises.

With the ability to create a message in a new Slack channel, we are all set to now integrate our resolver function.

Create the resolver

Just like GraphQL queries, we need to create a resolver to handle the new incoming request.

In your terminal or command prompt, change directory to the web-api folder within your project folder then create the file api/slack-integration/resolvers/messages.createMessage.mutation.js and open it.

touch ./api/slack-integration/resolvers/messages.createMessage.mutation.js

At the top of the file we need to import the slackMessageMap function we previously used for our query and then create a new function definition that we'll export as our GraphQL resolver.

File path icon web-api/api/slack-integration/resolvers/messages.createMessage.mutation.js
const slackMessageMap = require('../../slack-integration/data-mappings/message');

module.exports = async function createMessage (parent, args, { dataSources, session }) {

};

The async keyword will allow us to use the await keyword in the function body. This will pause execution of the function to resolve a promise and get the result before moving on to execute the next line of code. We can do this by adding the next two lines.

File path icon web-api/api/slack-integration/resolvers/messages.createMessage.mutation.js
const slackMessageMap = require('../../slack-integration/data-mappings/message');

module.exports = async function createMessage (parent, args, { dataSources, session }) {
  const channelName = 'conversation-1';
  const conversation = await dataSources.conversationsAPI.getConversation(channelName);
};

We first need to check if our conversation exists before we create a message. We will also create another async method that we will conditionally call to create a new conversation if one doesn't exist.

File path icon web-api/api/slack-integration/resolvers/messages.createMessage.mutation.js
const slackMessageMap = require('../../slack-integration/data-mappings/message');

module.exports = async function createMessage (parent, args, { dataSources, session }) {
  const channelName = 'conversation-1';
  const conversation = await dataSources.conversationsAPI.getConversation(channelName);

  async function createConversation() { // <-- New async method
    return dataSources.conversationsAPI.createConversation(channelName, [process.env.SLACK_ME_ID])
      .then(channel => dataSources.conversationsAPI.inviteUser({
        channelId: channel.id,
        userIds: [process.env.SLACK_ME_ID, process.env.SLACK_APP_ID]
      }));
  }
};

In the new method, we call our createConversation and inviteUser methods to first create a conversation, then invite the relevent users to the conversation, including your new Slack app using the environment variables we previously set (process.env.SLACK_APP_ID).

We can now add the code that will conditionally call these methods if a conversation doesn't exist to first create the channel and id or use the existing found channel id needed to post a new message. After the message has been created, we return a mapped version of the message data to the client using the function slackMessageMap.

File path icon web-api/api/slack-integration/resolvers/messages.createMessage.mutation.js
const slackMessageMap = require('../../slack-integration/data-mappings/message');

module.exports = async function createMessage (parent, args, { dataSources, session }) {
  const channelName = 'conversation-1';
  const conversation = await dataSources.conversationsAPI.getConversation(channelName);

  async function createConversation() {
    // Existing code...
  }

  // New return value
  return (conversation ? Promise.resolve(conversation) : createConversation())
    .then(channel => dataSources.conversationsAPI.chatPostMessage(channel.id, args.text))
    .then(slackMessageMap);
};

This wraps up creating our GraphQL mutation to create a message. Finally, we have to register our new resolver to the exported object that will get included in the Apollo Server resolvers.

Open the file api/slack-integration/resolvers/messages.resolver.js. We need to import our new mutation resolver and declare it in our exports object.

File path icon web-api/api/slack-integration/resolvers/messages.resolver.js
const messages = require('./messages.messages.query');
const Message = require('./message');
const createMessage = require('./messages.createMessage.mutation'); // <-- New import

module.exports = {
  Query: {
    messages
  },

  Mutation: { // <-- New property to add
    createMessage
  },

  Message
};

Testing our GraphQL Mutation

We can now test our new mutation. Open a browser window and navigate to the GraphQL playground at: http://localhost:4123/graphql.

If you click and open the SCHEMA window on the right hand side, you'll see our new definition createMessage under the MUTATIONS heading. Clicking this will show all the options we can send with this type of query (in this case just text).

In the left hand pane, copy the following code and click the play button:

mutation createMessage {
  createMessage(text: "Colourful pandas") {
    id
    userId
    text
    fromYou
    timestamp
    user {
      id
      name
      colour
      avatarUrl
    }
  }
}

If you have the Slack application installed to your computer and have desktop notifications enabled, you should get an instant notification from your operating system with details of the new message.

If you open your installed Slack app, you should see a new entry in the conversation-1 channel. And finally if you refresh our Angular client application you should see the new message 'Colourful pandas' sent from 'You', which will be left-aligned in the user interface.