Skip to content
New issue

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

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

Already on GitHub? # to your account

Aliased field is cached differently based on order #12283

Closed
ItaiYosephi opened this issue Jan 19, 2025 · 5 comments
Closed

Aliased field is cached differently based on order #12283

ItaiYosephi opened this issue Jan 19, 2025 · 5 comments
Labels
🏓 awaiting-contributor-response requires input from a contributor

Comments

@ItaiYosephi
Copy link

ItaiYosephi commented Jan 19, 2025

Issue Description

In our app, we sometime use alias to get the full details of objects the might be deleted, and the actual field to get just the id, which is always returned.
Seems like the cache behaves differently based on the order.
For example, let's say that personDetails is returned null and person returns {id: "1"}.
if queried in the following order, we get null on both fields, even though the server did return value for person

  query GetPerson($id: ID!) {
    person(id: $id) {
      id
    }

    personDetails: person(id: $id) {
      id
      name
    }
  }

If the order is flipped, it seems work as expected:

  query GetPerson($id: ID!) {

    personDetails: person(id: $id) {
      id
      name
    }

    person(id: $id) {
      id
    }

  }

Link to Reproduction

https://codesandbox.io/p/devbox/distracted-neumann-cnz2rt?workspaceId=ws_3d3S1CmTc7BCJtbnRB7BQC

Reproduction Steps

No response

@apollo/client version

3.11.8

@phryneas
Copy link
Member

I think you might be breaking a base assumption of GraphQL about data integrity here - I don't think that this response is valid in the world of GraphQL:

{
  "personDetails": null,
  "person": {
    "id": "2",
    "__typename": "Person"
  }
}

(assuming that both personDetails and person are retrieved within the same query via person(id: $id) with $id being equal).

Since both of these have the same source field with the same arguments, they describe the same object - if you were to return null on one of them but not on the other, that might probably be acceptable if you were sending an error from the server, but even then it might already be problematic.

The cache certainly doesn't handle the case - we assume that if you return person(id: $id) with the same id twice, we can write those to the cache and there won't be any conflicts between those values, since they describe the same object.

To me this feels as if you're working around a problem, probably using the wrong tool for the job, based on a prior assumption (a bit of an xy-problem maybe).

Could you explain a bit more why you are actually doing this? Maybe we can identify a better tool for the job together.

@phryneas phryneas added the 🏓 awaiting-contributor-response requires input from a contributor label Jan 20, 2025
@ItaiYosephi
Copy link
Author

Hi @phryneas , thanks for you response!
It's a little strange that the order affects this behavior, this is from the reproduction:

Image
As you can see, the Get Person Fixed query behaves as I expected.

Let me clarify our use case. Subgraph A attempts to resolve an entity from another subgraph, B. If the entity has been deleted, subgraph B returns null. In our scenario, we want to inform users that the entity was deleted (as opposed to never existing) and display the ID of the deleted entity. We achieve this by using a streamlined query that requests only the id, ensuring that subgraph B is not required for resolution.

@github-actions github-actions bot removed the 🏓 awaiting-contributor-response requires input from a contributor label Jan 21, 2025
@phryneas
Copy link
Member

phryneas commented Jan 21, 2025

I've seen the reproduction before I answered yesterday - I'm not surprised by the result. Your response contains conflicting information.

I'll lay it out a bit more:

The order of operations here is

  • response get written to the cache.
  • data from the cache is delivered to your component

Or more closely - let's assume that $id is 5:

In case A:

  • The field person with the argument id: 5 will be saved in the cache on the Root Query object. It's normalizable so it's saved as a reference:
    RootQuery.person(id: 5) = { __ref: "Person:5" }

  • The field person with the argument id: 5 will be saved in the cache on the Root Query object: RootQuery.person(id: 5) = null

  • The value is read from the cache - it's null - null

  • The value is read from the cache - it's null - null

In case B:

  • The field person with the argument id: 5 will be saved in the cache on the Root Query object: RootQuery.person(id: 5) = null

  • The field person with the argument id: 5 will be saved in the cache on the Root Query object. It's normalizable so it's saved as a reference:
    RootQuery.person(id: 5) = { __ref: "Person:5" }

  • The value is read from the cache - you have missing fields: null. (Honestly I'm surprised that the cache doesn't detect missing data and reruns the query over the network to get the data at that point, you might look for stray network requests!)

  • The value is read from the cache - you have enough fields in the cache: { __typename: 'Person', id: 5 }


The fact that you are aliasing it in the query is a transport-level concern, but doesn't matter for storage. The cache doesn't care about the alias, otherwise you could accidentally use aliases to overwrite other fields by their name or things like that. Imagine a query like { id: age } - that would wreak havoc in the cache, clustering all returned persons of age 37 into one person, overwriting the real person 37.
The cache always saves in the name of the real field.


Let me clarify our use case. Subgraph A attempts to resolve an entity from another subgraph, B. If the entity has been deleted, subgraph B returns null. In our scenario, we want to inform users that the entity was deleted (as opposed to never existing) and display the ID of the deleted entity. We achieve this by using a streamlined query that requests only the id, ensuring that subgraph B is not required for resolution.

Honestly, that sounds like an abuse of an implementation detail - as I said, we assume that a GraphQL response is consistent within itself and this will break that assumption.

I would suggest that you create a new Query field like didThisPersonEverExist(id: $id) (yes, naming things is hard) instead as that will keep your result consistent and won't break the cache.

@phryneas phryneas added the 🏓 awaiting-contributor-response requires input from a contributor label Jan 21, 2025
@jerelmiller
Copy link
Member

Since this is not an issue with Apollo Client, I'm going to go ahead and close this issue. Please let us know if you need any more help. Thanks!

Copy link
Contributor

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
🏓 awaiting-contributor-response requires input from a contributor
Projects
None yet
Development

No branches or pull requests

3 participants