visit
type Query {
addresses: [Address]
address(customer_token: String): Address
contacts: [Contact]
contact(customer_token: String): Contact
customers: [Customer]
customer(token: String): Customer
credits: [Credit]
credit(customer_token: String): Credit
}
npm install ws
npm install graphql-ws @graphql-tools/schema
npm install graphql-subscriptions
With those items installed, I focused on updating the index.ts
from my original repository to extend the typedefs
constant with the following:
type Subscription {
creditUpdated: Credit
}
I also established a constant to house a new PubSub
instance and created a sample subscription that we will use later:
const pubsub = new PubSub();
pubsub.publish('CREDIT_BALANCE_UPDATED', {
creditUpdated: {
}
});
I cleaned up the existing resolvers and added a new Subscription
for this new use case:
const resolvers = {
Query: {
addresses: () => addresses,
address: (parent, args) => {
const customer_token = args.customer_token;
return addresses.find(address => address.customer_token === customer_token);
},
contacts: () => contacts,
contact: (parent, args) => {
const customer_token = args.customer_token;
return contacts.find(contact => contact.customer_token === customer_token);
},
customers: () => customers,
customer: (parent, args) => {
const token = args.token;
return customers.find(customer => customer.token === token);
},
credits: () => credits,
credit: (parent, args) => {
const customer_token = args.customer_token;
return credits.find(credit => credit.customer_token === customer_token);
}
},
Subscription: {
creditUpdated: {
subscribe: () => pubsub.asyncIterator(['CREDIT_BALANCE_UPDATED']),
}
}
};
const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const schema = makeExecutableSchema({ typeDefs, resolvers });
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
serverCleanup.dispose();
}
};
}
}
],
});
await server.start();
app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
context: async () => ({ pubsub })
}));
const PORT = Number.parseInt(process.env.PORT) || 4000;
httpServer.listen(PORT, () => {
console.log(`Server is now running on //localhost:${PORT}/graphql`);
console.log(`Subscription is now running on ws://localhost:${PORT}/graphql`);
});
function incrementCreditBalance() {
if (credits[0].balance >= credits[0].credit_limit) {
credits[0].balance = 0.00;
console.log(`Credit balance reset to ${credits[0].balance}`);
} else {
credits[0].balance += 50.00;
console.log(`Credit balance updated to ${credits[0].balance}`);
}
pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: credits[0] });
setTimeout(incrementCreditBalance, 5000);
}
incrementCreditBalance();
The full index.ts
file can be found .
$ heroku login
$ heroku create jvc-graphql-server-sub
Creating ⬢ jvc-graphql-server-sub... done
//jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/ | //git.heroku.com/jvc-graphql-server-sub.git
$ git remote
heroku
origin
Like I noted in my prior article, Apollo Server disables Apollo Explorer in production environments. To keep Apollo Explorer available for our needs, I needed to set the NODE_ENV
environment variable to development. I set that with the following CLI command:
$ heroku config:set NODE_ENV=development
Setting NODE_ENV and restarting ⬢ jvc-graphql-server-sub... done, v3
NODE_ENV: development
$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku
In the Settings section, I found the Heroku app URL for this service instance:
//jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/
Please note – this link will no longer be in service by the time this article is published.
Notice the Subscription responses on the right-hand side of the screen.
import { createClient } from "graphql-ws";
import { WebSocket } from "ws";
import http from "http";
// Create a dummy HTTP server to bind to Heroku's $PORT
const PORT = process.env.PORT || 3000;
http.createServer((req, res) => res.end('Server is running')).listen(PORT, () => {
console.log(`HTTP server running on port ${PORT}`);
});
const host_url = process.env.GRAPHQL_SUBSCRIPTION_HOST || 'ws://localhost:4000/graphql';
const client = createClient({
url: host_url,
webSocketImpl: WebSocket
});
const query = `subscription {
creditUpdated {
token
customer_token
credit_limit
balance
credit_score
}
}`;
function handleCreditUpdated(data) {
console.log('Received credit update:', data);
}
// Subscribe to the creditUpdated subscription
client.subscribe(
{
query,
},
{
next: (data) => handleCreditUpdated(data.data.creditUpdated),
error: (err) => console.error('Subscription error:', err),
complete: () => console.log('Subscription complete'),
}
);
The full index.js
file can be found .
We can deploy this simple Node.js application to Heroku too, making sure to set the GRAPHQL_SUBSCRIPTION_HOST
environment variable to the Heroku app URL we used earlier.
I also created the following Procfile
to tell Heroku how to start up my app:
web: node src/index.js
$ heroku create jvc-websocket-example
Creating ⬢ jvc-websocket-example... done
//jvc-websocket-example-62824c0b1df4.herokuapp.com/ | //git.heroku.com/jvc-websocket-example.git
Then, I set the the GRAPHQL_SUBSCRIPTION_HOST
environment variable to point to my running GraphQL server:
$ heroku --app jvc-websocket-example \
config:set \
GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql
$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku
By viewing the logs within the Heroku Dashboard for jvc-websocket-example
instance, we can see the multiple updates to the balance
property of the jvc-graphql-server-sub
service. In my demo, I was even able to capture the use case where the balance was reduced to zero, simulating that a payment was made:
In the terminal, we can access those same logs with the CLI command heroku logs.
2024-08-28T12:14:48.463846+00:00 app[web.1]: Received credit update: {
2024-08-28T12:14:48.463874+00:00 app[web.1]: token: 'credit-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]: customer_token: 'customer-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]: credit_limit: 10000,
2024-08-28T12:14:48.463875+00:00 app[web.1]: balance: 9950,
2024-08-28T12:14:48.463876+00:00 app[web.1]: credit_score: 750
2024-08-28T12:14:48.463876+00:00 app[web.1]: }
“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” — J. Vester