New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrapping a REST API in GraphQL #379

Closed
dalgard opened this Issue Jul 11, 2016 · 28 comments

Comments

Projects
None yet
8 participants
@dalgard

dalgard commented Jul 11, 2016

The article Wrapping a REST API in GraphQL describes how to use a client-side schema to resolve queries against a REST endpoint.

The schema is inserted as a network layer in Relay – can something similar be achieved with Apollo Client?

@dalgard

This comment has been minimized.

Show comment
Hide comment
@dalgard

dalgard Jul 11, 2016

Seems easier than I expected: Custom network interface

dalgard commented Jul 11, 2016

Seems easier than I expected: Custom network interface

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Jul 11, 2016

Member

Yep, that's it! Very similar to the Relay approach.

Member

stubailo commented Jul 11, 2016

Yep, that's it! Very similar to the Relay approach.

@stubailo stubailo closed this Jul 11, 2016

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 19, 2016

Great, I'm currently seeking for this. @dalgard Have you tried it yet?

linonetwo commented Aug 19, 2016

Great, I'm currently seeking for this. @dalgard Have you tried it yet?

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Aug 19, 2016

Member

By the way, if someone wants to write a post about this it would be awesome!

Member

stubailo commented Aug 19, 2016

By the way, if someone wants to write a post about this it would be awesome!

@dalgard

This comment has been minimized.

Show comment
Hide comment
@dalgard

dalgard Aug 23, 2016

@linonetwo Not yet. Don't know whether I'm going to need it within the next couple of months. Still interested, though!

dalgard commented Aug 23, 2016

@linonetwo Not yet. Don't know whether I'm going to need it within the next couple of months. Still interested, though!

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 24, 2016

OK I'm trying it...

what can be inferred from apollo's docs is just:

import { addQueryMerging } from 'apollo-client';

class REST2GraphQLInterface {

  query({ query, variables, debugName }) {
    return Promise.resolve({ data: {}, errors: [{ code: 0, msg: 'errorMsg' }] });
  }
}

export default addQueryMerging(REST2GraphQLInterface);

I need to refer to relay's doc for further implementation examples...

linonetwo commented Aug 24, 2016

OK I'm trying it...

what can be inferred from apollo's docs is just:

import { addQueryMerging } from 'apollo-client';

class REST2GraphQLInterface {

  query({ query, variables, debugName }) {
    return Promise.resolve({ data: {}, errors: [{ code: 0, msg: 'errorMsg' }] });
  }
}

export default addQueryMerging(REST2GraphQLInterface);

I need to refer to relay's doc for further implementation examples...

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 24, 2016

So I don't need a 「graphql」 package, instead a 「graphql-tool」 at the client side?

linonetwo commented Aug 24, 2016

So I don't need a 「graphql」 package, instead a 「graphql-tool」 at the client side?

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 24, 2016

Well, I think writing REST2GraphQL on the client side is much similar to what we usually wrote on the server side.

The only difference is that we use the connector to get data from database usually, now we use the connector to get data from REST endpoint.

I'm not sure whether I'm on the right path or not.

linonetwo commented Aug 24, 2016

Well, I think writing REST2GraphQL on the client side is much similar to what we usually wrote on the server side.

The only difference is that we use the connector to get data from database usually, now we use the connector to get data from REST endpoint.

I'm not sure whether I'm on the right path or not.

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Aug 24, 2016

Member

@linonetwo you're definitely on the right path. What you are basically doing is writing a "GraphQL server", but on the client, so any of the GraphQL server docs will apply.

You don't need apollo-server or express-graphql for this - those packages are for attaching GraphQL to a real web server, which you don't have or need on the client. You just need to get a GraphQLSchema instance and call it yourself via the graphql function from the graphql package.

Member

stubailo commented Aug 24, 2016

@linonetwo you're definitely on the right path. What you are basically doing is writing a "GraphQL server", but on the client, so any of the GraphQL server docs will apply.

You don't need apollo-server or express-graphql for this - those packages are for attaching GraphQL to a real web server, which you don't have or need on the client. You just need to get a GraphQLSchema instance and call it yourself via the graphql function from the graphql package.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 25, 2016

Thanks for guiding.

Another question: One REST endpoint would give me tons of data. Typically if I want to fill in blanks for this type;

# /api/account/whoami
type User {
  login: Boolean!
  username: String!
  password: String!

  id: Int!
  name: String!

  companyId: Int
  companyName: String
  departmentId: Int
  departmentName: String
  role: String
}

I will write resolver functions, each will just getting small piece of data.
though /api/account/whoami will return

{
  "code": 0,
  "data": {
    "companyId": 1,
    "companyName": "Company1",
    "departmentId": 1,
    "departmentName": "工程部",
    "id": 7,
    "name": "用户3",
    "role": "Customer",
    "username": "Customer3"
  }
}

which covers many of fields of User type.

should I still write resolving functions for every field?
And addQueryMerging(REST2GraphQLInterface) will do some magic for this?

I think I can cache data inside connector.
if /api/account/whoami returns

{
  "code": 0,
  "data": {
    "companyId": 1,
    "companyName": "Company1",
    "departmentId": 1,
    "departmentName": "工程部",
    "id": 7,
    "name": "用户3",
    "role": "Customer",
    "username": "Customer3"
  }
}

and I only need departmentName and companyName for this time, I can cache the whole json inside User model, and set an expire time or so. When next query about role goto User model will hit this cache .

But it looks redundantly with apollo-client 's client side cache.

So my opinion is to simulate the batching again here. If two queries come together within 10ms, second one will hit the cache.

Another option is caching at Model level and use graphQL decorator or so, to pass an 'force fetch' argument to this level. In this way, "client side server" 's caching won't be obscure to user.

Which way is better still need experiment.

linonetwo commented Aug 25, 2016

Thanks for guiding.

Another question: One REST endpoint would give me tons of data. Typically if I want to fill in blanks for this type;

# /api/account/whoami
type User {
  login: Boolean!
  username: String!
  password: String!

  id: Int!
  name: String!

  companyId: Int
  companyName: String
  departmentId: Int
  departmentName: String
  role: String
}

I will write resolver functions, each will just getting small piece of data.
though /api/account/whoami will return

{
  "code": 0,
  "data": {
    "companyId": 1,
    "companyName": "Company1",
    "departmentId": 1,
    "departmentName": "工程部",
    "id": 7,
    "name": "用户3",
    "role": "Customer",
    "username": "Customer3"
  }
}

which covers many of fields of User type.

should I still write resolving functions for every field?
And addQueryMerging(REST2GraphQLInterface) will do some magic for this?

I think I can cache data inside connector.
if /api/account/whoami returns

{
  "code": 0,
  "data": {
    "companyId": 1,
    "companyName": "Company1",
    "departmentId": 1,
    "departmentName": "工程部",
    "id": 7,
    "name": "用户3",
    "role": "Customer",
    "username": "Customer3"
  }
}

and I only need departmentName and companyName for this time, I can cache the whole json inside User model, and set an expire time or so. When next query about role goto User model will hit this cache .

But it looks redundantly with apollo-client 's client side cache.

So my opinion is to simulate the batching again here. If two queries come together within 10ms, second one will hit the cache.

Another option is caching at Model level and use graphQL decorator or so, to pass an 'force fetch' argument to this level. In this way, "client side server" 's caching won't be obscure to user.

Which way is better still need experiment.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 25, 2016

I will write a post after this...

I'm currently using「graphql」package to parse executeableSchema like this:

const executableSchema = makeExecutableSchema({
  typeDefs: schema,
  resolvers,
});

class REST2GraphQLInterface {
  query({ query, variables }) {
    return graphql(
      executableSchema,
      query,
      undefined, // should be rootValue here, I don'k know what its used for
      { 
        Config: new Config({ connector: serverConnector }),
        User: new User({ connector: serverConnector }),
      },
      variables
    );
  }
}

export default addQueryMerging(new REST2GraphQLInterface());

And it doesn't work, throwing Error: Must provide a schema definition from \node_modules\graphql\utilities\buildASTSchema.js

Maybe executableSchema is not what it wants?

linonetwo commented Aug 25, 2016

I will write a post after this...

I'm currently using「graphql」package to parse executeableSchema like this:

const executableSchema = makeExecutableSchema({
  typeDefs: schema,
  resolvers,
});

class REST2GraphQLInterface {
  query({ query, variables }) {
    return graphql(
      executableSchema,
      query,
      undefined, // should be rootValue here, I don'k know what its used for
      { 
        Config: new Config({ connector: serverConnector }),
        User: new User({ connector: serverConnector }),
      },
      variables
    );
  }
}

export default addQueryMerging(new REST2GraphQLInterface());

And it doesn't work, throwing Error: Must provide a schema definition from \node_modules\graphql\utilities\buildASTSchema.js

Maybe executableSchema is not what it wants?

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Aug 26, 2016

Member

What is schema? perhaps it's not the right kind of object?

Member

stubailo commented Aug 26, 2016

What is schema? perhaps it's not the right kind of object?

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Aug 26, 2016

Member

Also, you don't need to write resolvers like ({ field }) => field - that's the default behavior. So you only need explicit resolvers for the case where you need to do some transformation on the data.

Member

stubailo commented Aug 26, 2016

Also, you don't need to write resolvers like ({ field }) => field - that's the default behavior. So you only need explicit resolvers for the case where you need to do some transformation on the data.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Aug 28, 2016

OK, My only reference is GitHunt... Learnt from it :

type Entry {
  repository: Repository!
  postedBy: User!
  createdAt: Float! # Actually a date
  score: Int!
  comments: [Comment]! # Should this be paginated?
  commentCount: Int!
  id: Int!
  vote: Vote!
}
`];

export const resolvers = {
  Entry: {
    repository({ repository_name }, _, context) {
      return context.Repositories.getByFullName(repository_name);
    },
    postedBy({ posted_by }, _, context) {
      return context.Users.getByLogin(posted_by);
    },
    comments({ repository_name }, _, context) {
      return context.Comments.getCommentsByRepoName(repository_name);
    },
    createdAt: property('created_at'),
    commentCount({ repository_name }, _, context) {
      return context.Comments.getCommentCount(repository_name) || constant(0);
    },
    vote({ repository_name }, _, context) {
      if (!context.user) return { vote_value: 0 };
      return context.Entries.haveVotedForEntry(repository_name, context.user.login);
    },
  },

that
every field we are using should a resolver function, or we can't get data for it.

Are there any edge case learning material?

linonetwo commented Aug 28, 2016

OK, My only reference is GitHunt... Learnt from it :

type Entry {
  repository: Repository!
  postedBy: User!
  createdAt: Float! # Actually a date
  score: Int!
  comments: [Comment]! # Should this be paginated?
  commentCount: Int!
  id: Int!
  vote: Vote!
}
`];

export const resolvers = {
  Entry: {
    repository({ repository_name }, _, context) {
      return context.Repositories.getByFullName(repository_name);
    },
    postedBy({ posted_by }, _, context) {
      return context.Users.getByLogin(posted_by);
    },
    comments({ repository_name }, _, context) {
      return context.Comments.getCommentsByRepoName(repository_name);
    },
    createdAt: property('created_at'),
    commentCount({ repository_name }, _, context) {
      return context.Comments.getCommentCount(repository_name) || constant(0);
    },
    vote({ repository_name }, _, context) {
      if (!context.user) return { vote_value: 0 };
      return context.Entries.haveVotedForEntry(repository_name, context.user.login);
    },
  },

that
every field we are using should a resolver function, or we can't get data for it.

Are there any edge case learning material?

@stubailo

This comment has been minimized.

Show comment
Hide comment
@stubailo

stubailo Aug 29, 2016

Member

Note that id doesn't have a resolver. createdAt has one because we are renaming the field from created_at.

We're probably going to write a complete GraphQL server guide in the future, but it's not our top priority right now.

Member

stubailo commented Aug 29, 2016

Note that id doesn't have a resolver. createdAt has one because we are renaming the field from created_at.

We're probably going to write a complete GraphQL server guide in the future, but it's not our top priority right now.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Sep 8, 2016

I found that there were lots of posts on the Medium written by your team, which are extremely useful for beginners.Thank you guys!

But were they be linked or merged to docs, I won't spending so much time learning and experimenting it...

Now building a wrapping for RESTful endpoint is a very easy job, I'm almost done.
I'll write a post soon.

linonetwo commented Sep 8, 2016

I found that there were lots of posts on the Medium written by your team, which are extremely useful for beginners.Thank you guys!

But were they be linked or merged to docs, I won't spending so much time learning and experimenting it...

Now building a wrapping for RESTful endpoint is a very easy job, I'm almost done.
I'll write a post soon.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Sep 8, 2016

I have my try to make it my experiment repo

though my writing of resolver is kind of verbose...

linonetwo commented Sep 8, 2016

I have my try to make it my experiment repo

though my writing of resolver is kind of verbose...

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Sep 13, 2016

(I record my research path here just for indexing reference, providing help for who need it.)
( Problem figured out, it's due to apollo-client returning an object rather than string for query )

@stubailo You mentioned that I can call executableSchema via the graphql( ) function from the graphql package, I'm doing it like this:

// http://dev.apollodata.com/core/network.html#NetworkInterface
class REST2GraphQLInterface {
  query({ query, variables }) {
    console.log(executableSchema); // GraphQLSchema {_queryType: GraphQLObjectType, _mutationType: GraphQLObjectType, _subscriptionType: null, _directives: Array[3], _typeMap: Object…}
    console.log(query); // Object {kind: "Document", definitions: Array[1]}
    console.log(variables); // I'm not using it currently
    return graphql(
      executableSchema,
      query,
      undefined,
      {
        Config: new Config({ connector: serverConnector }),
        User: new User({ connector: serverConnector }),
        PowerEntity: new PowerEntity({ connector: serverConnector }),
        FortuneCookie: new FortuneCookie(),
      },
      variables
    );
  }
}

_20160913144600

But query it will throw out { errors: [ [TypeError: source.body.split is not a function] ] } just like this graphql-js issue descripes

Relay have a different NetworkInterface from Apollo, It has sendMutation(mutationRequest), sendQueries(queryRequests), supports(...options) .

While Apollo use single query(request: GraphQLRequest): Promise.
So I can't use package relay-local-schema as Facebook mentioned directly too.

But I believe @stubailo is right, since graphql( ) receives these arguments... Oh! I found it! The query receive from ApolloClient is mismatching the second argument of graphql( ) !

query is now an Object {kind: "Document", definitions: Array[1]} but graphql( ) needs a string.

How I can convert query to string remains a mystery. Since as document read query is just a string.
I tried to remove addQueryMerging( ) and

queryTransformer: addTypename,
shouldBatch: true,

from new ApolloClient({ })
but query still being an object.

linonetwo commented Sep 13, 2016

(I record my research path here just for indexing reference, providing help for who need it.)
( Problem figured out, it's due to apollo-client returning an object rather than string for query )

@stubailo You mentioned that I can call executableSchema via the graphql( ) function from the graphql package, I'm doing it like this:

// http://dev.apollodata.com/core/network.html#NetworkInterface
class REST2GraphQLInterface {
  query({ query, variables }) {
    console.log(executableSchema); // GraphQLSchema {_queryType: GraphQLObjectType, _mutationType: GraphQLObjectType, _subscriptionType: null, _directives: Array[3], _typeMap: Object…}
    console.log(query); // Object {kind: "Document", definitions: Array[1]}
    console.log(variables); // I'm not using it currently
    return graphql(
      executableSchema,
      query,
      undefined,
      {
        Config: new Config({ connector: serverConnector }),
        User: new User({ connector: serverConnector }),
        PowerEntity: new PowerEntity({ connector: serverConnector }),
        FortuneCookie: new FortuneCookie(),
      },
      variables
    );
  }
}

_20160913144600

But query it will throw out { errors: [ [TypeError: source.body.split is not a function] ] } just like this graphql-js issue descripes

Relay have a different NetworkInterface from Apollo, It has sendMutation(mutationRequest), sendQueries(queryRequests), supports(...options) .

While Apollo use single query(request: GraphQLRequest): Promise.
So I can't use package relay-local-schema as Facebook mentioned directly too.

But I believe @stubailo is right, since graphql( ) receives these arguments... Oh! I found it! The query receive from ApolloClient is mismatching the second argument of graphql( ) !

query is now an Object {kind: "Document", definitions: Array[1]} but graphql( ) needs a string.

How I can convert query to string remains a mystery. Since as document read query is just a string.
I tried to remove addQueryMerging( ) and

queryTransformer: addTypename,
shouldBatch: true,

from new ApolloClient({ })
but query still being an object.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Sep 13, 2016

Ok, I made it on the react-native side, in production!

I will link my blog post here later.
Chinese Version: 把REST包装成GraphQL
Translation is in progress.

So, @dalgard Apollo is proved to be capable of wrapping REST endpoint, on the server side, on the client side, and on the react-native side! : D

linonetwo commented Sep 13, 2016

Ok, I made it on the react-native side, in production!

I will link my blog post here later.
Chinese Version: 把REST包装成GraphQL
Translation is in progress.

So, @dalgard Apollo is proved to be capable of wrapping REST endpoint, on the server side, on the client side, and on the react-native side! : D

@Urigo

This comment has been minimized.

Show comment
Hide comment
@Urigo

Urigo Feb 7, 2017

Contributor

@linonetwo I would love to see an English version of that post!
If you need help with that I would love too

Contributor

Urigo commented Feb 7, 2017

@linonetwo I would love to see an English version of that post!
If you need help with that I would love too

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Feb 12, 2017

@Urigo Ok, I'm working on that, please wait for a while.

linonetwo commented Feb 12, 2017

@Urigo Ok, I'm working on that, please wait for a while.

@linonetwo

This comment has been minimized.

Show comment
Hide comment
@linonetwo

linonetwo Feb 12, 2017

@Urigo http://onetwo.ren/wrapping-restful-in-graphql/

Were there any bug or issue please point out. Any question, ask me and I will add some content. But what I want to say is that

Setting up an REST-GraphQL gateway is quite similar to set up a simple and naive GraphQL server

linonetwo commented Feb 12, 2017

@Urigo http://onetwo.ren/wrapping-restful-in-graphql/

Were there any bug or issue please point out. Any question, ask me and I will add some content. But what I want to say is that

Setting up an REST-GraphQL gateway is quite similar to set up a simple and naive GraphQL server

@lucasconstantino

This comment has been minimized.

Show comment
Hide comment
@lucasconstantino

lucasconstantino Mar 14, 2017

Contributor

I've being working on a project with apollo-client/server both on the browser wrapping a rest api. I works smoothly, with File upload support, Apollo client dev tools working properly, etc, etc. I'm working on a post in portuguese, but I guess I can make a copy in english and put in on Medium or similar. Does anyone have a better or more contextualized place to publish it, in case it fits?

Contributor

lucasconstantino commented Mar 14, 2017

I've being working on a project with apollo-client/server both on the browser wrapping a rest api. I works smoothly, with File upload support, Apollo client dev tools working properly, etc, etc. I'm working on a post in portuguese, but I guess I can make a copy in english and put in on Medium or similar. Does anyone have a better or more contextualized place to publish it, in case it fits?

@Urigo

This comment has been minimized.

Show comment
Hide comment
@Urigo

Urigo Mar 15, 2017

Contributor

@lucasconstantino Medium sounds great.
If you want we can review it and in the case will fit and a lot of people will be interested, publish it also under our Apollo publication

Contributor

Urigo commented Mar 15, 2017

@lucasconstantino Medium sounds great.
If you want we can review it and in the case will fit and a lot of people will be interested, publish it also under our Apollo publication

@SeanBannister

This comment has been minimized.

Show comment
Hide comment
@SeanBannister

SeanBannister Jul 7, 2017

For anyone else stumbling across this you might like to check out https://github.com/mstn/apollo-local-network-interface

SeanBannister commented Jul 7, 2017

For anyone else stumbling across this you might like to check out https://github.com/mstn/apollo-local-network-interface

@jozanza

This comment has been minimized.

Show comment
Hide comment
@jozanza

jozanza Nov 22, 2017

Looks like the approach with Apollo 2.0 is different with the "link" approach replacing network interfaces. Here's how I personally got my plain old GraphQLSchema instance working:

import { Observable, ApolloLink, execute, makePromise } from 'apollo-link'
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import { ApolloClient } from 'apollo-client'
import { graphql, print } from 'graphql'
import schema from './schema' // should be an instance of GraphQLSchema

const link = new ApolloLink(
  operation =>
    new Observable(observer => {
      const { query, variables, operationName } = operation
      graphql(schema, print(query), {}, {}, variables, operationName)
        .then(result => {
          observer.next(result)
          observer.complete(result)
        })
        .catch(e => observer.error(e))
    }),
)
const cache = new InMemoryCache()
//  Note: if your schema contains interfaces / union types, ApolloClient can't infer possibleTypes out-of-the-box
//  You'll have to let it know manually via a fragmentMatcher. For example:
//  const cache = new InMemoryCache({
//    fragmentMatcher: new IntrospectionFragmentMatcher({
//      introspectionQueryResultData: {
//        __schema: {
//          types: [{
//            kind: 'INTERFACE',
//            name: 'Either',
//            possibleTypes: [
//               { name: 'Left' },
//               { name: 'Right' },
//            ]
//          }] 
//        }
//      },
//    }),
//  })
const client = new ApolloClient({ link, cache })

jozanza commented Nov 22, 2017

Looks like the approach with Apollo 2.0 is different with the "link" approach replacing network interfaces. Here's how I personally got my plain old GraphQLSchema instance working:

import { Observable, ApolloLink, execute, makePromise } from 'apollo-link'
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
import { ApolloClient } from 'apollo-client'
import { graphql, print } from 'graphql'
import schema from './schema' // should be an instance of GraphQLSchema

const link = new ApolloLink(
  operation =>
    new Observable(observer => {
      const { query, variables, operationName } = operation
      graphql(schema, print(query), {}, {}, variables, operationName)
        .then(result => {
          observer.next(result)
          observer.complete(result)
        })
        .catch(e => observer.error(e))
    }),
)
const cache = new InMemoryCache()
//  Note: if your schema contains interfaces / union types, ApolloClient can't infer possibleTypes out-of-the-box
//  You'll have to let it know manually via a fragmentMatcher. For example:
//  const cache = new InMemoryCache({
//    fragmentMatcher: new IntrospectionFragmentMatcher({
//      introspectionQueryResultData: {
//        __schema: {
//          types: [{
//            kind: 'INTERFACE',
//            name: 'Either',
//            possibleTypes: [
//               { name: 'Left' },
//               { name: 'Right' },
//            ]
//          }] 
//        }
//      },
//    }),
//  })
const client = new ApolloClient({ link, cache })
@Vanuan

This comment has been minimized.

Show comment
Hide comment
@Vanuan

Vanuan Jan 3, 2018

Here's an example for Apollo client 2.0:

https://stackoverflow.com/a/48082522/99024

Vanuan commented Jan 3, 2018

Here's an example for Apollo client 2.0:

https://stackoverflow.com/a/48082522/99024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment