diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/CustomMonoGraphQLClient.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/CustomMonoGraphQLClient.kt index 49028034d..18ee43553 100644 --- a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/CustomMonoGraphQLClient.kt +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/CustomMonoGraphQLClient.kt @@ -16,6 +16,7 @@ package com.netflix.graphql.dgs.client +import org.intellij.lang.annotations.Language import reactor.core.publisher.Mono /** @@ -23,17 +24,20 @@ import reactor.core.publisher.Mono * The user is responsible for doing the actual HTTP request, making this pluggable with any HTTP client. * For a more convenient option, use [WebClientGraphQLClient] instead. */ -class CustomMonoGraphQLClient(private val url: String, private val monoRequestExecutor: MonoRequestExecutor) : MonoGraphQLClient { - override fun reactiveExecuteQuery(query: String): Mono { +class CustomMonoGraphQLClient( + private val url: String, + private val monoRequestExecutor: MonoRequestExecutor +) : MonoGraphQLClient { + override fun reactiveExecuteQuery(@Language("graphql") query: String): Mono { return reactiveExecuteQuery(query, emptyMap(), null) } - override fun reactiveExecuteQuery(query: String, variables: Map): Mono { + override fun reactiveExecuteQuery(@Language("graphql") query: String, variables: Map): Mono { return reactiveExecuteQuery(query, variables, null) } override fun reactiveExecuteQuery( - query: String, + @Language("graphql") query: String, variables: Map, operationName: String? ): Mono { diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClient.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClient.kt index a5bac8207..32b823476 100644 --- a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClient.kt +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClient.kt @@ -16,19 +16,6 @@ package com.netflix.graphql.dgs.client -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.util.ClassUtils -import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.server.ResponseStatusException -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import java.util.function.Consumer - /** * GraphQL client interface for blocking clients. */ @@ -57,10 +44,16 @@ interface GraphQLClient { */ fun executeQuery(query: String, variables: Map, operationName: String?): GraphQLResponse - @Deprecated("The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);")) + @Deprecated( + "The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", + ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);") + ) fun executeQuery(query: String, variables: Map, requestExecutor: RequestExecutor): GraphQLResponse = throw UnsupportedOperationException() - @Deprecated("The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);")) + @Deprecated( + "The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", + ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);") + ) fun executeQuery( query: String, variables: Map, @@ -73,159 +66,3 @@ interface GraphQLClient { fun createCustom(url: String, requestExecutor: RequestExecutor) = CustomGraphQLClient(url, requestExecutor) } } - -/** - * GraphQL client interface for reactive clients. - */ -interface MonoGraphQLClient { - /** - * A reactive call to execute a query and parse its result. - * Don't forget to subscribe() to actually send the query! - * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! - * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. - */ - fun reactiveExecuteQuery( - query: String - ): Mono - - /** - * A reactive call to execute a query and parse its result. - * Don't forget to subscribe() to actually send the query! - * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! - * @param variables A map of input variables - * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. - */ - fun reactiveExecuteQuery( - query: String, - variables: Map - ): Mono - - /** - * A reactive call to execute a query and parse its result. - * Don't forget to subscribe() to actually send the query! - * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! - * @param variables A map of input variables - * @param operationName Name of the operation - * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. - */ - fun reactiveExecuteQuery( - query: String, - variables: Map, - operationName: String? - ): Mono - - @Deprecated("The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);")) - fun reactiveExecuteQuery( - query: String, - variables: Map, - requestExecutor: MonoRequestExecutor - ): Mono = throw UnsupportedOperationException() - - @Deprecated("The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);")) - fun reactiveExecuteQuery( - query: String, - variables: Map, - operationName: String?, - requestExecutor: MonoRequestExecutor - ): Mono = throw UnsupportedOperationException() - - companion object { - @JvmStatic - fun createCustomReactive(url: String, requestExecutor: MonoRequestExecutor) = CustomMonoGraphQLClient(url, requestExecutor) - - @JvmStatic - fun createWithWebClient(webClient: WebClient) = WebClientGraphQLClient(webClient) - - @JvmStatic - fun createWithWebClient(webClient: WebClient, headersConsumer: Consumer) = WebClientGraphQLClient(webClient, headersConsumer) - } -} - -/** - * GraphQL client interface for reactive clients that support multiple results such as subscriptions. - */ -interface ReactiveGraphQLClient { - /** - * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! - * @param variables A map of input variables - * @return A [Flux] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. - */ - fun reactiveExecuteQuery( - query: String, - variables: Map - ): Flux - - /** - * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! - * @param variables A map of input variables - * @param operationName Operation name - * @return A [Flux] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. - */ - fun reactiveExecuteQuery( - query: String, - variables: Map, - operationName: String? - ): Flux -} - -@FunctionalInterface -/** - * Code responsible for executing the HTTP request for a GraphQL query. - * Typically provided as a lambda. - * @param url The URL the client was configured with - * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type. - * @param body The request body - * @returns HttpResponse which is a representation of the HTTP status code and the response body as a String. - */ -fun interface RequestExecutor { - fun execute(url: String, headers: Map>, body: String): HttpResponse -} - -data class HttpResponse(val statusCode: Int, val body: String?, val headers: Map>) { - constructor(statusCode: Int, body: String?) : this(statusCode, body, emptyMap()) -} - -@FunctionalInterface -/** - * Code responsible for executing the HTTP request for a GraphQL query. - * Typically provided as a lambda. Reactive version (Mono) - * @param url The URL the client was configured with - * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type. - * @param body The request body - * @returns Mono which is a representation of the HTTP status code and the response body as a String. - */ -fun interface MonoRequestExecutor { - fun execute(url: String, headers: Map>, body: String): Mono -} - -/** - * A transport level exception (e.g. a failed connection). This does *not* represent successful GraphQL responses that contain errors. - */ -class GraphQLClientException(statusCode: Int, url: String, response: String, request: String) : - ResponseStatusException(HttpStatus.valueOf(statusCode), "GraphQL server $url responded with status code $statusCode: '$response'. The request sent to the server was \n$request") - -internal object GraphQLClients { - internal val objectMapper: ObjectMapper = - if (ClassUtils.isPresent("com.fasterxml.jackson.module.kotlin.KotlinModule\$Builder", this::class.java.classLoader)) { - ObjectMapper().registerModule(KotlinModule.Builder().nullIsSameAsDefault(true).build()) - } else ObjectMapper().registerKotlinModule() - - internal val defaultHeaders: HttpHeaders = HttpHeaders.readOnlyHttpHeaders( - HttpHeaders().apply { - accept = listOf(MediaType.APPLICATION_JSON) - contentType = MediaType.APPLICATION_JSON - } - ) - - fun handleResponse(response: HttpResponse, requestBody: String, url: String): GraphQLResponse { - val (statusCode, body) = response - val headers = response.headers - if (statusCode !in 200..299) { - throw GraphQLClientException(statusCode, url, body ?: "", requestBody) - } - - return GraphQLResponse(body ?: "", headers) - } -} - -internal data class Request(val query: String, val variables: Map, val operationName: String?) diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClientException.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClientException.kt new file mode 100644 index 000000000..eb43507af --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClientException.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +import org.springframework.http.HttpStatus +import org.springframework.web.server.ResponseStatusException + +/** + * A transport level exception (e.g. a failed connection). This does *not* represent successful GraphQL responses that contain errors. + */ +class GraphQLClientException( + statusCode: Int, + url: String, + response: String, + request: String +) : + ResponseStatusException( + HttpStatus.valueOf(statusCode), + "GraphQL server $url responded with status code $statusCode: '$response'. The request sent to the server was \n$request" + ) diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClients.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClients.kt new file mode 100644 index 000000000..09c5314a4 --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/GraphQLClients.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.util.ClassUtils + +internal object GraphQLClients { + + internal val objectMapper: ObjectMapper = + if (ClassUtils.isPresent("com.fasterxml.jackson.module.kotlin.KotlinModule\$Builder", this::class.java.classLoader)) { + ObjectMapper().registerModule(KotlinModule.Builder().nullIsSameAsDefault(true).build()) + } else ObjectMapper().registerKotlinModule() + + internal val defaultHeaders: HttpHeaders = HttpHeaders.readOnlyHttpHeaders( + HttpHeaders().apply { + accept = listOf(MediaType.APPLICATION_JSON) + contentType = MediaType.APPLICATION_JSON + } + ) + + fun handleResponse(response: HttpResponse, requestBody: String, url: String): GraphQLResponse { + val (statusCode, body) = response + val headers = response.headers + if (statusCode !in 200..299) { + throw GraphQLClientException(statusCode, url, body ?: "", requestBody) + } + + return GraphQLResponse(body ?: "", headers) + } +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/HttpResponse.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/HttpResponse.kt new file mode 100644 index 000000000..452cd3969 --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/HttpResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +data class HttpResponse( + val statusCode: Int, + val body: String?, + val headers: Map> +) { + constructor(statusCode: Int, body: String?) : this(statusCode, body, emptyMap()) +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoGraphQLClient.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoGraphQLClient.kt new file mode 100644 index 000000000..9a5a8abcb --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoGraphQLClient.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +import org.intellij.lang.annotations.Language +import org.springframework.http.HttpHeaders +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono +import java.util.function.Consumer + +/** + * GraphQL client interface for reactive clients. + */ +interface MonoGraphQLClient { + /** + * A reactive call to execute a query and parse its result. + * Don't forget to subscribe() to actually send the query! + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + @Language("graphql") query: String + ): Mono + + /** + * A reactive call to execute a query and parse its result. + * Don't forget to subscribe() to actually send the query! + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param variables A map of input variables + * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + @Language("graphql") query: String, + variables: Map + ): Mono + + /** + * A reactive call to execute a query and parse its result. + * Don't forget to subscribe() to actually send the query! + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param variables A map of input variables + * @param operationName Name of the operation + * @return A [Mono] of [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + @Language("graphql") query: String, + variables: Map, + operationName: String? + ): Mono + + @Deprecated( + "The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", + ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);") + ) + fun reactiveExecuteQuery( + query: String, + variables: Map, + requestExecutor: MonoRequestExecutor + ): Mono = throw UnsupportedOperationException() + + @Deprecated( + "The RequestExecutor should be provided while creating the implementation. Use CustomGraphQLClient/CustomMonoGraphQLClient instead.", + ReplaceWith("Example: new CustomGraphQLClient(url, requestExecutor);") + ) + fun reactiveExecuteQuery( + query: String, + variables: Map, + operationName: String?, + requestExecutor: MonoRequestExecutor + ): Mono = throw UnsupportedOperationException() + + companion object { + @JvmStatic + fun createCustomReactive( + @Language("url") url: String, + requestExecutor: MonoRequestExecutor + ) = CustomMonoGraphQLClient(url, requestExecutor) + + @JvmStatic + fun createWithWebClient(webClient: WebClient) = WebClientGraphQLClient(webClient) + + @JvmStatic + fun createWithWebClient( + webClient: WebClient, + headersConsumer: Consumer + ) = WebClientGraphQLClient(webClient, headersConsumer) + } +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoRequestExecutor.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoRequestExecutor.kt new file mode 100644 index 000000000..8a2339850 --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/MonoRequestExecutor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +import reactor.core.publisher.Mono + +@FunctionalInterface +/** + * Code responsible for executing the HTTP request for a GraphQL query. + * Typically provided as a lambda. Reactive version (Mono) + * @param url The URL the client was configured with + * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type. + * @param body The request body + * @returns Mono which is a representation of the HTTP status code and the response body as a String. + */ +fun interface MonoRequestExecutor { + fun execute(url: String, headers: Map>, body: String): Mono +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/ReactiveGraphQLClient.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/ReactiveGraphQLClient.kt new file mode 100644 index 000000000..55985ea9c --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/ReactiveGraphQLClient.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +import reactor.core.publisher.Flux + +/** + * GraphQL client interface for reactive clients that support multiple results such as subscriptions. + */ +interface ReactiveGraphQLClient { + /** + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param variables A map of input variables + * @return A [Flux] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + query: String, + variables: Map + ): Flux + + /** + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param variables A map of input variables + * @param operationName Operation name + * @return A [Flux] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + query: String, + variables: Map, + operationName: String? + ): Flux +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/Request.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/Request.kt new file mode 100644 index 000000000..b38ce6123 --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/Request.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +internal data class Request(val query: String, val variables: Map, val operationName: String?) diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/RequestExecutor.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/RequestExecutor.kt new file mode 100644 index 000000000..f7eb87f06 --- /dev/null +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/RequestExecutor.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Netflix, 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 + * + * http://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.netflix.graphql.dgs.client + +@FunctionalInterface +/** + * Code responsible for executing the HTTP request for a GraphQL query. + * Typically provided as a lambda. + * @param url The URL the client was configured with + * @param headers A map of headers. The client sets some default headers such as Accept and Content-Type. + * @param body The request body + * @returns HttpResponse which is a representation of the HTTP status code and the response body as a String. + */ +fun interface RequestExecutor { + fun execute(url: String, headers: Map>, body: String): HttpResponse +} diff --git a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClient.kt b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClient.kt index 628ece6b0..3c36742da 100644 --- a/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClient.kt +++ b/graphql-dgs-client/src/main/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClient.kt @@ -16,9 +16,11 @@ package com.netflix.graphql.dgs.client +import org.intellij.lang.annotations.Language import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec import org.springframework.web.reactive.function.client.toEntity import reactor.core.publisher.Mono import java.util.function.Consumer @@ -49,7 +51,7 @@ class WebClientGraphQLClient( * @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. */ override fun reactiveExecuteQuery( - query: String + @Language("graphql") query: String ): Mono { return reactiveExecuteQuery(query, emptyMap(), null) } @@ -60,7 +62,7 @@ class WebClientGraphQLClient( * @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. */ override fun reactiveExecuteQuery( - query: String, + @Language("graphql") query: String, variables: Map ): Mono { return reactiveExecuteQuery(query, variables, null) @@ -73,9 +75,41 @@ class WebClientGraphQLClient( * @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. */ override fun reactiveExecuteQuery( - query: String, + @Language("graphql") query: String, variables: Map, operationName: String? + ): Mono { + return reactiveExecuteQuery(query, variables, operationName, REQUEST_BODY_URI_CUSTOMIZER_IDENTITY) + } + + /** + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param requestBodyUriCustomizer Allows customization of the URI and headers. + * This occurs before both, the [headersConsumer] and serialization of the GraphQL request to the body occurs. + * In other words, the [headersConsumer] will take precedence. + * @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + @Language("graphql") query: String, + requestBodyUriCustomizer: RequestBodyUriCustomizer + ): Mono { + return reactiveExecuteQuery(query, emptyMap(), null, requestBodyUriCustomizer) + } + + /** + * @param query The query string. Note that you can use [code generation](https://netflix.github.io/dgs/generating-code-from-schema/#generating-query-apis-for-external-services) for a type safe query! + * @param variables A map of input variables + * @param operationName GraphQL Operation name + * @param requestBodyUriCustomizer Allows customization of the URI and headers. + * This occurs before both, the [headersConsumer] and serialization of the GraphQL request to the body occurs. + * In other words, the [headersConsumer] will take precedence. + * @return A [Mono] of [GraphQLResponse]. [GraphQLResponse] parses the response and gives easy access to data and errors. + */ + fun reactiveExecuteQuery( + @Language("graphql") query: String, + variables: Map, + operationName: String?, + requestBodyUriCustomizer: RequestBodyUriCustomizer ): Mono { @Suppress("BlockingMethodInNonBlockingContext") val serializedRequest = GraphQLClients.objectMapper.writeValueAsString( @@ -86,10 +120,10 @@ class WebClientGraphQLClient( ) ) - return webclient.post() - .bodyValue(serializedRequest) + return requestBodyUriCustomizer.apply(webclient.post()) .headers { headers -> headers.addAll(GraphQLClients.defaultHeaders) } .headers(this.headersConsumer) + .bodyValue(serializedRequest) .retrieve() .toEntity() .map { response -> @@ -111,4 +145,28 @@ class WebClientGraphQLClient( return GraphQLResponse(body ?: "", headers) } + + companion object { + private val REQUEST_BODY_URI_CUSTOMIZER_IDENTITY = RequestBodyUriCustomizer { it } + } + + @FunctionalInterface + /** + * Allows customization of the request URI and headers [WebClientGraphQLClient], returning the + * modified [RequestBodySpec]. This could be used to set URI query parameters, for example: + * + * _Note the example uses Kotlin syntax_ + * ``` + * { request -> + * request.uri{ uriBuilder -> + * uriBuilder + * .queryParam("q1", "foo") + * .build() + * } + * } + * ``` + */ + fun interface RequestBodyUriCustomizer { + fun apply(spec: WebClient.RequestBodyUriSpec): RequestBodySpec + } } diff --git a/graphql-dgs-client/src/test/java/com/netflix/graphql/client/GraphQLResponseJavaTest.java b/graphql-dgs-client/src/test/java/com/netflix/graphql/client/GraphQLResponseJavaTest.java index 34bc92a3c..b46786ef0 100644 --- a/graphql-dgs-client/src/test/java/com/netflix/graphql/client/GraphQLResponseJavaTest.java +++ b/graphql-dgs-client/src/test/java/com/netflix/graphql/client/GraphQLResponseJavaTest.java @@ -21,9 +21,7 @@ import org.springframework.http.*; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; import static java.util.Collections.emptyMap; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; diff --git a/graphql-dgs-client/src/test/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClientTest.kt b/graphql-dgs-client/src/test/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClientTest.kt index 5697cf2d5..46b1a5784 100644 --- a/graphql-dgs-client/src/test/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClientTest.kt +++ b/graphql-dgs-client/src/test/kotlin/com/netflix/graphql/dgs/client/WebClientGraphQLClientTest.kt @@ -21,21 +21,29 @@ import com.netflix.graphql.dgs.DgsQuery import com.netflix.graphql.dgs.DgsTypeDefinitionRegistry import com.netflix.graphql.dgs.autoconfig.DgsAutoConfiguration import com.netflix.graphql.dgs.webmvc.autoconfigure.DgsWebMvcAutoConfiguration -import graphql.language.FieldDefinition -import graphql.language.ObjectTypeDefinition -import graphql.language.TypeName +import graphql.schema.idl.SchemaParser import graphql.schema.idl.TypeDefinitionRegistry -import org.junit.jupiter.api.BeforeEach +import io.netty.handler.logging.LogLevel +import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Test import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.web.server.LocalServerPort +import org.springframework.http.client.reactive.ReactorClientHttpConnector import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient +import reactor.netty.transport.logging.AdvancedByteBufFormat import reactor.test.StepVerifier +@Suppress("GraphQLUnresolvedReference") @SpringBootTest( - classes = [DgsAutoConfiguration::class, DgsWebMvcAutoConfiguration::class, WebClientGraphQLClientTest.TestApp::class], + classes = [ + DgsAutoConfiguration::class, + DgsWebMvcAutoConfiguration::class, + WebClientGraphQLClientTest.TestApp::class + ], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT ) class WebClientGraphQLClientTest { @@ -43,15 +51,10 @@ class WebClientGraphQLClientTest { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @LocalServerPort lateinit var port: Integer - lateinit var client: WebClientGraphQLClient - - @BeforeEach - fun setup() { - client = MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:$port/graphql")) - } @Test fun `Successful graphql response`() { + val client = MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:$port/graphql")) val result = client.reactiveExecuteQuery("{hello}").map { r -> r.extractValue("hello") } StepVerifier.create(result) @@ -61,9 +64,10 @@ class WebClientGraphQLClientTest { @Test fun `Extra header can be provided`() { - client = MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:$port/graphql")) { headers -> - headers.add("myheader", "test") - } + val client = + MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:$port/graphql")) { headers -> + headers.add("myheader", "test") + } val result = client.reactiveExecuteQuery("{withHeader}").map { r -> r.extractValue("withHeader") } StepVerifier.create(result) @@ -71,8 +75,42 @@ class WebClientGraphQLClientTest { .verifyComplete() } + @Test + fun `Request parameters can be added, per request`() { + val httpClient: HttpClient = HttpClient + .create() + .wiretap( + "reactor.netty.http.client.HttpClient", + LogLevel.INFO, + AdvancedByteBufFormat.TEXTUAL + ) + + val webClient = + WebClient.builder() + .clientConnector(ReactorClientHttpConnector(httpClient)) + .baseUrl("http://localhost:$port/graphql") + .build() + + val client = MonoGraphQLClient.createWithWebClient(webClient) + val result = client.reactiveExecuteQuery( + query = "{ withUriParam }", + requestBodyUriCustomizer = { + it.uri { uriBuilder -> + uriBuilder + .queryParam("q1", "one") + .build() + } + } + ).map { r -> r.extractValue("withUriParam") } + + StepVerifier.create(result) + .expectNext("Parameter q1: one") + .verifyComplete() + } + @Test fun `Graphql errors should be handled`() { + val client = MonoGraphQLClient.createWithWebClient(WebClient.create("http://localhost:$port/graphql")) val errors = client.reactiveExecuteQuery("{error}").map { r -> r.errors } StepVerifier.create(errors) @@ -100,27 +138,25 @@ class WebClientGraphQLClientTest { return "Header value: $myheader" } + @DgsQuery + fun withUriParam(@RequestParam("q1") param: String): String { + return "Parameter q1: $param" + } + @DgsTypeDefinitionRegistry fun typeDefinitionRegistry(): TypeDefinitionRegistry { - val newRegistry = TypeDefinitionRegistry() - newRegistry.add( - ObjectTypeDefinition.newObjectTypeDefinition().name("Query") - .fieldDefinition( - FieldDefinition.newFieldDefinition() - .name("hello") - .type(TypeName("String")).build() - ).fieldDefinition( - FieldDefinition.newFieldDefinition() - .name("withHeader") - .type(TypeName("String")).build() - ).fieldDefinition( - FieldDefinition.newFieldDefinition() - .name("error") - .type(TypeName("String")).build() - ) - .build() - ) - return newRegistry + val schemaParser = SchemaParser() + + @Language("graphql") + val gqlSchema = """ + type Query{ + hello: String + withHeader: String + withUriParam: String + error: String + } + """.trimMargin() + return schemaParser.parse(gqlSchema) } } } diff --git a/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt b/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt index e58e840f4..6193a810b 100644 --- a/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt +++ b/graphql-dgs-spring-boot-micrometer/src/test/kotlin/com/netflix/graphql/dgs/metrics/micrometer/MicrometerServletSmokeTest.kt @@ -937,7 +937,7 @@ class MicrometerServletSmokeTest { val executor = ThreadPoolTaskExecutor() executor.corePoolSize = 1 executor.maxPoolSize = 1 - executor.threadNamePrefix = "${MicrometerServletSmokeTest::class.java.simpleName}-test-" + executor.setThreadNamePrefix("${MicrometerServletSmokeTest::class.java.simpleName}-test-") executor.setQueueCapacity(10) executor.initialize() return executor