apollo-client: .readQuery errors on queries which originally had NULL variables

Trying to read a query which originally had NULL values (perfectly acceptable in the case of cursor pagination for example) will fail, because the call to .readQuery will strip NULL values.

Example: Given that this query have been executed on the client:

query getUsers($cursor: String) {
    users(first: 50, after: $cursor) {
        id
    }
}

Apollo will cache it on a store key looking something like $ROOT_QUERY.users({"first":"10","after": NULL}). Then, when trying to .readQuery with that exact same query, one will get an error like:

Error: Can't find field users({"first":50}) on object ($ROOT_QUERY) {
  "users({\"first\":50,\"after\":null})": {
    "type": "id",
    "id": "$ROOT_QUERY.users({\"first\":50,\"after\":null})",
    "generated": true
  },
  "__typename": "User"
}

Notice that the object passed in Can't find field users({"first": 50}) lacks the {"after": NULL} which is actually in the store key.

Why? I think I found the problem. Given how the store key is resolved in https://github.com/apollographql/apollo-client/blob/master/src/data/storeUtils.ts#L163, any GraphQL query that has an null argument, will cause this error.

The keys in the store can look like this $ROOT_QUERY.whatever.foo.bar({"foo":"bar","baz": NULL}), but when the readStoreResolver tries to find them in the store, it looks for $ROOT_QUERY.whatever.foo.bar({"foo":"bar"}) because the NULL values are undefined at this point, and will be thrown away by JSON.stringify().

Version

  • apollo-client@latest

Originally discussed in https://github.com/apollographql/apollo-client/issues/1701

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 30
  • Comments: 34 (8 by maintainers)

Commits related to this issue

Most upvoted comments

None of the workaround with including empty fields in the variables argument of readQueryworked for me. Would love to see a fix for readQuery without variables.

Just noting that this is a problem for me, too (for purposes of assessing issue impact).

    "apollo-boost": "^0.1.1",
    "apollo-link-context": "^1.0.6",
    "apollo-link-ws": "^1.0.7",
    "graphql": "^0.13.1",
    "react": "16.2.0",
    "react-apollo": "^2.0.4", 

Here’s a more detailed example of how I solved this. Hopefully this makes more sense

/* query. Note both variables (`id` and `after`) are optional */
const FETCH_MESSAGES = gql`
    query Messages($id: ID, $after: String) {
        messages(id: $id, after: $after) {
            edges {
                ...
            }
        }
    }`;

/* mutation */
const CREATE_MESSAGE = gql`
    mutation CreateMessage($message: String!) {
        createMessage(message: $message) {
            message {
              ...
            }
      }
}`;

/* Component definition */
class MessagesView extends React.Component {
    ...
}

const fetchGQL = graphql(FETCH_MESSAGES, {
    options: (ownProps) => {
        const id = ownProps.id || '';
        return {
            variables: { id: id, after: '' }  // `after` here is an empty string
        };
    },
});

/* query update */
const createGQL = graphql(CREATE_MESSAGE, {
    props: ({ mutate }) => {
        return {
            createMessage: ({ message }) => {
                return mutate({
                    variables: { message },
                    update: (store, { data: { createMessage } }) => {
                        // `after` and `id` set as empty strings when calling both readyQuery and writeQuery
                        let data = store.readQuery({ query: FETCH_MESSAGES, variables: { id: '', after: ''} });
                        data.messages.edges.push(createMessage.message);
                        store.writeQuery({ query: FETCH_MESSAGES, variables: { id: '' after: ''}, data });
                    },
                })
            }
        }
    }
};

export default compose(
    fetchGQL,
    createGQL,
)(MessagesView);

As a workaround, I provided default argument values in my query. Providing variables in readQuery and writeQuery didn’t work in my case.

query getPosts($offset: Int = 0, $limit: Int = 10) {
    posts(offset: $offset, limit: $limit) {
        token
        text
        author {
            firstname
            lastname
        }
    }
}

Maybe this is helpful!

Edit:

For required arguments (that cannot have default values) I have to pass variables in update:

query getUserPosts($token: String!, $offset: Int = 0, $limit: Int = 10) {
    posts(token: $token, offset: $offset, limit: $limit) {
        token
        text
        author {
            firstname
            lastname
        }
    }
}
// `token` is passed to custom mutate method
update: (proxy, { data: { createPost }}) => {
    const data = proxy.readQuery({query: getUserPosts, variables: { token }})
    data.posts = [createPost, ...data.posts]
    proxy.writeQuery({query: getUserPosts, variables: { token }, data})
},

@jbaxleyiii Here’s a how it should work, based on my previous comment

How it currently works

  • if a variable is optional you have to give it an initial value e.g. empty string for String and ID types.

Expected:

  • the queries should accept nulls in case optional params are not provided

Example, given that after is an optional cursor variable, then both queries below would return the same result:

// query 1: after as an empty string
const fetchGQL = graphql(FETCH_MESSAGES, {
    options: (ownProps) => {
        const id = ownProps.id || '';
        return {
            variables: { id: id, after: '' }
        };
    },
});

// query 2: `after` here is implicitly assumed to be null. ownProps.id could also be null.
const fetchGQL = graphql(FETCH_MESSAGES, {
    options: (ownProps) => {
        return {
            variables: { id: ownProps.id }
        };
    },
});

Similarly, the following should work based on the same principle:


// `after` and `id` here SHOULD NOT have to be provided as empty strings but currently 
// you have to provide them in order for `readQuery` and `writeQuery` to work
update: (store, { data: { createMessage } }) => {
    // `after` and `id` set as empty strings when calling both readyQuery and writeQuery
    let data = store.readQuery({ query: FETCH_MESSAGES, variables: { id: '', after: ''} });
    data.messages.edges.push(createMessage.message);
    store.writeQuery({ query: FETCH_MESSAGES, variables: { id: '', after: ''}, data });
},

// This is what is expected but it doesn't work.
update: (store, { data: { createMessage } }) => {
    // Notice `after` and `id` here are not provided and are therefore implicitly implied to be null
    let data = store.readQuery({ query: FETCH_MESSAGES });
    data.messages.edges.push(createMessage.message);
    store.writeQuery({ query: FETCH_MESSAGES, data });
},

Is there a way to check if the query is loaded before tying to update it?

@jbaxleyiii I cannot get .readQuery to work at all in 2.0. Using the graphql HoC lets me query just fine, but .readQuery gives me issues. Example detailed below.

Setting up client like:

const apolloClient = new ApolloClient({
  link: new HttpLink({
    uri: '/graphql',
    credentials: 'same-origin',
  }),
  cache: new InMemoryCache(),
});

and then trying to do something like:

  import apolloClient from 'somewhere.js';

  const x = apolloClient.readQuery({
    query: queries.users.findUser, // works fine via HoC

    variables: {
      id: 1,
      after: '', // also tried this as suggested earlier in this issue thread, but it didn't work.
    },
  });

Let me know if I can further help.

For anyone else having this problem when using cursors, I got around this by setting a default empty string value i.e. { variables: after: '' } on both the initial query and the .readQuery