Using the GraphQL API

Once you have an access token, you can now use it to get (and push) data to/from Pitchly.

This step assumes you have generated an access token, which you will attach to every API request. Learn how to generate access tokens.

What is GraphQL?

GraphQL is a fairly new API framework that replaces the more common REST API. It was developed by Facebook to better handle data distribution at scale. We chose to use GraphQL for several of our own reasons:

  1. Built-in subscription support enables Apps to get data in real time, a key design principle in Pitchly.

  2. You can choose exactly the data you want to receive, spanning several possible data sets, in total fewer network requests.

  3. It uses a single endpoint, creating less breaking changes, allowing for incremental adoptability and better version control.

  4. Plays well with modern frontend frameworks, like React, Vue.js, and Meteor.

If you are developing your App using a modern web application framework, we recommend using Apollo Client to work with our GraphQL API.

If you are not developing your App using a modern web application framework, you can still make calls to our GraphQL API over REST. This is because, at its core, GraphQL is really an abstraction over HTTP calls. It is a structured query language that gets sent over an HTTP request, just like REST, except the query is put inside the body of the request. Without a framework, like Apollo Client, however, you won't have the added perks of a client-side cache or automatic subscription updates.

On this page, we will be going over how to use the Pitchly GraphQL API using Apollo Client. See: Using the GraphQL API over REST to learn how to make HTTP requests directly to the Pitchly GraphQL API.

Using Apollo Client

Apollo Client is compatible with several major frontend web frameworks, including React, Vue.js, Meteor, Angular, Ember, as well as native mobile on iOS and Android. You can get real snazzy with it, but we're just going to show a dirt basic example to start. The examples below are compatible with any frontend JavaScript framework you choose.

Basic query

npm install --save apollo-client graphql-tag graphql apollo-link-http apollo-cache-inmemory
import { ApolloClient } from 'apollo-client';
import gql from 'graphql-tag';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
link: new HttpLink({
uri: 'https://v2.pitchly.net/graphql'
}),
cache: new InMemoryCache()
});

Let's get information about the current database:

client.query({
query: gql`
query database($id: ID!, $accessToken: String!) {
database(id: $id, accessToken: $accessToken) {
_id
organizationId
name
description
color
fields {
_id
name
description
type
primary
required
restrict
database
}
}
}
`,
variables: {
id: pitchly.getDatabaseId(), // gets the current database ID
accessToken: "MY-ACCESS-TOKEN"
}
})
.then(result => console.log("Result:", result.data))
.catch(error => console.log("Error:", error.graphQLErrors[0].data));

We will get back several properties of the database, including its name, description, color, and all of its fields, in the form of a Promise.

This is great for a one-time query, but it only gives us a snapshot into a particular database on Pitchly in that moment. What if I want to be notified whenever the database changes?

Real-time subscriptions

Subscriptions are a feature of GraphQL that allow you to receive notifications of data changes on Pitchly as soon as they happen, over websockets.

We'll need just a few more NPM packages (we've included the ones above again):

npm install --save apollo-client graphql-tag graphql apollo-link apollo-link-http apollo-link-ws apollo-utilities apollo-cache-inmemory subscriptions-transport-ws

We need to make some changes to the ApolloClient:

import { ApolloClient } from 'apollo-client';
import gql from 'graphql-tag';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
link: split(
({ query }) => { // use WebSocket or HTTP, depending on the operation
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
new WebSocketLink({
uri: 'wss://v2.pitchly.net/subscriptions',
options: {
reconnect: true // automatically reconnect if disconnected
}
}),
new HttpLink({
uri: 'https://v2.pitchly.net/graphql'
})
),
cache: new InMemoryCache()
});

Next, we will re-use the same query we initially had, but this time, we want to watch it change as new data from the subscription comes in. This will require two calls: one to the query and one to a subscription.

// begin the query
const queryObserver = client.watchQuery({
query: gql`
query database($id: ID!, $accessToken: String!) {
database(id: $id, accessToken: $accessToken) {
_id
organizationId
name
description
color
fields {
_id
name
description
type
primary
required
restrict
database
}
}
}
`,
variables: {
id: pitchly.getDatabaseId(), // gets the current database ID
accessToken: "MY-ACCESS-TOKEN"
}
});
// declare the GraphQL subscription
const subscriptionObserver = client.subscribe({
query: gql`
subscription databaseUpdated($id: ID!, $accessToken: String!) {
databaseUpdated(id: $id, accessToken: $accessToken) {
_id
organizationId
name
description
color
fields {
_id
name
description
type
primary
required
restrict
database
}
}
}
`,
variables: {
id: pitchly.getDatabaseId(), // gets the current database ID
accessToken: "MY-ACCESS-TOKEN"
}
});
// get updates from the query as subscription updates come in
const querySub = queryObserver.subscribe({
// next: ({ errors, data, loading, networkStatus, stale }) => {
next: ({ loading, data }) => {
if (!loading) {
console.log(data); // runs whenever new data has come in
}
}
});
// this is necessary to listen to subscription updates
const subscriptionSub = subscriptionObserver.subscribe(); // seems counterintuitive, but subscribe returns an Observable to which we must also subscribe
// make sure to unsubscribe from all Observables when you no longer need the data
querySub.unsubscribe();
subscriptionSub.unsubscribe();

There are understandably a few confusing things here, so let's break it down.

There are two main differences between a query and a subscription:

  • A query retrieves a particular set of data.

  • A subscription is just a listener. It does not request any data when first connected, but only opens up a connection to get new data.

To make things even more interesting, there are also two different types of queries:

The Apollo Cache

One important thing to know about Apollo is that queries do not hold the data they request in isolation. Under every query, subscription, and mutation is a single global cache that houses all requested data. The cache serves as a single source of truth for all requested API data, regardless of which query requested it.

The Apollo cache is global and semi-persistent, but don't confuse this with your browser's cache. Apollo's cache only lives inside the page session and does not carry across page reloads.

Subscriptions are able to function because the call to the subscription automatically "pulls down" updates into the Apollo cache when updated data arrives. Every watched query which uses that same data will automatically "pull up" the update from the cache.

This is great most of the time, but there may be cases where you want to do custom logic on a subscription update. Learn more below.

Observables

You may have noticed a lot of subscribes floating around. Well, they're not all related to subscriptions.

An unfortunate choice of the makers of Apollo was to use the same method name for both creating GraphQL subscriptions and subscribing to their results from the Apollo cache. The first is a client-server intraction, while the latter is a client-client cache interaction.

Both client.watchQuery and client.subscribe methods return an Observable, which represents that query or subscription and the data it uses in the cache. When something changes to the data that query or subscription represents, you can capture that update by subscribing to its Observable. For subscriptions, subscribing to the Observable is necessary to tell Apollo you are interested in that data.

In addition to querying data, you can also perform actions against the Observable, such as refetching with different variables, checking load status, and paginating.

Remember that client.watchQuery is not the same as client.query. The first will return an Observable, whereas the latter will return a Promise because it is only run once.

After you are done with a set of data, make sure to unsubscribe from its updates using observerSub.unsubscribe(), where observerSub is the return value of observer.subscribe().

Mutations

Mutations are used whenever you want to "post" data to Pitchly, such as when you want to insert a row to a database. Mutations are very similar to queries in that they run once and return a Promise, but they also have some additional options.

client.mutate({
mutation: gql`
mutation insertRow($databaseId: ID!, $row: DataRowInput!, $accessToken: String!) {
insertRow(databaseId: $databaseId, row: $row, accessToken: $accessToken) {
_id
cols {
fieldId
value
}
}
}
`,
variables: {
databaseId: pitchly.getDatabaseId(), // gets the current database ID
row: {
cols: [
{
fieldId: nameFieldId,
value: {
val: "Foo Bar"
}
},
{
fieldId: dateFieldId,
value: {
val: "2018-1-23" //dates can take a JS Date object or a date string
}
},
{
fieldId: otherFieldId,
value: {
val: null //represents "empty" value
}
}
]
},
accessToken: "MY-ACCESS-TOKEN"
}
})
.then(result => console.log("Result:", result.data))
.catch(error => console.log("Error:", error.graphQLErrors[0].data));

This example will return the new row's _id and cols.

Mutations can do some unique things, like Optimistic UI, to keep latency at a minimum. But if you are using subscriptions, all of your queries using the affected data will be updated at near real time automatically after your mutation affects the relevant data, resulting in a positive UI loop. This makes for a very quick and seamless real-time experience for not only the user updating the data, but also for all other users viewing the same data. And it ensures that no user ever acts on data that is out of date.

More tips & tricks

Detecting load status

If you would like to get the loading status of a query with Apollo, perhaps to show a loading indicator, you can use the next function in combination with notifyOnNetworkStatusChange: true.

const observer = client.watchQuery({
...,
notifyOnNetworkStatusChange: true // add this to your query to trigger "next" when loading
});
const sub = observer.subscribe({
// next: ({ errors, data, loading, networkStatus, stale }) => {
next: ({ loading }) => {
console.log(loading ? "Loading" : "Done Loading");
}
});
// call this when you're no longer interested in the data
sub.unsubscribe();

Note that subscribe here is not related to Subscriptions, but rather it "subscribes" to an Observer.

In addition to detecting the load status of a watched query, next can also be used to capture subscription updates, depending on your use case. Learn more.

Re-run a query with new variables

You can forcibly re-run a query by using refetch.

const observer = client.watchQuery({...});
observer.refetch({
// new variables...
id: pitchly.getDatabaseId(), // gets the current database ID
accessToken: "NEW-ACCESS-TOKEN"
});

This will make a new network request to Pitchly using the new variables for the query. The variables object that is passed to refetch is completely optional, and refetch can be called alone to simply re-run the query with the existing variables intact.

Common use cases for refetch are:

  • You are not using subscriptions and would like to manually re-query on an event or interval (if you are refetching on an interval, consider using pollInterval instead.)

  • The access token, or another parameter, has changed and you need to update it

  • You need to re-capture a set of data for which there is no subscription

Change caching behavior

Caching in Apollo can be useful when dealing with repetitive queries against the same data. But sometimes it can make things difficult to debug, or Apollo may think you have all the data for an object because it already exists in the cache, when really you don't have all the data you need. If you're making a request for the same object but can't see the new data come in past the data you previously queried for, caching may be your issue.

There are several solutions, but a simple one is to change the caching behavior of a query by changing its fetchPolicy. Just add it as an additional parameter to the query.

client.query({
query: gql`...`,
variables: {...},
fetchPolicy: "network-only",
})
.then(result => console.log("Result:", result.data))
.catch(error => console.log("Error:", error.graphQLErrors[0].data));

Possible options are:

  • cache-first

  • cache-and-network

  • network-only

  • cache-only

  • no-cache

  • standby

Read about each type of fetchPolicy here.

fetchPolicy can be applied to watchQuery, query, mutate, and subscribe operations.

Pagination

Pagination can be achieved without much manual effort using Apollo's fetchMore function. Call fetchMore the moment you would like to get more results.

const observer = client.watchQuery({...});
observer.fetchMore({
variables: {
skip: observer.currentResult().data.data.length // or something like this
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return Object.assign({}, prev, {
data: [...prev.data, ...fetchMoreResult.data]
});
}
});

Merging subscription updates

Subscriptions in Apollo are, by default, pretty smart. In most cases, they will merge new data into Apollo's cache for you, giving all your existing queries access to the new data automatically. But there are a few cases where this won't work:

  • The data coming from the subscription doesn't match the format of the data in the cache

  • A range of data was queried and a new row of data was created. We've been told about the new row, but we don't know where it is in relation to other rows (does it fit inside the range?)

  • Something new was created and we want to add it to our cache, but we didn't have a query for it previously because it didn't exist yet!

In these cases, either one of these two methods can help to update existing queries with new data:

  1. refetch

refetch

Using refetch simply runs the entire query again whenever we get notice from the subscription that something has changed. This method has pros and cons.

Pros:

  • Simplest way to keep data in sync

  • Maintains original sort order when getting an array of data

  • Automatically handles insertions and deletions

Cons:

  • Not very efficient, downloads entire results again and doubles network requests

  • Not suited for large or growing data sets (i.e. using infinite scroll). When querying a list of rows in a database, for example, you may not even be able to do a full refetch if you have used pagination to load more than the return limit (currently 100 rows at a time).

const queryObserver = client.watchQuery({...});
const subscriptionObserver = client.subscribe({...});
const subscriptionSub = subscriptionObserver.subscribe({
next: ({ data }) => {
if (data && data.dataRowsChanged) {
queryObserver.refetch(); // reload the query that's using this data
// or see the changed data directly at data.dataRowsChanged
}
}
});
// call this when you're no longer interested in the data
subscriptionSub.unsubscribe();

subscribeToMore

subscribeToMore is similar to fetchMore for pagination, except it can run multiple times for each update instead of only once, and is triggered by receipt of an update versus when you call it. It, too, has advantages and disadvantages.

Pros:

  • Fine-grained control over how the subscription updates the Apollo cache

  • Very efficient, does not make extraneous network requests, uses what it receives

  • Suited for large and growing data sets (i.e. using infinite scroll)

Cons:

  • More complicated to set up, subscription data (including insertions and deletions) must be merged manually

  • Doesn't maintain original sort order (hard to know where new items go)

Below is a real-world example of code we used in our Documents App to update documents with data in real time while also having infinite scroll through the use of fetchMore. We additionally use Underscore.js and Meteor's client-side Collections here.

const queryObserver = client.watchQuery({...});
queryObserver.subscribeToMore({
document: gql`
subscription dataRowsChanged($databaseId: ID!, $accessToken: String!) {
dataRowsChanged(databaseId: $databaseId, accessToken: $accessToken) {
_id
cols {
fieldId
value
}
}
}
`,
variables: {
databaseId: pitchly.getDatabaseId(), // gets the current database ID
accessToken: "MY-ACCESS-TOKEN"
},
updateQuery: (prev, { subscriptionData }) => {
// On subscription update, update the local cache (update and delete only; no insert yet)
// TODO: We want to catch inserts ideally as well
if (!subscriptionData.data) return prev;
const dataRowsChanged = subscriptionData.data.dataRowsChanged;
const deletedRowIds = _.pluck(_.filter(dataRowsChanged, (row) => !row.cols), "_id");
const updatedRows = _.filter(dataRowsChanged, (row) => !!row.cols);
let resultData = prev.data;
if (deletedRowIds.length > 0) {
// delete, and remove from selections if selected
Selections.remove({ _id: { $in: deletedRowIds } });
resultData = _.reject(resultData, (row) => _.contains(deletedRowIds, row._id));
}
if (updatedRows.length > 0) {
// only update, don't insert because we don't know where it would go
resultData = _.map(resultData, (row) => {
const newRow = _.findWhere(updatedRows, { _id: row._id });
if (newRow) return newRow;
return row;
});
}
return { data: resultData };
}
});

Query Batching

Pitchly supports query batching on all GraphQL API calls. Query batching allows multiple queries to be sent in a single request if they are executed very close together, saving valuable load time. This means that if you render several components, for example a navbar, sidebar, and content, and each of those do their own GraphQL query, they will all be sent in one roundtrip. In Apollo, it is very easy to enable query batching.

In your ApolloClient setup, replace the following section of code:

...
new HttpLink({
uri: 'https://v2.pitchly.net/graphql'
})
...

with:

import { BatchHttpLink } from 'apollo-link-batch-http';
...
new BatchHttpLink({
uri: 'https://v2.pitchly.net/graphql',
// batchMax: 10 // max number of items to batch (10 is default)
})
...

Also make sure to install the NPM package:

npm install --save apollo-link-batch-http

See the Apollo Guide for more options around query batching.

References