Skip to content

Commit ee738ba

Browse files
committed
Allow client to use post method with persisted query.
1 parent 6e0cc97 commit ee738ba

File tree

7 files changed

+378
-42
lines changed

7 files changed

+378
-42
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ internal val MESSAGE_DIGEST: MessageDigest = MessageDigest.getInstance("SHA-256"
1111
fun GraphQLClientRequest<*>.getQueryId(): String =
1212
String.format(
1313
"%064x",
14-
BigInteger(1, MESSAGE_DIGEST.digest(this.query.toByteArray(StandardCharsets.UTF_8)))
14+
BigInteger(1, MESSAGE_DIGEST.digest(this.query?.toByteArray(StandardCharsets.UTF_8)))
1515
).also {
1616
MESSAGE_DIGEST.reset()
1717
}
1818

1919
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+
)

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
package com.expediagroup.graphql.client.types
1818

1919
data class AutomaticPersistedQueriesSettings(
20-
val enabled: Boolean = false
20+
val enabled: Boolean = false,
21+
val httpMethod: HttpMethod = HttpMethod.GET
2122
) {
2223
companion object {
2324
const val VERSION: Int = 1
2425
}
26+
sealed class HttpMethod {
27+
object GET : HttpMethod()
28+
object POST : HttpMethod()
29+
}
2530
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ 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
32-
val extensions: Any?
33+
val extensions: Map<String, Any>?
3334
get() = null
3435

3536
/**

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

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.ktor
1818

1919
import com.expediagroup.graphql.client.GraphQLClient
2020
import com.expediagroup.graphql.client.extensions.getQueryId
21+
import com.expediagroup.graphql.client.extensions.toExtentionsBodyMap
2122
import com.expediagroup.graphql.client.extensions.toQueryParamString
2223
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
2324
import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer
@@ -58,29 +59,69 @@ open class GraphQLKtorClient(
5859
version = AutomaticPersistedQueriesSettings.VERSION,
5960
sha256Hash = queryId
6061
)
62+
val extensions = request.extensions?.let {
63+
automaticPersistedQueriesExtension.toExtentionsBodyMap().plus(it)
64+
} ?: automaticPersistedQueriesExtension.toExtentionsBodyMap()
6165

62-
val apqRawResultWithoutQuery: String = httpClient.get(url) {
63-
expectSuccess = true
64-
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded)
65-
accept(ContentType.Application.Json)
66-
url {
67-
parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString())
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()
6877
}
69-
}.body()
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+
}
7093

7194
serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let {
7295
if (it.errors.isNullOrEmpty() && it.data != null) return it
7396
}
7497

75-
val apqRawResultWithQuery: String = httpClient.get(url) {
76-
expectSuccess = true
77-
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded)
78-
accept(ContentType.Application.Json)
79-
url {
80-
parameters.append("query", serializer.serialize(request))
81-
parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString())
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()
82110
}
83-
}.body()
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+
}
84125

85126
serializer.deserialize(apqRawResultWithQuery, request.responseType())
86127
} else {

clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.github.tomakehurst.wiremock.WireMockServer
3333
import com.github.tomakehurst.wiremock.client.MappingBuilder
3434
import com.github.tomakehurst.wiremock.client.WireMock
3535
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
36+
import com.github.tomakehurst.wiremock.matching.EqualToJsonPattern
3637
import com.github.tomakehurst.wiremock.matching.EqualToPattern
3738
import io.ktor.client.HttpClient
3839
import io.ktor.client.engine.okhttp.OkHttp
@@ -242,7 +243,7 @@ class GraphQLKtorClientTest {
242243
}
243244

244245
@Test
245-
fun `verifies spring web client can execute query using apq with cached query`() {
246+
fun `verifies spring web client can execute query using GET with persisted query`() {
246247
val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!"))
247248
WireMock.stubFor(
248249
WireMock
@@ -274,7 +275,39 @@ class GraphQLKtorClientTest {
274275
}
275276

276277
@Test
277-
fun `verifies spring web client can execute query using apq with non-cached query`() {
278+
fun `verifies spring web client can execute query using POST with persisted query`() {
279+
val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!"))
280+
WireMock.stubFor(
281+
WireMock
282+
.post("/graphql")
283+
.withHeader("Content-Type", EqualToPattern("application/json"))
284+
.willReturn(
285+
WireMock.aResponse()
286+
.withStatus(200)
287+
.withHeader("Content-Type", "application/json")
288+
.withBody(objectMapper.writeValueAsString(expectedResponse))
289+
)
290+
)
291+
292+
val client = GraphQLKtorClient(
293+
url = URL("${wireMockServer.baseUrl()}/graphql"),
294+
serializer = GraphQLClientJacksonSerializer(),
295+
automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST)
296+
)
297+
298+
runBlocking {
299+
val result: GraphQLClientResponse<HelloWorldResult> = client.execute(HelloWorldQuery())
300+
301+
assertNotNull(result)
302+
assertNotNull(result.data)
303+
assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld)
304+
assertNull(result.errors)
305+
assertNull(result.extensions)
306+
}
307+
}
308+
309+
@Test
310+
fun `verifies spring web client can execute query using GET with not persisted query`() {
278311
val expectedErrorResponse = JacksonGraphQLResponse(
279312
data = null,
280313
errors = listOf(
@@ -331,6 +364,94 @@ class GraphQLKtorClientTest {
331364
}
332365
}
333366

367+
@Test
368+
fun `verifies spring web client can execute query using POST with not persisted query`() {
369+
val expectedErrorResponse = JacksonGraphQLResponse(
370+
data = null,
371+
errors = listOf(
372+
JacksonGraphQLError(
373+
message = "PersistedQueryNotFound"
374+
)
375+
),
376+
extensions = mapOf("persistedQueryId" to "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae", "classification" to "PersistedQueryNotFound")
377+
)
378+
WireMock.stubFor(
379+
WireMock
380+
.post("/graphql")
381+
.withHeader("Content-Type", EqualToPattern("application/json"))
382+
.withRequestBody(
383+
EqualToJsonPattern(
384+
"""
385+
{
386+
"operationName":"HelloWorld",
387+
"extensions": {
388+
"persistedQuery": {
389+
"version": 1,
390+
"sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae"
391+
}
392+
}
393+
}
394+
""".trimIndent(),
395+
true,
396+
true
397+
)
398+
)
399+
.willReturn(
400+
WireMock.aResponse()
401+
.withStatus(200)
402+
.withHeader("Content-Type", "application/json")
403+
.withBody(objectMapper.writeValueAsString(expectedErrorResponse))
404+
)
405+
)
406+
407+
val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!"))
408+
WireMock.stubFor(
409+
WireMock
410+
.post("/graphql")
411+
.withHeader("Content-Type", EqualToPattern("application/json"))
412+
.withRequestBody(
413+
EqualToJsonPattern(
414+
"""
415+
{
416+
"query": "query HelloWorldQuery { helloWorld }",
417+
"operationName":"HelloWorld",
418+
"extensions": {
419+
"persistedQuery": {
420+
"version": 1,
421+
"sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae"
422+
}
423+
}
424+
}
425+
""".trimIndent(),
426+
true,
427+
true
428+
)
429+
)
430+
.willReturn(
431+
WireMock.aResponse()
432+
.withStatus(200)
433+
.withHeader("Content-Type", "application/json")
434+
.withBody(objectMapper.writeValueAsString(expectedResponse))
435+
)
436+
)
437+
438+
val client = GraphQLKtorClient(
439+
url = URL("${wireMockServer.baseUrl()}/graphql"),
440+
serializer = GraphQLClientJacksonSerializer(),
441+
automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST)
442+
)
443+
444+
runBlocking {
445+
val result: GraphQLClientResponse<HelloWorldResult> = client.execute(HelloWorldQuery())
446+
447+
assertNotNull(result)
448+
assertNotNull(result.data)
449+
assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld)
450+
assertNull(result.errors)
451+
assertNull(result.extensions)
452+
}
453+
}
454+
334455
@Test
335456
fun `verifies Non-OK HTTP responses will throw error`() {
336457
WireMock.stubFor(

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

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.spring
1818

1919
import com.expediagroup.graphql.client.GraphQLClient
2020
import com.expediagroup.graphql.client.extensions.getQueryId
21+
import com.expediagroup.graphql.client.extensions.toExtentionsBodyMap
2122
import com.expediagroup.graphql.client.extensions.toQueryParamString
2223
import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer
2324
import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer
@@ -61,33 +62,73 @@ open class GraphQLWebClient(
6162
version = AutomaticPersistedQueriesSettings.VERSION,
6263
sha256Hash = queryId
6364
)
65+
val extensions = request.extensions?.let {
66+
automaticPersistedQueriesExtension.toExtentionsBodyMap().plus(it)
67+
} ?: automaticPersistedQueriesExtension.toExtentionsBodyMap()
6468

65-
val apqRawResultWithoutQuery = client
66-
.get()
67-
.uri {
68-
it.queryParam("extension", "{extension}").build(automaticPersistedQueriesExtension.toQueryParamString())
69+
val apqRawResultWithoutQuery: String = when (automaticPersistedQueriesSettings.httpMethod) {
70+
is AutomaticPersistedQueriesSettings.HttpMethod.GET -> {
71+
client
72+
.get()
73+
.uri {
74+
it.queryParam("extension", "{extension}").build(automaticPersistedQueriesExtension.toQueryParamString())
75+
}
76+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
77+
.accept(MediaType.APPLICATION_JSON)
78+
.retrieve()
79+
.awaitBody()
6980
}
70-
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
71-
.accept(MediaType.APPLICATION_JSON)
72-
.retrieve()
73-
.awaitBody<String>()
81+
82+
is AutomaticPersistedQueriesSettings.HttpMethod.POST -> {
83+
val requestWithoutQuery = object : GraphQLClientRequest<T> by request {
84+
override val query = null
85+
override val extensions = extensions
86+
}
87+
client
88+
.post()
89+
.apply(requestCustomizer)
90+
.accept(MediaType.APPLICATION_JSON)
91+
.contentType(MediaType.APPLICATION_JSON)
92+
.bodyValue(serializer.serialize(requestWithoutQuery))
93+
.retrieve()
94+
.awaitBody()
95+
}
96+
}
7497

7598
serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let {
7699
if (it.errors.isNullOrEmpty() && it.data != null) return it
77100
}
78101

79-
val apqRawResultWithQuery = client
80-
.get()
81-
.uri {
82-
it
83-
.queryParam("query", "{query}")
84-
.queryParam("extension", "{extension}")
85-
.build(serializer.serialize(request), automaticPersistedQueriesExtension.toQueryParamString())
102+
val apqRawResultWithQuery: String = when (automaticPersistedQueriesSettings.httpMethod) {
103+
is AutomaticPersistedQueriesSettings.HttpMethod.GET -> {
104+
client
105+
.get()
106+
.uri {
107+
it
108+
.queryParam("query", "{query}")
109+
.queryParam("extension", "{extension}")
110+
.build(serializer.serialize(request), automaticPersistedQueriesExtension.toQueryParamString())
111+
}
112+
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
113+
.accept(MediaType.APPLICATION_JSON)
114+
.retrieve()
115+
.awaitBody()
86116
}
87-
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
88-
.accept(MediaType.APPLICATION_JSON)
89-
.retrieve()
90-
.awaitBody<String>()
117+
118+
is AutomaticPersistedQueriesSettings.HttpMethod.POST -> {
119+
val requestWithQuery = object : GraphQLClientRequest<T> by request {
120+
override val extensions = extensions
121+
}
122+
client
123+
.post()
124+
.apply(requestCustomizer)
125+
.accept(MediaType.APPLICATION_JSON)
126+
.contentType(MediaType.APPLICATION_JSON)
127+
.bodyValue(serializer.serialize(requestWithQuery))
128+
.retrieve()
129+
.awaitBody()
130+
}
131+
}
91132

92133
serializer.deserialize(apqRawResultWithQuery, request.responseType())
93134
} else {

0 commit comments

Comments
 (0)