Apollo Client Default Values for Missing Fields

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

But what if you want to ask for incremental data updates (e.g. as part of a subscription). There is no way in GraphQL to listen for incremental data from a subscription. Of course, you can ask for all data and then the server could be coded to transmit incremental updates. But this doesn’t work on the client-side if you are using Apollo Client because it expects all data that you have queried for in your subscription.

There are some libraries on the that try to provide a way to remove duplicates from GraphQL responses, e.g. graphql-deduplicator but it doesn’t have support for merging missing fields.

Here is a quick implementation that defaults all missing fields to a null value. You can then use your own implementation to merge the values as you see fit, but this gets rid of the Apollo Client errors of the kind Missing field X.

The first step is to add a method to your Model (it assumes that all your models inherit from a base Model) objects that you want to handle.

class Model {
  // Converts missing fields to nulls to play well with Apollo client
  static fillUndefineds(clazz: typeof Model, object: any): any {
    if (!object) { return object; }
    const meta = clazz.meta as any;
    _.forEach(meta.attributes, ({ id, clazz }: any, key) => {
      if (typeof object[id] === 'undefined') {
        object[id] = null;
      } else if (object[id] && clazz) {
        object[id] = Model.fillUndefineds(clazz, object[id]);
      }
    });
    return object;
  }
}

The next step is to mark some meta attributes on the models you are trying to convert. You can create your own format for the meta, but here is something that I have used in the past.

class Movie extends Model {}
Movie.meta = {
  attributes: {
    name: { id: 'name', type: TYPE_STRING }
  }
}

class Event extends Model {}
Event.meta = {
  attributes: {
    movie: { id: 'movie', clazz: Movie }
  }
}

Finally, we can create a custom SubscriptionClient and set it on our Link.

// To automatically convert missing backend values to null, use this client
// This doesn't suit well for partial objects (which contain only the changed data)
// but can be used if the app is sure that it will receive a full object.
class WSSubscriptionClient extends SubscriptionClient {
  request(req: any): Observable<any> {
    const parentObservable = super.request(req);
    const observable = {
      subscribe: (observer: Observer<any>) => parentObservable.subscribe({
        next: (value: any) => {
          if (!value || !value.data || !value.data.event) { return observer.next(value); }
          const event = Model.fillUndefineds(Event, value.data.event);
          return observer.next(_.extend({}, value, { data: { event } }));
        },
        error: (err) => observer.error && observer.error(err),
        complete: () => observer.complete && observer.complete(),
      })
    };
    return observable;
  }
}

const subscriptionClient = new WSSubscriptionClient(url, { reconnect: true });
const wsLink = new WebSocketLink(subscriptionClient);
const apolloClient = new ApolloClient({ link: wsLink, cache: new InMemoryCache() });
Published 13 Apr 2020

I build mobile and web applications. Full Stack, Rails, React, Typescript, Kotlin, Swift
Pulkit Goyal on Twitter