-
Notifications
You must be signed in to change notification settings - Fork 19
Persisted Queries
Persisted queries allows clients to use HTTP GET instead of HTTP POST, making it easier to cache in a CDN (which might not allow caching of HTTP POST). The automated part is that the protocol allows clients to register new query id:s on the fly, so they do not have to be known by the server beforehand.
Apollo outlines the protocol in a blog post: Improve GraphQL Performance with Automatic Persisted Queries. And the exact protocol can be found here: Persisted Query support with Apollo Link
Due to the declarative nature of Retrofit services and that the details of the protocol can differ depending on the backend, It would not make sense if this library did the persisted query negotiation under the hood. Therefore it requires some setup on your part.
Since one query potentially require two web requests, there will also be two methods in the Retrofit service declaration
@GET("graphql")
@Headers("Content-Type: application/json")
fun getTrendingPersistedQuery(@Query("extensions") String extensions, @Query("operationName") String operationName, @Query("variables") String variables): Call<GraphContainer<TrendingFeed>>
@POST("graphql")
@GraphQuery("Trending")
@Headers("Content-Type: application/json")
fun getTrending(@Body QueryContainerBuilder queryContainerBuilder): Call<GraphContainer<TrendingFeed>>
Additionally, the protocol relies on the client sending a SHA256 hash calculated from the query. Only the contents of the query, not the whole web request body. You can use the PersistedQueryHashCalculator
for this.
private fun persistedQueryRequest() {
val queryName = "Trending"
val persistedQueryHashCalculator = PersistedQueryHashCalculator(context?.applicationContext)
val queryContainerBuilder = QueryContainerBuilder()
.setOperationName(queryName)
.putVariable("type", feedType)
.putPersistedQueryHash(persistedQueryHashCalculator.getOrCreateAPQHash(queryName).orEmpty())
val container = queryContainerBuilder.build()
val queryParameters: PersistedQueryUrlParameters = PersistedQueryUrlParameterBuilder(container, Gson()).build()
val indexModel = WebFactory.getInstance(context)
.createService(IndexModel::class.java)
val persistedQueryRequest = indexModel.getTrendingUsingPersistedQuery(queryParameters.extensions, queryParameters.operationName, queryParameters.variables)
val fallbackRequest = indexModel.getTrending(queryContainerBuilder)
doPersistedQueryNegotiation(
persistedQueryRequest,
fallbackRequest,
result = { trendingFeed: TrendingFeed? ->
// do something with TrendingFeed
},
error = { error: Throwable? ->
// handle error
}
)
}
Assuming that you use retrofit2.Call
as your way of performing asynchronous web requests using Retrofit, an implementation of the negotiation part could look as such:
private fun <T> doPersistedQueryNegotiation(request: Call<GraphContainer<T>>,
fallbackRequest: Call<GraphContainer<T>>,
result: (T?) -> Unit,
error: (Throwable?) -> Unit) {
request.enqueue(object : retrofit2.Callback<GraphContainer<T>> {
override fun onResponse(call: Call<GraphContainer<T>>, response: Response<GraphContainer<T>>) {
if (shouldUseFallbackRequest(response.body())) {
// using fallback request
fallbackRequest.enqueue(object : retrofit2.Callback<GraphContainer<T>> {
override fun onResponse(call: Call<GraphContainer<T>>, response: Response<GraphContainer<T>>) {
// persisted Query successfully registered
handleResponse(response, result, error)
}
override fun onFailure(call: Call<GraphContainer<T>>, t: Throwable) {
error.invoke(t)
}
})
} else {
// persisted Query responded successfully
handleResponse(response, result, error)
}
}
override fun onFailure(call: Call<GraphContainer<T>>, t: Throwable) {
// Error during initial persisted query request
error.invoke(t)
}
})
}
private fun <T> shouldUseFallbackRequest(graphContainer: GraphContainer<T>?): Boolean {
if (graphContainer!!.errors != null) {
for ((message) in graphContainer.errors!!) {
if (PersistedQueryErrors.APQ_QUERY_NOT_FOUND_ERROR.equals(message!!, ignoreCase = true)) {
// Persisted query not found
return true
} else if (PersistedQueryErrors.APQ_NOT_SUPPORTED_ERROR.equals(message, ignoreCase = true)) {
// Persisted query not supported
return true
}
}
}
return false
}
private fun <T> handleResponse(response: Response<GraphContainer<T>>, result: (T?) -> Unit, error: (Throwable?) -> Unit) {
if (response.isSuccessful && response.body()?.errors.isNullOrEmpty()) {
result.invoke(response.body()?.data)
} else {
error.invoke(HttpException(response))
}
}