apollographql / apollo-kotlin

:rocket:  A strongly-typed, caching GraphQL client for the JVM, Android, and Kotlin multiplatform.
https://www.apollographql.com/docs/kotlin
MIT License
3.73k stars 654 forks source link

how pass query variables to client in every request ? #6068

Open namdroid opened 1 month ago

namdroid commented 1 month ago

Question

my request shows like that:

curl --request POST \
 -H 'Content-Type: application/json' \
 --data '{"query":"query GetBestSellers($category: ProductCategory){bestSellers(category: $category){title}}", "operationName":"GetBestSellers", "variables":{"category":"BOOKS"}}' \
 https://rover.apollo.dev/quickstart/products/graphql

How can i pass "variables":{"category":"BOOKS"}} ?

martinbonnin commented 1 month ago

Hi 👋 Apollo Kotlin uses codegen to generate models for your operation. In this example, you should get a GetBestSellers(category: ProductCategory) class where you can pass your variables. You can take a look at https://www.apollographql.com/docs/kotlin/advanced/operation-variables for more details.

namdroid commented 1 month ago

Hi Martin, of course i can pass parameter to GetBestSellers directly, but waht i need is a addtional variables

{"query":"query
{
  footer(lang: GERMAN) {
      links {
        text
        url
      }
    }
  }
}", "variables":{"appName":"myapp"}}'

Bildschirmfoto 2024-07-23 um 14 45 54

namdroid commented 1 month ago

in javascript should be:

const gqlResp = await client
      .query({
        query,
        variables: {
          lang,
          appName: 'myApp',
          ...moreVariables,
        },
        fetchPolicy: 'no-cache',
      });
martinbonnin commented 1 month ago

Looking at your initial query:

query GetBestSellers($category: ProductCategory){
  bestSellers(category: $category) {
    title
  }
}

This query defines a $category variable, which is sent as JSON and Apollo Kotlin will serialize it automatically when you construct a GetBestSellers operation:

val operation = GetBestSellers(ProductCategory(Books))

Your second query doesn't have an appName variable:

query {
  footer(lang: GERMAN) {
    links {
      text
      url
    }
  }
}

You could patch the HTTP POST body to include additional variables if that's what you're trying to do but I would recommend against that. variables is for GraphQL variables. It's surprising to pass values there that are not in your GraphQL operation.

If you're trying to send additional context to your server, I would recommend using an HTTP header instead.

namdroid commented 1 month ago

Ho can i patch the HTTP POST body to include additional variables ?

martinbonnin commented 1 month ago

The most efficient way is to implement your own HttpRequestComposer.

Another easier albeit slightly less efficient (because it needs to rewrite the post body), is to use OkHttp interceptors

namdroid commented 1 month ago

thanks martin, so here is my HttpRequestComposer class:

    class GraphQLHttpRequestComposer(url: String, gson: Gson) : HttpRequestComposer {
        override fun <D : Operation.Data> compose(apolloRequest: ApolloRequest<D>): HttpRequest {
            val operation = apolloRequest.operation

            // Create a MapJsonWriter to capture variables
            val mapJsonWriter = MapJsonWriter()

            // Convert the captured map to a JsonObject
            val variablesJson = gson.toJsonTree(mapJsonWriter.root()).asJsonObject

            // Add appName to variables
            variablesJson.addProperty("appName", „MyApp“)

            // Create the full request body
            val bodyJson = JsonObject().apply {
                addProperty("query", operation.document())
                addProperty("operationName", operation.name())
                add("variables", variablesJson)
            }

            val bodyString = gson.toJson(bodyJson)

            val httpBody = object : HttpBody {
                override val contentLength: Long = bodyString.toByteArray().size.toLong()
                override val contentType: String = "application/json; charset=utf-8"

                override fun writeTo(bufferedSink: BufferedSink) {
                    bufferedSink.writeUtf8(bodyString)
                }
            }

            return HttpRequest.Builder(HttpMethod.Post, "url")
                .addHeader("Content-Type", "application/json")
                .body(httpBody)
                .build()
        }
    }

    fun provideApolloClient(okHttpClient: OkHttpClient, graphqlUrl: String): ApolloClient {

        val networkTransport = HttpNetworkTransport.Builder()
            .httpRequestComposer(GraphQLHttpRequestComposer(graphqlUrl, gson))
            .okHttpClient(okHttpClient)
            .build()

        return ApolloClient.Builder()
            .networkTransport(networkTransport)
            .addInterceptor(LoggingApolloInterceptor())
            .httpMethod(HttpMethod.Get)
            .autoPersistedQueries()
            .build()
    }

But it don't work !I don't know why

namdroid commented 1 month ago

may new method added to Apollo client in the future that support variables in request body ?

martinbonnin commented 1 month ago

Try something like below (wildly untested):

class MyHttpRequestComposer(val serverUrl: String): HttpRequestComposer {
  override fun <D : Operation.Data> compose(apolloRequest: ApolloRequest<D>): HttpRequest {
    check(apolloRequest.httpMethod == HttpMethod.Post) {
      "This composer only supports POST"
    }

    val operation = apolloRequest.operation
    val customScalarAdapters = apolloRequest.executionContext[CustomScalarAdapters]!!

    val body = buildJsonByteString {
      writeObject {
        name("query")
        value(operation.document())
        name("operationName")
        value(operation.name())
        name("variables")
        writeObject {
          operation.serializeVariables(this, customScalarAdapters, false)
          // Add your variables here
          name("appName")
          value("MyAppName")
        }
      }
    }
    return HttpRequest.Builder(HttpMethod.Post, serverUrl)
        .body(
            object : HttpBody {
              override val contentType = "application/json"
              override val contentLength = body.size.toLong()

              override fun writeTo(bufferedSink: BufferedSink) {
                bufferedSink.write(body)
              }
            }
        )
        .build()
  }
}

may new method added to Apollo client in the future that support variables in request body ?

That is very unlikely. As said before, sending extra fields in GraphQL variables is not standard. Is there a reason you cannot use a HTTP header or another way for this?