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

Created on 17 Aug 2017  路  34Comments  路  Source: apollographql/apollo-client

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

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

馃悶 bug

Most helpful comment

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.

All 34 comments

@stubailo, there you go 馃毝

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

Same issue here. I only got readQuery to work once I specified all the same arguments as the original query, including empty fields in the variables argument of readQuery.

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

same problem here

I have this problem when not passing in an optional offset variable that is passed to the skip filter

Thanks for the reproduction! I will take a look!

@mathiasjakobsen this appears to be working correctly in the 2.0? Can you verify?

I wrote a test for this issue which is passing on the 2.0 branch. Am I missing something of how you are using readQuery? test

I get the error when I go to edit record trough the url directly without to visit the page list , where the ROOT_QUERY is updated
no way to check if the query is loaded before tying to update it?, version 2.0 looks unstable and not a lot of documentation, can take a lot of weeks to have a 2.0 version stable.

UPDATE: looking at the store. I've found a method

const tableListObj = 'customers';
update: (store, { data: { [tableAction]: reponseRecord } }) => {
if (store.data.ROOT_QUERY[tableListObj]) {
const data = store.readQuery({ query: gqlsCustomer.gqlCustomerList });
if (action === 'Update') {
data.customers.filter(record => (record.id === reponseRecord.id))[0] = reponseRecord;
} else if (action === 'Add') {
data.customers.push(reponseRecord);
}
store.writeQuery({ query: gqlsCustomer.gqlCustomerList, data });

but I don't know if for the v2 is valid.

@odero I did not understand what id after: ''

@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.

@tsnieman we have quite a few tests for this including a new set for the 2.0 with this reported issue. I really need a reproduction to test against at this point

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);

@odero thank you for providing this code! Would you also add a block of how you think it should work? Then we can write it as a test case and work to improve!

@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 });
},

@odero thanks so much! I'll see what I can do!

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Apollo Client!

Any updates? I recently got a crash in my app on this line and I suspect it's related. What's worse is that it seems to be ignoring the try/catch block. Error is Can't find field messages({"first":50}) on object (Conversation:5ee127ef-9ec6-4cc1-a4ca-61f69c896d84) { "uuid": "5ee127ef-9ec6-4cc1-a4ca-61f69c896d84", "__typename": "Conversation" }.

try {
聽 const result: ConversationDetailQuery = apolloClient.readQuery({
聽 query: conversationDetailQuery,
聽 variables: { uuid: conversationUuid },
聽 });
  ....
} catch (e) {}

@danieljvdm FWIW, try not using the standard try/catch mechanism, rather use .then().catch() pattern for the apollo Mutation, it seems to work better

Has this been fixed yet? It is definitely still an issue.

I basically have two pages. A page that lists all items and a page that lists saved items. On the saved items page, I have no trouble using readQuery and writeQuery to update based on what the user is saving (or un-saving).

However, on the general list page, as soon as I try to readQuery, with a variable that is their userId, it fails on a totally unrelated query, saying it can't find that variable. Well, of course you can't.

My solution was to simply passively call that query on that page. Once I did that, it could magically find it without tripping up on the other queries on that page.

But this is bulky and inefficient.

Please fix this

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.

+1

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})
},

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", 

+1

+1

+1

+1

+1

Any update on this?

I'm not sure this is an issue any longer with the @connection(key: "whatever") rollout. See https://www.apollographql.com/docs/react/features/pagination.html

readFromStore.js
image

objId shoud be string... In my case with error

Error: Can't find field href on object ([object Object]) undefined

objId is typeof object 馃槙

image

The original issue here has been fixed. If anyone is still encountering this problem with a current day version of Apollo Client, please post back with a small runnable reproduction that demonstrates this issue. Thanks!

I had the same issue.
Usually I did something like this:

<Component>
   <TableHeader>
      <Mutation update={...}> { /* update the query called below in Component. */}
         {...}
      </Mutation>
   </TableHeader>
   <Query query={...}>
      <TableContent data={...} />
   </Query>
</Component>

Where TableHeader and TableContent are two different components in their own files.

This used to work.

For a separate use case I had to move the Query inside the TableContent. Which caused the TableHeader to throw the error mentioned in this issue. Apparently, the Query has to be made in the respective component or it's parent component if the Mutation's update method is to access that.

This takes away the global store pattern in my opinion. Wasn't expecting this behaviour. The workaround that I have right now is calling the Query with cache-first (or no-network) policy from the TableHeader. Which doesn't look very neat.

Is there a better way to tackle this kind of behaviour?

Was this page helpful?
0 / 5 - 0 ratings