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 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
Having issues? Debug with the Heroku command
heroku logs --tail
to see what's happening on the Heroku server.
Test the endpoints!
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.