Skip to content

Commit

Permalink
Merge pull request #1290 from Netflix/feature/WebClientGraphQLClient_uri
Browse files Browse the repository at this point in the history
WebClientGraphQLClient RequestBodyUriCustomizer
  • Loading branch information
berngp committed Oct 24, 2022
2 parents 9c439d3 + 0f9cd9a commit 708e15c
Show file tree
Hide file tree
Showing 14 changed files with 486 additions and 216 deletions.
Expand Up @@ -16,24 +16,28 @@

package com.netflix.graphql.dgs.client

import org.intellij.lang.annotations.Language
import reactor.core.publisher.Mono

/**
* Non-blocking implementation of a GraphQL client, based on the [Mono] type.
* 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<GraphQLResponse> {
class CustomMonoGraphQLClient(
private val url: String,
private val monoRequestExecutor: MonoRequestExecutor
) : MonoGraphQLClient {
override fun reactiveExecuteQuery(@Language("graphql") query: String): Mono<GraphQLResponse> {
return reactiveExecuteQuery(query, emptyMap(), null)
}

override fun reactiveExecuteQuery(query: String, variables: Map<String, Any>): Mono<GraphQLResponse> {
override fun reactiveExecuteQuery(@Language("graphql") query: String, variables: Map<String, Any>): Mono<GraphQLResponse> {
return reactiveExecuteQuery(query, variables, null)
}

override fun reactiveExecuteQuery(
query: String,
@Language("graphql") query: String,
variables: Map<String, Any>,
operationName: String?
): Mono<GraphQLResponse> {
Expand Down
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -57,10 +44,16 @@ interface GraphQLClient {
*/
fun executeQuery(query: String, variables: Map<String, Any>, 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<String, Any>, 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<String, Any>,
Expand All @@ -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<GraphQLResponse>

/**
* 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<String, Any>
): Mono<GraphQLResponse>

/**
* 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<String, Any>,
operationName: String?
): Mono<GraphQLResponse>

@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<String, Any>,
requestExecutor: MonoRequestExecutor
): Mono<GraphQLResponse> = 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<String, Any>,
operationName: String?,
requestExecutor: MonoRequestExecutor
): Mono<GraphQLResponse> = 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<HttpHeaders>) = 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<String, Any>
): Flux<GraphQLResponse>

/**
* @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<String, Any>,
operationName: String?
): Flux<GraphQLResponse>
}

@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<String, List<String>>, body: String): HttpResponse
}

data class HttpResponse(val statusCode: Int, val body: String?, val headers: Map<String, List<String>>) {
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<HttpResponse> 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<String, List<String>>, body: String): Mono<HttpResponse>
}

/**
* 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<String, Any>, val operationName: String?)
@@ -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"
)
@@ -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)
}
}
@@ -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<String, List<String>>
) {
constructor(statusCode: Int, body: String?) : this(statusCode, body, emptyMap())
}

0 comments on commit 708e15c

Please sign in to comment.