From a46e2c57db5f882bfef8689ba62f1297571e3542 Mon Sep 17 00:00:00 2001 From: Hayato Kihara Date: Sat, 24 Jun 2023 03:57:18 +0900 Subject: [PATCH 1/5] add automatic persisted queries implementation to client --- .../graphql/client/GraphQLClient.kt | 4 +- .../AutomaticPersistedQueriesExtensions.kt | 19 ++++ .../AutomaticPersistedQueriesExtension.kt | 22 +++++ .../AutomaticPersistedQueriesSettings.kt | 27 +++++ .../client/types/GraphQLClientRequest.kt | 4 +- .../graphql/client/ktor/GraphQLKtorClient.kt | 61 ++++++++++-- .../client/ktor/GraphQLKtorClientTest.kt | 99 ++++++++++++++++++- .../graphql/client/spring/GraphQLWebClient.kt | 83 +++++++++++++--- .../client/spring/GraphQLWebClientTest.kt | 99 ++++++++++++++++++- 9 files changed, 394 insertions(+), 24 deletions(-) create mode 100644 clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt create mode 100644 clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt create mode 100644 clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLClient.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLClient.kt index 231a4743f4..53a25fa77e 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLClient.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/GraphQLClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.expediagroup.graphql.client +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse @@ -23,6 +24,7 @@ import com.expediagroup.graphql.client.types.GraphQLClientResponse * A lightweight typesafe GraphQL HTTP client. */ interface GraphQLClient { + val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings /** * Executes [GraphQLClientRequest] and returns corresponding [GraphQLClientResponse]. diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt new file mode 100644 index 0000000000..94a4832880 --- /dev/null +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt @@ -0,0 +1,19 @@ +package com.expediagroup.graphql.client.extensions + +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension +import com.expediagroup.graphql.client.types.GraphQLClientRequest +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +internal val MESSAGE_DIGEST: MessageDigest = MessageDigest.getInstance("SHA-256") + +fun GraphQLClientRequest<*>.getQueryId(): String = + String.format( + "%064x", + BigInteger(1, MESSAGE_DIGEST.digest(this.query.toByteArray(StandardCharsets.UTF_8))) + ).also { + MESSAGE_DIGEST.reset() + } + +fun AutomaticPersistedQueriesExtension.toQueryParamString() = """{"persistedQuery":{"version":$version,"sha256Hash":"$sha256Hash"}}""" diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt new file mode 100644 index 0000000000..6845304d99 --- /dev/null +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.client.types + +interface AutomaticPersistedQueriesExtension { + val version: Int + val sha256Hash: String +} diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt new file mode 100644 index 0000000000..7145d54200 --- /dev/null +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.client.types + +interface AutomaticPersistedQueriesSettings { + val enabled: Boolean + val version: Int +} + +val defaultAutomaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { + override val enabled = false + override val version = 1 +} diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt index 943642e0e8..6d902ca396 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ interface GraphQLClientRequest { get() = null val variables: Any? get() = null + val extensions: Any? + get() = null /** * Parameterized type of a corresponding GraphQLResponse. diff --git a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt index 57c8d172eb..aebd55ddb5 100644 --- a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt +++ b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,27 @@ package com.expediagroup.graphql.client.ktor import com.expediagroup.graphql.client.GraphQLClient +import com.expediagroup.graphql.client.extensions.getQueryId +import com.expediagroup.graphql.client.extensions.toQueryParamString import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse +import com.expediagroup.graphql.client.types.defaultAutomaticPersistedQueriesSettings import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.plugins.expectSuccess import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.accept +import io.ktor.client.request.get +import io.ktor.client.request.header import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders import io.ktor.http.content.TextContent import java.io.Closeable import java.net.URL @@ -39,16 +48,52 @@ import java.net.URL open class GraphQLKtorClient( private val url: URL, private val httpClient: HttpClient = HttpClient(engineFactory = CIO), - private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer() + private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer(), + override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = defaultAutomaticPersistedQueriesSettings ) : GraphQLClient, Closeable { override suspend fun execute(request: GraphQLClientRequest, requestCustomizer: HttpRequestBuilder.() -> Unit): GraphQLClientResponse { - val rawResult: String = httpClient.post(url) { - expectSuccess = true - apply(requestCustomizer) - setBody(TextContent(serializer.serialize(request), ContentType.Application.Json)) - }.body() - return serializer.deserialize(rawResult, request.responseType()) + val queryId = request.getQueryId() + val automaticPersistedQueriesExtension = object : AutomaticPersistedQueriesExtension { + override val version: Int + get() = automaticPersistedQueriesSettings.version + override val sha256Hash: String + get() = queryId + } + + return if (automaticPersistedQueriesSettings.enabled) { + val apqRawResultWithoutQuery: String = httpClient.get(url) { + expectSuccess = true + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + url { + parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + } + }.body() + + serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let { + if (it.errors.isNullOrEmpty() && it.data != null) return it + } + + val apqRawResultWithQuery: String = httpClient.get(url) { + expectSuccess = true + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + url { + parameters.append("query", serializer.serialize(request)) + parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + } + }.body() + + serializer.deserialize(apqRawResultWithQuery, request.responseType()) + } else { + val rawResult: String = httpClient.post(url) { + expectSuccess = true + apply(requestCustomizer) + setBody(TextContent(serializer.serialize(request), ContentType.Application.Json)) + }.body() + serializer.deserialize(rawResult, request.responseType()) + } } override suspend fun execute(requests: List>, requestCustomizer: HttpRequestBuilder.() -> Unit): List> { diff --git a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt index ddda194b53..6c84bddc0c 100644 --- a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt +++ b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import com.expediagroup.graphql.client.serialization.serializers.AnyKSerializer import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLError import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLSourceLocation +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -240,6 +241,102 @@ class GraphQLKtorClientTest { } } + @Test + fun `verifies spring web client can execute query using apq with cached query`() { + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .get("""/graphql?extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D""") + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLKtorClient( + url = URL("${wireMockServer.baseUrl()}/graphql"), + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { + override val enabled = true + override val version = 1 + } + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + + @Test + fun `verifies spring web client can execute query using apq with non-cached query`() { + val expectedErrorResponse = JacksonGraphQLResponse( + data = null, + errors = listOf( + JacksonGraphQLError( + message = "PersistedQueryNotFound" + ) + ), + extensions = mapOf("persistedQueryId" to "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae", "classification" to "PersistedQueryNotFound") + ) + WireMock.stubFor( + WireMock + .get("""/graphql?extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D""") + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedErrorResponse)) + ) + ) + + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .get( + """ + |/graphql?query=%7B%22query%22%3A%22query+HelloWorldQuery+%7B+helloWorld+%7D%22%2C%22operationName%22%3A%22HelloWorld%22%7D& + |extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D + """.trimMargin().replace("\n", "") + ) + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLKtorClient( + url = URL("${wireMockServer.baseUrl()}/graphql"), + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { + override val enabled = true + override val version = 1 + } + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + @Test fun `verifies Non-OK HTTP responses will throw error`() { WireMock.stubFor( diff --git a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt index b8a65003d9..7eb1a87016 100644 --- a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt +++ b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,22 @@ package com.expediagroup.graphql.client.spring import com.expediagroup.graphql.client.GraphQLClient +import com.expediagroup.graphql.client.extensions.getQueryId +import com.expediagroup.graphql.client.extensions.toQueryParamString import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse +import com.expediagroup.graphql.client.types.defaultAutomaticPersistedQueriesSettings import kotlinx.coroutines.reactive.awaitSingle +import org.springframework.http.HttpHeaders import org.springframework.http.MediaType +import org.springframework.web.reactive.function.client.ClientRequest import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBody +import java.net.URI /** * A lightweight typesafe GraphQL HTTP client using Spring WebClient engine. @@ -31,21 +40,71 @@ import org.springframework.web.reactive.function.client.WebClient open class GraphQLWebClient( url: String, private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer(), - builder: WebClient.Builder = WebClient.builder() + builder: WebClient.Builder = WebClient.builder(), + override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = defaultAutomaticPersistedQueriesSettings ) : GraphQLClient { - private val client: WebClient = builder.baseUrl(url).build() + private val client: WebClient = builder + .baseUrl(url) + .filter { request, next -> + val encodedUri = request.url().toString().replace("%20", "+") + val filtered = ClientRequest + .from(request) + .url(URI.create(encodedUri)) + .build() + next.exchange(filtered) + }.build() override suspend fun execute(request: GraphQLClientRequest, requestCustomizer: WebClient.RequestBodyUriSpec.() -> Unit): GraphQLClientResponse { - val rawResult = client.post() - .apply(requestCustomizer) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(serializer.serialize(request)) - .retrieve() - .bodyToMono(String::class.java) - .awaitSingle() - return serializer.deserialize(rawResult, request.responseType()) + val queryId = request.getQueryId() + val automaticPersistedQueriesExtension = object : AutomaticPersistedQueriesExtension { + override val version: Int + get() = automaticPersistedQueriesSettings.version + override val sha256Hash: String + get() = queryId + } + + return if (automaticPersistedQueriesSettings.enabled) { + val apqRawResultWithoutQuery = client + .get() + .uri { + it.queryParam("extension", "{extension}").build(automaticPersistedQueriesExtension.toQueryParamString()) + } + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() + + serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let { + if (it.errors.isNullOrEmpty() && it.data != null) return it + } + + val apqRawResultWithQuery = client + .get() + .uri { + it + .queryParam("query", "{query}") + .queryParam("extension", "{extension}") + .build(serializer.serialize(request), automaticPersistedQueriesExtension.toQueryParamString()) + } + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() + + serializer.deserialize(apqRawResultWithQuery, request.responseType()) + } else { + val rawResult = client.post() + .apply(requestCustomizer) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(serializer.serialize(request)) + .retrieve() + .bodyToMono(String::class.java) + .awaitSingle() + + serializer.deserialize(rawResult, request.responseType()) + } } override suspend fun execute(requests: List>, requestCustomizer: WebClient.RequestBodyUriSpec.() -> Unit): List> { diff --git a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt index 477d015286..94752a69da 100644 --- a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt +++ b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2023 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import com.expediagroup.graphql.client.serialization.serializers.AnyKSerializer import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLError import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse import com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLSourceLocation +import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -247,6 +248,102 @@ class GraphQLWebClientTest { } } + @Test + fun `verifies spring web client can execute query using apq with cached query`() { + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .get("""/graphql?extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D""") + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLWebClient( + url = "${wireMockServer.baseUrl()}/graphql", + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { + override val enabled = true + override val version = 1 + } + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + + @Test + fun `verifies spring web client can execute query using apq with non-cached query`() { + val expectedErrorResponse = JacksonGraphQLResponse( + data = null, + errors = listOf( + JacksonGraphQLError( + message = "PersistedQueryNotFound" + ) + ), + extensions = mapOf("persistedQueryId" to "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae", "classification" to "PersistedQueryNotFound") + ) + WireMock.stubFor( + WireMock + .get("""/graphql?extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D""") + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedErrorResponse)) + ) + ) + + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .get( + """ + |/graphql?query=%7B%22query%22%3A%22query+HelloWorldQuery+%7B+helloWorld+%7D%22%2C%22operationName%22%3A%22HelloWorld%22%7D& + |extension=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae%22%7D%7D + """.trimMargin().replace("\n", "") + ) + .withHeader("Content-Type", EqualToPattern("application/x-www-form-urlencoded")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLWebClient( + url = "${wireMockServer.baseUrl()}/graphql", + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { + override val enabled = true + override val version = 1 + } + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + @Test fun `verifies Non-OK HTTP responses will throw error`() { WireMock.stubFor( From 225bd08114bd6ea7e055df13c25f440a93b285a0 Mon Sep 17 00:00:00 2001 From: Hayato Kihara Date: Sun, 25 Jun 2023 00:17:25 +0900 Subject: [PATCH 2/5] change AutomaticPersistedQueriesExtension and AutomaticPersistedQueriesSettings to data class --- .../types/AutomaticPersistedQueriesExtension.kt | 6 +++--- .../types/AutomaticPersistedQueriesSettings.kt | 14 +++++++------- .../graphql/client/ktor/GraphQLKtorClientTest.kt | 16 ++++++++-------- .../client/spring/GraphQLWebClientTest.kt | 16 ++++++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt index 6845304d99..2c26dbcfa7 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesExtension.kt @@ -16,7 +16,7 @@ package com.expediagroup.graphql.client.types -interface AutomaticPersistedQueriesExtension { - val version: Int +data class AutomaticPersistedQueriesExtension( + val version: Int, val sha256Hash: String -} +) diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt index 7145d54200..d21252c06f 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt @@ -16,12 +16,12 @@ package com.expediagroup.graphql.client.types -interface AutomaticPersistedQueriesSettings { - val enabled: Boolean +data class AutomaticPersistedQueriesSettings( + val enabled: Boolean, val version: Int -} +) -val defaultAutomaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { - override val enabled = false - override val version = 1 -} +val defaultAutomaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( + enabled = false, + version = 1 +) diff --git a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt index 6c84bddc0c..91512d3aee 100644 --- a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt +++ b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt @@ -259,10 +259,10 @@ class GraphQLKtorClientTest { val client = GraphQLKtorClient( url = URL("${wireMockServer.baseUrl()}/graphql"), serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { - override val enabled = true - override val version = 1 - } + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( + enabled = true, + version = 1 + ) ) runBlocking { @@ -320,10 +320,10 @@ class GraphQLKtorClientTest { val client = GraphQLKtorClient( url = URL("${wireMockServer.baseUrl()}/graphql"), serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { - override val enabled = true - override val version = 1 - } + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( + enabled = true, + version = 1 + ) ) runBlocking { diff --git a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt index 94752a69da..7ecb7e8fb6 100644 --- a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt +++ b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt @@ -266,10 +266,10 @@ class GraphQLWebClientTest { val client = GraphQLWebClient( url = "${wireMockServer.baseUrl()}/graphql", serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { - override val enabled = true - override val version = 1 - } + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( + enabled = true, + version = 1 + ) ) runBlocking { @@ -327,10 +327,10 @@ class GraphQLWebClientTest { val client = GraphQLWebClient( url = "${wireMockServer.baseUrl()}/graphql", serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = object : AutomaticPersistedQueriesSettings { - override val enabled = true - override val version = 1 - } + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( + enabled = true, + version = 1 + ) ) runBlocking { From 38c3d4e437d81168c248f284a947e58ebbf3908f Mon Sep 17 00:00:00 2001 From: Hayato Kihara Date: Sun, 25 Jun 2023 00:19:19 +0900 Subject: [PATCH 3/5] move the automaticPersistedQueriesExtension property into apq enabled scope --- .../graphql/client/ktor/GraphQLKtorClient.kt | 14 ++++++-------- .../graphql/client/spring/GraphQLWebClient.kt | 14 ++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt index aebd55ddb5..9dd7a2a551 100644 --- a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt +++ b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt @@ -53,15 +53,13 @@ open class GraphQLKtorClient( ) : GraphQLClient, Closeable { override suspend fun execute(request: GraphQLClientRequest, requestCustomizer: HttpRequestBuilder.() -> Unit): GraphQLClientResponse { - val queryId = request.getQueryId() - val automaticPersistedQueriesExtension = object : AutomaticPersistedQueriesExtension { - override val version: Int - get() = automaticPersistedQueriesSettings.version - override val sha256Hash: String - get() = queryId - } - return if (automaticPersistedQueriesSettings.enabled) { + val queryId = request.getQueryId() + val automaticPersistedQueriesExtension = AutomaticPersistedQueriesExtension( + version = automaticPersistedQueriesSettings.version, + sha256Hash = queryId + ) + val apqRawResultWithoutQuery: String = httpClient.get(url) { expectSuccess = true header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) diff --git a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt index 7eb1a87016..f55a6b08e3 100644 --- a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt +++ b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt @@ -56,15 +56,13 @@ open class GraphQLWebClient( }.build() override suspend fun execute(request: GraphQLClientRequest, requestCustomizer: WebClient.RequestBodyUriSpec.() -> Unit): GraphQLClientResponse { - val queryId = request.getQueryId() - val automaticPersistedQueriesExtension = object : AutomaticPersistedQueriesExtension { - override val version: Int - get() = automaticPersistedQueriesSettings.version - override val sha256Hash: String - get() = queryId - } - return if (automaticPersistedQueriesSettings.enabled) { + val queryId = request.getQueryId() + val automaticPersistedQueriesExtension = AutomaticPersistedQueriesExtension( + version = automaticPersistedQueriesSettings.version, + sha256Hash = queryId + ) + val apqRawResultWithoutQuery = client .get() .uri { From 6e0cc972a7a1faf2e06bed14b19877fb946e67ea Mon Sep 17 00:00:00 2001 From: Hayato Kihara Date: Wed, 28 Jun 2023 03:03:45 +0900 Subject: [PATCH 4/5] Fix defaultAutomaticPersistedQueriesSettings.version to be const. --- .../types/AutomaticPersistedQueriesSettings.kt | 14 ++++++-------- .../graphql/client/ktor/GraphQLKtorClient.kt | 5 ++--- .../graphql/client/ktor/GraphQLKtorClientTest.kt | 10 ++-------- .../graphql/client/spring/GraphQLWebClient.kt | 5 ++--- .../graphql/client/spring/GraphQLWebClientTest.kt | 10 ++-------- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt index d21252c06f..43481d8f22 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt @@ -17,11 +17,9 @@ package com.expediagroup.graphql.client.types data class AutomaticPersistedQueriesSettings( - val enabled: Boolean, - val version: Int -) - -val defaultAutomaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( - enabled = false, - version = 1 -) + val enabled: Boolean = false +) { + companion object { + const val VERSION: Int = 1 + } +} diff --git a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt index 9dd7a2a551..fd1631588a 100644 --- a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt +++ b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt @@ -25,7 +25,6 @@ import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse -import com.expediagroup.graphql.client.types.defaultAutomaticPersistedQueriesSettings import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO @@ -49,14 +48,14 @@ open class GraphQLKtorClient( private val url: URL, private val httpClient: HttpClient = HttpClient(engineFactory = CIO), private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer(), - override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = defaultAutomaticPersistedQueriesSettings + override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings() ) : GraphQLClient, Closeable { override suspend fun execute(request: GraphQLClientRequest, requestCustomizer: HttpRequestBuilder.() -> Unit): GraphQLClientResponse { return if (automaticPersistedQueriesSettings.enabled) { val queryId = request.getQueryId() val automaticPersistedQueriesExtension = AutomaticPersistedQueriesExtension( - version = automaticPersistedQueriesSettings.version, + version = AutomaticPersistedQueriesSettings.VERSION, sha256Hash = queryId ) diff --git a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt index 91512d3aee..d22d2be9f7 100644 --- a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt +++ b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt @@ -259,10 +259,7 @@ class GraphQLKtorClientTest { val client = GraphQLKtorClient( url = URL("${wireMockServer.baseUrl()}/graphql"), serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( - enabled = true, - version = 1 - ) + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true) ) runBlocking { @@ -320,10 +317,7 @@ class GraphQLKtorClientTest { val client = GraphQLKtorClient( url = URL("${wireMockServer.baseUrl()}/graphql"), serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( - enabled = true, - version = 1 - ) + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true) ) runBlocking { diff --git a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt index f55a6b08e3..19fe612c71 100644 --- a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt +++ b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt @@ -25,7 +25,6 @@ import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesExtension import com.expediagroup.graphql.client.types.AutomaticPersistedQueriesSettings import com.expediagroup.graphql.client.types.GraphQLClientRequest import com.expediagroup.graphql.client.types.GraphQLClientResponse -import com.expediagroup.graphql.client.types.defaultAutomaticPersistedQueriesSettings import kotlinx.coroutines.reactive.awaitSingle import org.springframework.http.HttpHeaders import org.springframework.http.MediaType @@ -41,7 +40,7 @@ open class GraphQLWebClient( url: String, private val serializer: GraphQLClientSerializer = defaultGraphQLSerializer(), builder: WebClient.Builder = WebClient.builder(), - override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = defaultAutomaticPersistedQueriesSettings + override val automaticPersistedQueriesSettings: AutomaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings() ) : GraphQLClient { private val client: WebClient = builder @@ -59,7 +58,7 @@ open class GraphQLWebClient( return if (automaticPersistedQueriesSettings.enabled) { val queryId = request.getQueryId() val automaticPersistedQueriesExtension = AutomaticPersistedQueriesExtension( - version = automaticPersistedQueriesSettings.version, + version = AutomaticPersistedQueriesSettings.VERSION, sha256Hash = queryId ) diff --git a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt index 7ecb7e8fb6..11e7b0527f 100644 --- a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt +++ b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt @@ -266,10 +266,7 @@ class GraphQLWebClientTest { val client = GraphQLWebClient( url = "${wireMockServer.baseUrl()}/graphql", serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( - enabled = true, - version = 1 - ) + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true) ) runBlocking { @@ -327,10 +324,7 @@ class GraphQLWebClientTest { val client = GraphQLWebClient( url = "${wireMockServer.baseUrl()}/graphql", serializer = GraphQLClientJacksonSerializer(), - automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings( - enabled = true, - version = 1 - ) + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true) ) runBlocking { From ee738baa9f9982807f88c4957f0924c2c212f046 Mon Sep 17 00:00:00 2001 From: Hayato Kihara Date: Sun, 2 Jul 2023 20:49:46 +0900 Subject: [PATCH 5/5] Allow client to use post method with persisted query. --- .../AutomaticPersistedQueriesExtensions.kt | 8 +- .../AutomaticPersistedQueriesSettings.kt | 7 +- .../client/types/GraphQLClientRequest.kt | 5 +- .../graphql/client/ktor/GraphQLKtorClient.kt | 71 +++++++--- .../client/ktor/GraphQLKtorClientTest.kt | 125 +++++++++++++++++- .../graphql/client/spring/GraphQLWebClient.kt | 79 ++++++++--- .../client/spring/GraphQLWebClientTest.kt | 125 +++++++++++++++++- 7 files changed, 378 insertions(+), 42 deletions(-) diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt index 94a4832880..eedc787f72 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/extensions/AutomaticPersistedQueriesExtensions.kt @@ -11,9 +11,15 @@ internal val MESSAGE_DIGEST: MessageDigest = MessageDigest.getInstance("SHA-256" fun GraphQLClientRequest<*>.getQueryId(): String = String.format( "%064x", - BigInteger(1, MESSAGE_DIGEST.digest(this.query.toByteArray(StandardCharsets.UTF_8))) + BigInteger(1, MESSAGE_DIGEST.digest(this.query?.toByteArray(StandardCharsets.UTF_8))) ).also { MESSAGE_DIGEST.reset() } fun AutomaticPersistedQueriesExtension.toQueryParamString() = """{"persistedQuery":{"version":$version,"sha256Hash":"$sha256Hash"}}""" +fun AutomaticPersistedQueriesExtension.toExtentionsBodyMap() = mapOf( + "persistedQuery" to mapOf( + "version" to version, + "sha256Hash" to sha256Hash + ) +) diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt index 43481d8f22..26f28ad3a3 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/AutomaticPersistedQueriesSettings.kt @@ -17,9 +17,14 @@ package com.expediagroup.graphql.client.types data class AutomaticPersistedQueriesSettings( - val enabled: Boolean = false + val enabled: Boolean = false, + val httpMethod: HttpMethod = HttpMethod.GET ) { companion object { const val VERSION: Int = 1 } + sealed class HttpMethod { + object GET : HttpMethod() + object POST : HttpMethod() + } } diff --git a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt index 6d902ca396..a9ef6a68ca 100644 --- a/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt +++ b/clients/graphql-kotlin-client/src/main/kotlin/com/expediagroup/graphql/client/types/GraphQLClientRequest.kt @@ -24,12 +24,13 @@ import kotlin.reflect.KClass * @see [GraphQL Over HTTP](https://graphql.org/learn/serving-over-http/#post-request) for additional details */ interface GraphQLClientRequest { - val query: String + val query: String? + get() = null val operationName: String? get() = null val variables: Any? get() = null - val extensions: Any? + val extensions: Map? get() = null /** diff --git a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt index fd1631588a..f7e4a9e36d 100644 --- a/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt +++ b/clients/graphql-kotlin-ktor-client/src/main/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClient.kt @@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.ktor import com.expediagroup.graphql.client.GraphQLClient import com.expediagroup.graphql.client.extensions.getQueryId +import com.expediagroup.graphql.client.extensions.toExtentionsBodyMap import com.expediagroup.graphql.client.extensions.toQueryParamString import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer @@ -58,29 +59,69 @@ open class GraphQLKtorClient( version = AutomaticPersistedQueriesSettings.VERSION, sha256Hash = queryId ) + val extensions = request.extensions?.let { + automaticPersistedQueriesExtension.toExtentionsBodyMap().plus(it) + } ?: automaticPersistedQueriesExtension.toExtentionsBodyMap() - val apqRawResultWithoutQuery: String = httpClient.get(url) { - expectSuccess = true - header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) - accept(ContentType.Application.Json) - url { - parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + val apqRawResultWithoutQuery: String = when (automaticPersistedQueriesSettings.httpMethod) { + is AutomaticPersistedQueriesSettings.HttpMethod.GET -> { + httpClient + .get(url) { + expectSuccess = true + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + url { + parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + } + }.body() } - }.body() + + is AutomaticPersistedQueriesSettings.HttpMethod.POST -> { + val requestWithoutQuery = object : GraphQLClientRequest by request { + override val query = null + override val extensions = extensions + } + httpClient + .post(url) { + expectSuccess = true + apply(requestCustomizer) + accept(ContentType.Application.Json) + setBody(TextContent(serializer.serialize(requestWithoutQuery), ContentType.Application.Json)) + }.body() + } + } serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let { if (it.errors.isNullOrEmpty() && it.data != null) return it } - val apqRawResultWithQuery: String = httpClient.get(url) { - expectSuccess = true - header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) - accept(ContentType.Application.Json) - url { - parameters.append("query", serializer.serialize(request)) - parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + val apqRawResultWithQuery: String = when (automaticPersistedQueriesSettings.httpMethod) { + is AutomaticPersistedQueriesSettings.HttpMethod.GET -> { + httpClient + .get(url) { + expectSuccess = true + header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + url { + parameters.append("query", serializer.serialize(request)) + parameters.append("extension", automaticPersistedQueriesExtension.toQueryParamString()) + } + }.body() } - }.body() + + is AutomaticPersistedQueriesSettings.HttpMethod.POST -> { + val requestWithQuery = object : GraphQLClientRequest by request { + override val extensions = extensions + } + httpClient + .post(url) { + expectSuccess = true + apply(requestCustomizer) + accept(ContentType.Application.Json) + setBody(TextContent(serializer.serialize(requestWithQuery), ContentType.Application.Json)) + }.body() + } + } serializer.deserialize(apqRawResultWithQuery, request.responseType()) } else { diff --git a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt index d22d2be9f7..ebd64aad66 100644 --- a/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt +++ b/clients/graphql-kotlin-ktor-client/src/test/kotlin/com/expediagroup/graphql/client/ktor/GraphQLKtorClientTest.kt @@ -33,6 +33,7 @@ import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.MappingBuilder import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.matching.EqualToJsonPattern import com.github.tomakehurst.wiremock.matching.EqualToPattern import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp @@ -242,7 +243,7 @@ class GraphQLKtorClientTest { } @Test - fun `verifies spring web client can execute query using apq with cached query`() { + fun `verifies spring web client can execute query using GET with persisted query`() { val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) WireMock.stubFor( WireMock @@ -274,7 +275,39 @@ class GraphQLKtorClientTest { } @Test - fun `verifies spring web client can execute query using apq with non-cached query`() { + fun `verifies spring web client can execute query using POST with persisted query`() { + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLKtorClient( + url = URL("${wireMockServer.baseUrl()}/graphql"), + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST) + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + + @Test + fun `verifies spring web client can execute query using GET with not persisted query`() { val expectedErrorResponse = JacksonGraphQLResponse( data = null, errors = listOf( @@ -331,6 +364,94 @@ class GraphQLKtorClientTest { } } + @Test + fun `verifies spring web client can execute query using POST with not persisted query`() { + val expectedErrorResponse = JacksonGraphQLResponse( + data = null, + errors = listOf( + JacksonGraphQLError( + message = "PersistedQueryNotFound" + ) + ), + extensions = mapOf("persistedQueryId" to "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae", "classification" to "PersistedQueryNotFound") + ) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .withRequestBody( + EqualToJsonPattern( + """ + { + "operationName":"HelloWorld", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae" + } + } + } + """.trimIndent(), + true, + true + ) + ) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedErrorResponse)) + ) + ) + + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .withRequestBody( + EqualToJsonPattern( + """ + { + "query": "query HelloWorldQuery { helloWorld }", + "operationName":"HelloWorld", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae" + } + } + } + """.trimIndent(), + true, + true + ) + ) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLKtorClient( + url = URL("${wireMockServer.baseUrl()}/graphql"), + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST) + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + @Test fun `verifies Non-OK HTTP responses will throw error`() { WireMock.stubFor( diff --git a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt index 19fe612c71..1518f5b446 100644 --- a/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt +++ b/clients/graphql-kotlin-spring-client/src/main/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClient.kt @@ -18,6 +18,7 @@ package com.expediagroup.graphql.client.spring import com.expediagroup.graphql.client.GraphQLClient import com.expediagroup.graphql.client.extensions.getQueryId +import com.expediagroup.graphql.client.extensions.toExtentionsBodyMap import com.expediagroup.graphql.client.extensions.toQueryParamString import com.expediagroup.graphql.client.serializer.GraphQLClientSerializer import com.expediagroup.graphql.client.serializer.defaultGraphQLSerializer @@ -61,33 +62,73 @@ open class GraphQLWebClient( version = AutomaticPersistedQueriesSettings.VERSION, sha256Hash = queryId ) + val extensions = request.extensions?.let { + automaticPersistedQueriesExtension.toExtentionsBodyMap().plus(it) + } ?: automaticPersistedQueriesExtension.toExtentionsBodyMap() - val apqRawResultWithoutQuery = client - .get() - .uri { - it.queryParam("extension", "{extension}").build(automaticPersistedQueriesExtension.toQueryParamString()) + val apqRawResultWithoutQuery: String = when (automaticPersistedQueriesSettings.httpMethod) { + is AutomaticPersistedQueriesSettings.HttpMethod.GET -> { + client + .get() + .uri { + it.queryParam("extension", "{extension}").build(automaticPersistedQueriesExtension.toQueryParamString()) + } + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() } - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .awaitBody() + + is AutomaticPersistedQueriesSettings.HttpMethod.POST -> { + val requestWithoutQuery = object : GraphQLClientRequest by request { + override val query = null + override val extensions = extensions + } + client + .post() + .apply(requestCustomizer) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(serializer.serialize(requestWithoutQuery)) + .retrieve() + .awaitBody() + } + } serializer.deserialize(apqRawResultWithoutQuery, request.responseType()).let { if (it.errors.isNullOrEmpty() && it.data != null) return it } - val apqRawResultWithQuery = client - .get() - .uri { - it - .queryParam("query", "{query}") - .queryParam("extension", "{extension}") - .build(serializer.serialize(request), automaticPersistedQueriesExtension.toQueryParamString()) + val apqRawResultWithQuery: String = when (automaticPersistedQueriesSettings.httpMethod) { + is AutomaticPersistedQueriesSettings.HttpMethod.GET -> { + client + .get() + .uri { + it + .queryParam("query", "{query}") + .queryParam("extension", "{extension}") + .build(serializer.serialize(request), automaticPersistedQueriesExtension.toQueryParamString()) + } + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() } - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .awaitBody() + + is AutomaticPersistedQueriesSettings.HttpMethod.POST -> { + val requestWithQuery = object : GraphQLClientRequest by request { + override val extensions = extensions + } + client + .post() + .apply(requestCustomizer) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(serializer.serialize(requestWithQuery)) + .retrieve() + .awaitBody() + } + } serializer.deserialize(apqRawResultWithQuery, request.responseType()) } else { diff --git a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt index 11e7b0527f..7ebcbdca46 100644 --- a/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt +++ b/clients/graphql-kotlin-spring-client/src/test/kotlin/com/expediagroup/graphql/client/spring/GraphQLWebClientTest.kt @@ -33,6 +33,7 @@ import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.MappingBuilder import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.matching.EqualToJsonPattern import com.github.tomakehurst.wiremock.matching.EqualToPattern import io.netty.channel.ChannelOption import io.netty.handler.timeout.ReadTimeoutException @@ -249,7 +250,7 @@ class GraphQLWebClientTest { } @Test - fun `verifies spring web client can execute query using apq with cached query`() { + fun `verifies spring web client can execute query using GET with persisted query`() { val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) WireMock.stubFor( WireMock @@ -281,7 +282,39 @@ class GraphQLWebClientTest { } @Test - fun `verifies spring web client can execute query using apq with non-cached query`() { + fun `verifies spring web client can execute query using POST with persisted query`() { + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLWebClient( + url = "${wireMockServer.baseUrl()}/graphql", + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST) + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + + @Test + fun `verifies spring web client can execute query using GET with not persisted query`() { val expectedErrorResponse = JacksonGraphQLResponse( data = null, errors = listOf( @@ -338,6 +371,94 @@ class GraphQLWebClientTest { } } + @Test + fun `verifies spring web client can execute query using POST with not persisted query`() { + val expectedErrorResponse = JacksonGraphQLResponse( + data = null, + errors = listOf( + JacksonGraphQLError( + message = "PersistedQueryNotFound" + ) + ), + extensions = mapOf("persistedQueryId" to "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae", "classification" to "PersistedQueryNotFound") + ) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .withRequestBody( + EqualToJsonPattern( + """ + { + "operationName":"HelloWorld", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae" + } + } + } + """.trimIndent(), + true, + true + ) + ) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedErrorResponse)) + ) + ) + + val expectedResponse = JacksonGraphQLResponse(data = HelloWorldResult("Hello World!")) + WireMock.stubFor( + WireMock + .post("/graphql") + .withHeader("Content-Type", EqualToPattern("application/json")) + .withRequestBody( + EqualToJsonPattern( + """ + { + "query": "query HelloWorldQuery { helloWorld }", + "operationName":"HelloWorld", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "dd79d72356e3cfd09a542b572c3c73e4e8d90c1c7d5c27d74bcff4e7423178ae" + } + } + } + """.trimIndent(), + true, + true + ) + ) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(objectMapper.writeValueAsString(expectedResponse)) + ) + ) + + val client = GraphQLWebClient( + url = "${wireMockServer.baseUrl()}/graphql", + serializer = GraphQLClientJacksonSerializer(), + automaticPersistedQueriesSettings = AutomaticPersistedQueriesSettings(enabled = true, httpMethod = AutomaticPersistedQueriesSettings.HttpMethod.POST) + ) + + runBlocking { + val result: GraphQLClientResponse = client.execute(HelloWorldQuery()) + + assertNotNull(result) + assertNotNull(result.data) + assertEquals(expectedResponse.data?.helloWorld, result.data?.helloWorld) + assertNull(result.errors) + assertNull(result.extensions) + } + } + @Test fun `verifies Non-OK HTTP responses will throw error`() { WireMock.stubFor(