thecodingmachine / graphqlite

Use PHP Attributes/Annotations to declare your GraphQL API
https://graphqlite.thecodingmachine.io
MIT License
555 stars 95 forks source link

Apollo Automatic persisted queries #611

Closed oprypkhantc closed 1 year ago

oprypkhantc commented 1 year ago

Closes #566

This PR implements Automatic persisted queries by Apollo:

This diagram shows the process:

image

Essentially, whenever an Apollo client with enabled APQ attempts to execute a query, these are the steps taken:

  1. Given query query { field } needs to be executed, it does hash('sha256', 'query { field }') and sends the resulting hash to the server: /graphql?hash=7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8
  2. Server sees that hash and triggers a persisted query loader, in our case - CachePersistedQueryLoader. It checks if there's a 7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8 entry in cache.
  3. Given there's none, it haults execution and returns a GraphQL error with extensions.code = 'PERSISTED_QUERY_NOT_FOUND'. That tells Apollo clients that persisted queries are supported and it should go to the next step
  4. Apollo client sends BOTH the query and the hash to the server: /graphql?hash=7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8&query=query { field }
  5. Server sees that hash and again triggers a persisted query loader. It once again sees that there's nothing in cache, but this time the full query was provided. It checks if provided hash 7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8 matches the provided query using hash('sha256', 'query { field }') === $hash. If so, the server safely knows that this hash 7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8 will always correspond to query query { field }. It will write it to the cache: cache->set('7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8', 'query { field }'). Afterwards, the request is processed normally, as if the client sent just the query { field } in the first place.
  6. Next time an Apollo client (that or any other) wants to execute query query { field }, they'll also first try executing it with just the hash: /graphql?hash=7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8
  7. This time, the server will actually have a corresponding query for that hash in cache and will simply execute it as if a client sent /graphql?query=query { field }

The reason authentication (or any other variables or headers) doesn't matter here is that only the string query { field } is cached. If two different clients try to use the same persisted query, both requests will still go through the full parsing, validation and execution process and may yield different responses.

Hope that clears it up.

oojacoboo commented 1 year ago

@oprypkhantc thanks, this looks good.

I am wondering, why the client needs to send the sha256 hash and the query, and not just simply the sha256. If this is truly deterministic, as the code is written, why wouldn't the server just set the hash key on the first persisted query request, instead of requiring the client to resubmit the request? It seems like unnecessary communication and complications. The server is already validating the hash.

oprypkhantc commented 1 year ago

The first persisted query request in this case would only contain the hash of the query from the client, say 7b82cd908482825da2a4381cdda62a1384faa0c1b4c248e086aa44aa59fb9cd8, but it doesn't have the query. So if the cache doesn't have a query for that hash, the only option server has is to request the client to provide the query, because it's impossible to extract the query from the hash.

Plus, if your APQ is configured correctly, 99% of those "first" persisted query requests won't require another request from the client because the cache would be populated already from one of the previous requests.

oojacoboo commented 1 year ago

Thanks @oprypkhantc, I'm clear on the implementation and it looks good - merging.

oprypkhantc commented 1 year ago

Awesome @oojacoboo :)