Skip to content

Commit 707731a

Browse files
authored
add automatic persisted queries implementation to client (#1802)
### 📝 Description - add apq interfaces to client - add apq implementation to ktor client - add apq implementation to spring client ### 🔗 Related Issues #1640
1 parent e7ea357 commit 707731a

File tree

9 files changed

+711
-25
lines changed

9 files changed

+711
-25
lines changed

clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLClient.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2023 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,15 @@
1616

1717
package com.expediagroup.graphql.client
1818

19+
import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings
1920
import com.expediagroup.graphql.client.types.GraphQLClientRequest
2021
import com.expediagroup.graphql.client.types.GraphQLClientResponse
2122

2223
/**
2324
* A lightweight typesafe GraphQL HTTP client.
2425
*/
2526
interface GraphQLClient<RequestCustomizer> {
27+
val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings
2628

2729
/**
2830
* Executes [GraphQLClientRequest] and returns corresponding [GraphQLClientResponse].
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.expediagroup.graphql.client.extensions
2+
3+
import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension
4+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
5+
import java.math.BigInteger
6+
import java.nio.charset.StandardCharsets
7+
import java.security.MessageDigest
8+
9+
internal val MESSAGE_DIGEST: MessageDigest = MessageDigest.getInstance("SHA-256")
10+
11+
fun GraphQLClientRequest<*>.getQueryId(): String =
12+
String.format(
13+
"%064x",
14+
BigInteger(1, MESSAGE_DIGEST.digest(this.query?.toByteArray(StandardCharsets.UTF_8)))
15+
).also {
16+
MESSAGE_DIGEST.reset()
17+
}
18+
19+
fun AutomaticPersistedQueriesExtension.toQueryParamString() = """{"persistedQuery":{"version":$version,"sha256Hash":"$sha256Hash"}}"""
20+
fun AutomaticPersistedQueriesExtension.toExtentionsBodyMap() = mapOf(
21+
"persistedQuery" to mapOf(
22+
"version" to version,
23+
"sha256Hash" to sha256Hash
24+
)
25+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.client.types
18+
19+
data class AutomaticPersistedQueriesExtension(
20+
val version: Int,
21+
val sha256Hash: String
22+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.client.types
18+
19+
data class AutomaticPersistedQueriesSettings(
20+
val enabled: Boolean = false,
21+
val httpMethod: HttpMethod = HttpMethod.GET
22+
) {
23+
companion object {
24+
const val VERSION: Int = 1
25+
}
26+
sealed class HttpMethod {
27+
object GET : HttpMethod()
28+
object POST : HttpMethod()
29+
}
30+
}

clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2023 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,11 +24,14 @@ import kotlin.reflect.KClass
2424
* @see [GraphQL Over HTTP](https://graphql.org/learn/serving-over-http/#post-request) for additional details
2525
*/
2626
interface GraphQLClientRequest<T : Any> {
27-
val query: String
27+
val query: String?
28+
get() = null
2829
val operationName: String?
2930
get() = null
3031
val variables: Any?
3132
get() = null
33+
val extensions: Map<String, Any>?
34+
get() = null
3235

3336
/**
3437
* Parameterized type of a corresponding GraphQLResponse.

clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt

+91-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,18 +17,27 @@
1717
package com.expediagroup.graphql.client.ktor
1818

1919
import com.expediagroup.graphql.client.GraphQLClient
20+
import com.expediagroup.graphql.client.extensions.getQueryId
21+
import com.expediagroup.graphql.client.extensions.toExtentionsBodyMap
22+
import com.expediagroup.graphql.client.extensions.toQueryParamString
2023
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
2124
import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer
25+
import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension
26+
import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings
2227
import com.expediagroup.graphql.client.types.GraphQLClientRequest
2328
import com.expediagroup.graphql.client.types.GraphQLClientResponse
2429
import io.ktor.client.HttpClient
2530
import io.ktor.client.call.body
2631
import io.ktor.client.engine.cio.CIO
2732
import io.ktor.client.plugins.expectSuccess
2833
import io.ktor.client.request.HttpRequestBuilder
34+
import io.ktor.client.request.accept
35+
import io.ktor.client.request.get
36+
import io.ktor.client.request.header
2937
import io.ktor.client.request.post
3038
import io.ktor.client.request.setBody
3139
import io.ktor.http.ContentType
40+
import io.ktor.http.HttpHeaders
3241
import io.ktor.http.content.TextContent
3342
import java.io.Closeable
3443
import java.net.URL
@@ -39,16 +48,90 @@ import java.net.URL
3948
open class GraphQLKtorClient(
4049
private val url: URL,
4150
private val httpClient: HttpClient = HttpClient(engineFactory = CIO),
42-
private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer()
51+
private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer(),
52+
override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings()
4353
) : GraphQLClient<HttpRequestBuilder>, Closeable {
4454

4555
override suspend fun <T : Any> execute(request: GraphQLClientRequest<T>, requestCustomizer: HttpRequestBuilder.() -> Unit): GraphQLClientResponse<T> {
46-
val rawResult: String = httpClient.post(url) {
47-
expectSuccess = true
48-
apply(requestCustomizer)
49-
setBody(TextContent(serializer.serialize(request), ContentType.Application.Json))
50-
}.body()
51-
return serializer.deserialize(rawResult, request.responseType())
56+
return if (automaticPersistedQueriesSettings.enabled) {
57+
val queryId = request.getQueryId()
58+
val automaticPersistedQueriesExtension = AutomaticPersistedQueriesExtension(
59+
version = AutomaticPersistedQueriesSettings.VERSION,
60+
sha256Hash = queryId
61+
)
62+
val extensions = request.extensions?.let {
63+
automaticPersistedQueriesExtension.toExtentionsBodyMap().plus(it)
64+
} ?: automaticPersistedQueriesExtension.toExtentionsBodyMap()
65+
66+
val apqRawResultWithoutQuery: String = when (automaticPersistedQueriesSettings.httpMethod) {
67+
is AutomaticPersistedQueriesSettings.HttpMethod.GET -> {
68+
httpClient
69+
.get(url) {
70+
expectSuccess = true
71+
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded)
72+
accept(ContentType.Application.Json)
73+
url {
74+
parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString())
75+
}
76+
}.body()
77+
}
78+
79+
is AutomaticPersistedQueriesSettings.HttpMethod.POST -> {
80+
val requestWithoutQuery = object : GraphQLClientRequest<T> by request {
81+
override val query = null
82+
override val extensions = extensions
83+
}
84+
httpClient
85+
.post(url) {
86+
expectSuccess = true
87+
apply(requestCustomizer)
88+
accept(ContentType.Application.Json)
89+
setBody(TextContent(serializer.serialize(requestWithoutQuery), ContentType.Application.Json))
90+
}.body()
91+
}
92+
}
93+
94+
serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let {
95+
if (it.errors.isNullOrEmpty() && it.data != null) return it
96+
}
97+
98+
val apqRawResultWithQuery: String = when (automaticPersistedQueriesSettings.httpMethod) {
99+
is AutomaticPersistedQueriesSettings.HttpMethod.GET -> {
100+
httpClient
101+
.get(url) {
102+
expectSuccess = true
103+
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded)
104+
accept(ContentType.Application.Json)
105+
url {
106+
parameters.append("query", serializer.serialize(request))
107+
parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString())
108+
}
109+
}.body()
110+
}
111+
112+
is AutomaticPersistedQueriesSettings.HttpMethod.POST -> {
113+
val requestWithQuery = object : GraphQLClientRequest<T> by request {
114+
override val extensions = extensions
115+
}
116+
httpClient
117+
.post(url) {
118+
expectSuccess = true
119+
apply(requestCustomizer)
120+
accept(ContentType.Application.Json)
121+
setBody(TextContent(serializer.serialize(requestWithQuery), ContentType.Application.Json))
122+
}.body()
123+
}
124+
}
125+
126+
serializer.deserialize(apqRawResultWithQuery, request.responseType())
127+
} else {
128+
val rawResult: String = httpClient.post(url) {
129+
expectSuccess = true
130+
apply(requestCustomizer)
131+
setBody(TextContent(serializer.serialize(request), ContentType.Application.Json))
132+
}.body()
133+
serializer.deserialize(rawResult, request.responseType())
134+
}
52135
}
53136

54137
override suspend fun execute(requests: List<GraphQLClientRequest<*>>, requestCustomizer: HttpRequestBuilder.() -> Unit): List<GraphQLClientResponse<*>> {

0 commit comments

Comments
 (0)