Chapter 9: Subscriptions

Full Stack TypeScript

link Subscriptions

By the end of this lesson, developers will be able to:

  • Subscribe to a live feed of new places

Introduction

In this section, we introduce Websocket connections for our local server.

Postgres

Please install Postgres for local server development using:

brew install postgres
brew services start postgres
createdb `whoami`

Dependencies

yarn add graphql-subscriptions graphql-postgres-subscriptions
yarn add -D @types/pg
yarn start

Create a new file called @types/graphql-postgres-subscriptions.d.ts:

declare module 'graphql-postgres-subscriptions';

Once you have that the server is running, we add subscriptions to the TypeGraphQL project.

link PubSub

Create a new file called src/pubSub.ts:

import { PostgresPubSub } from 'graphql-postgres-subscriptions';
import { Client } from 'pg';

const DATABASE_URL = process.env.DATABASE_URL || 'postgres';

const client = new Client({
  connectionString: DATABASE_URL,
  ssl: process.env.DATABASE_URL ? true : false
});

client.connect();

const pubSub = new PostgresPubSub({ client });

export default pubSub;

link Apollo Server

Import and integrate the pubSub module with your Apollo Server:

import pubSub from './pubSub';

{/* ... */}

  // build TypeGraphQL executable schema
  const schema = await buildSchema({
    // add all typescript resolvers
    // __dirname + '/resolvers/*.ts'
    resolvers: [PlaceResolver, AuthResolver],
    validate: true,
    // automatically create `schema.gql` file with schema definition in current folder
    emitSchemaFile: path.resolve(__dirname, 'schema.gql'),
    pubSub
  });

  // Create GraphQL server
  const apolloServer = new ApolloServer({
    schema,
    context: ({ req, res }) => ({ req, res }),
    introspection: true,
    // enable GraphQL Playground
    playground: true,
    subscriptions: {
      keepAlive: 1000
    }
  });

  apolloServer.applyMiddleware({ app, cors: true });

  const port = process.env.PORT || 4000;

  const httpServer = http.createServer(app);
  apolloServer.installSubscriptionHandlers(httpServer);

  // Start the server
  httpServer.listen(port, () => {
    console.log(
      `🚀 Server ready at http://localhost:${port}${apolloServer.graphqlPath}`
    );
    console.log(
      `🚀 Subscriptions ready at ws://localhost:${port}${apolloServer.subscriptionsPath}`
    );
  });

link Place Resolver

Try running the server, and see if there are any connection issues. If not let's continue with the new PubSub engine.

Let's revisit the src/resolvers/PlaceResolver.ts and add the PubSubEngine:

import {
  Resolver,
  Query,
  Arg,
  Mutation,
  Ctx,
  PubSub,
  Subscription,
  Root,
  Publisher
} from 'type-graphql';
import { plainToClass } from 'class-transformer';
import { PubSubEngine } from 'graphql-subscriptions';

enum Topic {
  PlaceAdded = 'NEW_PLACE_ADDED'
}

{/* ... */}

@Mutation(() => Place)
  async createPlace(
    @Arg('place') placeInput: PlaceInput,
    @Ctx() ctx: { req: Request },
    @PubSub(Topic.PlaceAdded) publish: Publisher<Place>
  ): Promise<Place> {
    const userId = getUserId(ctx);
    if (userId) {
      const place = plainToClass(Place, {
        description: placeInput.description,
        title: placeInput.title,
        imageUrl: placeInput.imageUrl,
        creationDate: new Date()
      });
      const user = await User.findOne(userId);
      if (user) {
        const newPlace = await Place.create({
          ...place,
          user
        }).save();
        await publish(newPlace);
        return newPlace;
      }
      throw new Error('User not found');
    }
    throw new Error('User not found');
  }

{/* ... */}

@Subscription(() => Place, {
    topics: Topic.PlaceAdded
  })
  newPlaceAdded(@Root() place: Place): Place {
    return place;
  }

Open the graphQL playground and insert a new subscription:

subscription NewPlaceAdded {
  newPlaceAdded {
    id
    title
    description
    imageUrl
    user {
      id
      username
    }
  }
}

Then try to edit or add a new place and viola! Live events are published in real time.

link Deployment

Let's deploy our app to heroku.

First we need to update our package.json:

"scripts": {
    "start": "node dist/index.js",
    "dev": "nodemon --exec ts-node --files src/index.ts",
    "build": "tsc",
    "heroku-postbuild": "tsc",
    "generate": "graphql-codegen"
  }

Create a new file called Procfile and insert the following:

web: node dist/index.js

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

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

link Client Subscriptions

Dependencies

yarn add apollo-link-ws subscriptions-transport-ws

link Generate Subscriptions

Create a new file called graphql/newPlaceAdded.graphql:

subscription NewPlaceAdded {
  newPlaceAdded {
    id
    title
    description
    imageUrl
    creationDate
    user {
      id
      username
    }
  }
}
yarn generate

link Update Apollo Client

Add the following to graphql/index.ts:

import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { split } from 'apollo-link';

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
  credentials: 'include'
});

// Create a WebSocket link
const wsLink = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    lazy: true,
    reconnect: true
  }
});

// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
  // split based on operation type
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

link Update Places

Now we can refactor the screens/Places to show new places as they are created. We will also refactor out the "Create Place" button, as it is no longer needed.

import React, { useEffect } from 'react';
import { SafeAreaView, FlatList, Button } from 'react-native';
import { usePlacesQuery, NewPlaceAddedDocument } from '../../graphql';
import { CardView } from '../components';

interface Props {
  navigation;
}

const Places: React.FC<Props> = props => {
  const { data, subscribeToMore } = usePlacesQuery();
  const { navigation } = props;

  useEffect(() => {
    subscribeToMore({
      document: NewPlaceAddedDocument,
      updateQuery: (prev, { subscriptionData }) => {
        if (!subscriptionData.data) return prev;
        const newPlace = (subscriptionData.data as any).newPlaceAdded;
        // add new place
        return Object.assign({}, prev, {
          places: [newPlace, ...prev.places]
        });
      }
    });
  }, []);

  return (
    <SafeAreaView>
      <FlatList
        data={data && data.places ? data.places : []}
        keyExtractor={item => `${item.id}`}
        renderItem={({ item }) => (
          <CardView
            {...(item as any)}
            onPress={() => navigation.navigate('Detail', { item })}
          />
        )}
      />
    </SafeAreaView>
  );
};
export default Places;

Try creating a new place, and watching the feed's live updates. If everything works, congratulations!

WELL DONE!

Wrapping Up

Today we added subscriptions to our TypeGraphQL server, and then deployed it to Heroku. We also added live updates to the app's home feed.

format_list_bulleted
help_outline