ExpediaGroup / graphql-kotlin

Libraries for running GraphQL in Kotlin
https://opensource.expediagroup.com/graphql-kotlin/
Apache License 2.0
1.74k stars 347 forks source link

No code generation occurs, no logs explaining why #1926

Closed snowe2010 closed 8 months ago

snowe2010 commented 8 months ago

With the following gradle config, I can pull the schema from the server, it saves it to build/schema.graphql, but then when I try to generate the client with graphqlGenerateClient all that happens is that the folder is made, with no code generation occurring. There are no logs explaining why. I've also searched for a flag in the gradle plugin to log anything, but there is nothing available.

graphql {
    client {
        headers = mapOf(
            "Authorization" to "..."
        )
        queryFileDirectory = "build"
        endpoint = "https://api.us-west-2.amazonaws.com/graphql"
        packageName = "com.example.generated"
        schemaFile = File("build/schema.graphql")
        serializer = GraphQLSerializer.KOTLINX
    }
}

image

Gradle Debug logs


2024-02-16T16:39:17.170-0700 [INFO] [org.gradle.internal.execution.steps.ResolveCachingStateStep] Caching disabled for task ':graphqlGenerateClient' because:
  Build cache is disabled
  Caching has not been enabled for the task
2024-02-16T16:39:17.170-0700 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot task inputs for :graphqlGenerateClient' completed
2024-02-16T16:39:17.170-0700 [DEBUG] [org.gradle.internal.execution.steps.SkipUpToDateStep] Determining if task ':graphqlGenerateClient' is up-to-date
2024-02-16T16:39:17.170-0700 [INFO] [org.gradle.internal.execution.steps.SkipUpToDateStep] Task ':graphqlGenerateClient' is not up-to-date because:
  Executed with '--rerun-tasks'.
2024-02-16T16:39:17.170-0700 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [/Users/tylerthrailkill/Documents/dev/work/tax-service/build/generated/source/graphql/main]
2024-02-16T16:39:17.171-0700 [DEBUG] [org.gradle.internal.execution.steps.CreateOutputsStep] Ensuring directory exists for property outputDirectory at /Users/tylerthrailkill/Documents/dev/work/tax-service/build/generated/source/graphql/main
2024-02-16T16:39:17.171-0700 [DEBUG] [org.gradle.api.internal.tasks.execution.TaskExecution] Executing actions for task ':graphqlGenerateClient'.
2024-02-16T16:39:17.171-0700 [DEBUG] [org.gradle.api.Task] generating GraphQL client
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task] GraphQL Client generator configuration:
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   schema file = /Users/tylerthrailkill/Documents/dev/work/tax-service/build/schema.graphql
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   queries
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]     - schema.graphql
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   packageName = task ':graphqlGenerateClient' property 'packageName'
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   allowDeprecatedFields = task ':graphqlGenerateClient' property 'allowDeprecatedFields'
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   parserOptions = task ':graphqlGenerateClient' property 'parserOptions'
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task]   converters
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task] 
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task] -- end GraphQL Client generator configuration --
2024-02-16T16:39:17.172-0700 [DEBUG] [org.gradle.api.Task] worker classpath: 
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.expediagroup/graphql-kotlin-client-generator/7.0.2/2d6dab81dd5b975871c8c4b097f1ed7d35093515/graphql-kotlin-client-generator-7.0.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.2/9353b021f10c307c00328f52090de2bdb4b6ff9c/jackson-databind-2.15.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.2/4724a65ac8e8d156a24898d50fd5dbd3642870b8/jackson-annotations-2.15.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.2/a6fe1836469a69b3ff66037c324d75fc66ef137c/jackson-core-2.15.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-kotlin/2.15.2/475c9721f5a2a5b7bea57d504bd8b0586d1ba5e/jackson-module-kotlin-2.15.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.expediagroup/graphql-kotlin-client/7.0.2/b9bc5fb5a33f778e5feccb470c0cfcedf6dfa01/graphql-kotlin-client-7.0.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.squareup/kotlinpoet/1.14.2/b6741951260d555a05932e39de177fc64ef450bd/kotlinpoet-1.14.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-serialization-jackson-jvm/2.3.4/9105ec70f9b09e472de19867942f366169d7026c/ktor-serialization-jackson-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-reflect/1.8.22/b52be44bc57cb6fd2169a29aefa4507c4e49c858/kotlin-reflect-1.8.22.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-serialization-core-jvm/1.5.1/e26cf5dfbcfe3e82ca196694dfd305753b1a49b9/kotlinx-serialization-core-jvm-1.5.1.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-serialization-json-jvm/1.5.1/21ac91884e0b9462b9106d1a83e5e0d200170c65/kotlinx-serialization-json-jvm-1.5.1.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-client-apache-jvm/2.3.4/4b1e6d0974211c53e37841bc4f7554a8319886cc/ktor-client-apache-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-client-content-negotiation-jvm/2.3.4/acb20f3b68ae0c265f16607ffd03a3ec89eb0f99/ktor-client-content-negotiation-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-client-core-jvm/2.3.4/7e6b050441579959bf26d2f07143099809d7b276/ktor-client-core-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-websocket-serialization-jvm/2.3.4/a6d6a02d434ca6894655f1f3c199c1cd7b8a5b2e/ktor-websocket-serialization-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-serialization-jvm/2.3.4/b246a515c777d23e12abb7824773333cc41357f0/ktor-serialization-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-events-jvm/2.3.4/1d08d8375a808a2832f3a5f52c8e088c1ab2c86f/ktor-events-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-websockets-jvm/2.3.4/593843969c814e5de2f5cdb64441d6bcd0eeb010/ktor-websockets-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-http-jvm/2.3.4/dc1b1cb0e6ef918c8a432106a81a10b72304b58e/ktor-http-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-slf4j/1.7.3/34086fbfd556969d86b45ee2a69b816ee2413701/kotlinx-coroutines-slf4j-1.7.3.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-utils-jvm/2.3.4/ac6c349dbbe6c6f7dbe6f522531c2fad03733ef6/ktor-utils-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/io.ktor/ktor-io-jvm/2.3.4/9b5c5d64c49b4cd1843bd589cdc0b360ddf11480/ktor-io-jvm-2.3.4.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-core-jvm/1.7.3/2b09627576f0989a436a00a4a54b55fa5026fb86/kotlinx-coroutines-core-jvm-1.7.3.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlinx/kotlinx-coroutines-jdk8/1.7.3/263611b87f7546402852e43a8dc7ef3223ada42a/kotlinx-coroutines-jdk8-1.7.3.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk8/1.8.22/b25c86d47d6b962b9cf0f8c3f320c8a10eea3dd1/kotlin-stdlib-jdk8-1.8.22.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.8.22/4dabb8248310d833bb6a8b516024a91fd3d275c/kotlin-stdlib-jdk7-1.8.22.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.8.22/636bf8b320e7627482771bbac9ed7246773c02bd/kotlin-stdlib-1.8.22.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/com.graphql-java/graphql-java/21.1/ab899d43e862537cf042e82da69ed03b8661074e/graphql-java-21.1.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.7/41eb7184ea9d556f23e18b5cb99cad1f8581fc00/slf4j-api-2.0.7.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.8.22/1a8e3601703ae14bb58757ea6b2d8e8e5935a586/kotlin-stdlib-common-1.8.22.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/23.0.0/8cc20c07506ec18e0834947b84a864bfc094484e/annotations-23.0.0.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.reactivestreams/reactive-streams/1.0.3/d9fb7a7926ffa635b3dcaa5049fb2bfa25b3e7d0/reactive-streams-1.0.3.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpasyncclient/4.1.5/cd18227f1eb8e9a263286c1d7362ceb24f6f9b32/httpasyncclient-4.1.5.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore-nio/4.4.15/85d2b6825d42db909a1474f0ffbd6328429b7a32/httpcore-nio-4.4.15.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpclient/4.5.13/e5f6cae5ca7ecaac1ec2827a9e2d65ae2869cada/httpclient-4.5.13.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/org.apache.httpcomponents/httpcore/4.4.15/7f2e0c573eaa7a74bac2e89b359e1f73d92a0a1d/httpcore-4.4.15.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/commons-logging/commons-logging/1.2/4bfc12adfe4842bf07b657f0369c4cb522955686/commons-logging-1.2.jar
/Users/tylerthrailkill/.gradle/caches/modules-2/files-2.1/commons-codec/commons-codec/1.11/3acb4705652e16236558f0f4f2192cc33c3bd189/commons-codec-1.11.jar
2024-02-16T16:39:17.175-0700 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: released lock on worker lease
2024-02-16T16:39:17.289-0700 [DEBUG] [org.gradle.internal.resources.AbstractTrackedResourceLock] Execution worker Thread 4: acquired lock on worker lease
2024-02-16T16:39:17.289-0700 [DEBUG] [org.gradle.api.Task] successfully generated GraphQL HTTP client
2024-02-16T16:39:17.289-0700 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Execute generateGraphQLClientAction for :graphqlGenerateClient' completed
2024-02-16T16:39:17.289-0700 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Executing task ':graphqlGenerateClient'' completed
2024-02-16T16:39:17.289-0700 [DEBUG] [org.gradle.internal.vfs.impl.AbstractVirtualFileSystem] Invalidating VFS paths: [/Users/tylerthrailkill/Documents/dev/work/tax-service/build/generated/source/graphql/main]
2024-02-16T16:39:17.290-0700 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Build operation 'Snapshot outputs after executing task ':graphqlGenerateClient'' completed
2024-02-16T16:39:17.290-0700 [DEBUG] [org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter] Removed task artifact state for task ':graphqlGenerateClient' from context.
2024-02-16T16:39:17.290-0700 [DEBUG] [org.gradle.internal.operations.DefaultBuildOperationRunner] Completing Build operation 'Task :graphqlGenerateClient'

dariuszkuc commented 8 months ago

👋 did you specify any query files? i.e. are there any query files under build?

snowe2010 commented 8 months ago

No, I don't have any query files, I thought the introspection would pull them from the endpoint. Do I have to duplicate the query files from the providing service into our codebase? Kinda defeats the purpose of introspecting the schema doesn't it?

snowe2010 commented 8 months ago

I added an example query in to test and it doesn't generate any of the schema, only two objects, one which isn't even valid... image

image

while the introspected schema has over a thousand lines in it, including the Product object. image

snowe2010 commented 8 months ago

Ok, yeah so it's looking like I'm going to have to manually copy and paste every single query in to get it to generate the proper schema... That's quite annoying to say the least... Why doesn't it generate the schema without query files? I shouldn't have to generate a client just to get schema objects, especially if I want to do something with those schema objects outside of the ktor or spring ecosystem.

dariuszkuc commented 8 months ago

I think you misunderstood what this library is about - graphql-kotlin is a code-first GraphQL library meaning your schema is auto-generated from your source code (through reflection), i.e. you write functions returning data and it will automatically map it to a corresponding GraphQL representation. While we do provide Spring and Ktor integrations, you can easily integrate this schema codegen capability with any server of your choice. Since schema artifact is often useful in other tooling (e.g. for schema checks), schema (in SDL format) can be generated at build time using one of the provided plugins (or you can use 3rd party tool to download the incomplete* schema through introspection) but it is an artifact of a build and not the input.

graphql-kotlin also provides simple client which relies on auto-generated client code to interact with your server (which you tried to use). Client codegen should generate valid code though so there might be a bug there as you shouldn't get empty data class like that (is query valid?).

To reiterate -> graphql-kotlin does not generate any server code. If you prefer SDL first solution (i.e. start from a schema vs code) then you might want to take a look at Netflix DGS which also provides some codegen capabilities.

*introspection does not contain any custom directive information

snowe2010 commented 8 months ago

I understand it's not a server solution. I'm not a acting as a server in this instance. I'm acting as the client. I would assume that as a client, I should be able to take an introspected schema (from the providing server) and generate a client data model that I can then use as I want. I don't understand your comment "graphql-kotlin is a code-first GraphQL library meaning your schema is auto-generated from your source code (through reflection)" as that's the exact opposite of what a client should need to do. If I consume a swagger-api, then I should be able to take that swagger spec and generate a model off of it, as a client, and then use that model to communicate however I want. But in this case it seems I have to provide significant details on my side copying the actual queries that the server supports in order to get any model generated.

In the past (2017 or thereabouts) I used an apollo solution that literally just generated a model. It didn't generate anything else, no queries were required, so this is just really weird to me.

snowe2010 commented 8 months ago

I've read over the apollo kotlin docs and they're much more clear. I think I'm going to switch over to that. I see now why the newer way of doing things is to require a query, even though I honestly think that's quite ridiculous (what happens when you need to add a new query and the nullability changes?). Seems better to just make it all nullable and deal with it rather than generate models based off of a query that could change at any moment. In any case, Apollo supports fragments which I will need if I'm going to be copy pasting queries from the other team into our codebase, else I'm going to have a maintenance nightmare. Thanks for the talk.

dariuszkuc commented 8 months ago

Apologies but it looks like I misunderstood what you were trying to do (i.e. trying to generate server code matching your full GraphQL schema).

Difference between OpenAPI and GraphQL is that in the first one clients always get the complete model vs in the latter one clients have to explicitly ask for the data they want. OpenAPI also doesn't have notion of non-null. Since GraphQL is strongly typed and your clients only get the data they explicitly ask for -> in general, smart clients will only generate data models that match your existing queries, they won't generate catch-all full schema as that is not really useful for the clients. Furthermore it also has side effects for clients written in a typesafe language, i.e. if you never request a field but it is non-nullable then Kotlin code will be problematic as you will never get the value for it but Kotlin language requires a value there.

AFAIK all the smart client libs (including Apollo Client|Kotlin|iOS) will generate the code based on your queries and only on your queries.

what happens when you need to add a new query and the nullability changes?

What you are talking here is a schema evolution which is outside of client control. Depending whether it is null->non-null or non-null->null it can be fine (first one) or a breaking change (the latter). There are other tools to detect this but in general schema owners should try to not break their clients (and at least communicate breaking changes in advance using @Deprecated).

Seems better to just make it all nullable and deal with it rather than generate models based off of a query that could change at any moment.

If you do this then you loose all the type safety information from the schema -> if everything is always nullable whats the point of having fields non-nullable? Isn't the whole point of getting non-nullable fields to indicate that you don't have to wrap it in if != null checks everywhere?


re: Apollo Kotlin -> indeed that is a much more advanced client with a number of additional features that is constantly improving. Intent behind graphql-kotlin was to provide a very simple client to handle some basic use cases but indeed it is pretty limited.

snowe2010 commented 8 months ago

OpenAPI also doesn't have notion of non-null.

openapi supports nullability just fine. The openapi-generator generates kotlin models perfectly fine with nullable and non-nullable fields. You just have to specify in the spec if it's nullable, else it defaults to non-nullable.

AFAIK all the smart client libs (including Apollo Client|Kotlin|iOS) will generate the code based on your queries and only on your queries.

I had tested a previous library a few months ago that generated java models just fine and did not require a query. That's most likely because they are less concerned with nullability, or because they understand that model generation is often desired even when you have no queries yet.

What you are talking here is a schema evolution which is outside of client control. Depending whether it is null->non-null or non-null->null it can be fine (first one) or a breaking change (the latter). There are other tools to detect this but in general schema owners should try to not break their clients (and at least communicate breaking changes in advance using @Deprecated).

no I'm not talking about schema evolution. I'm talking about a case like this:

you have an underlying model on the server like this:

data class A(
  firstName: String? = null,
  lastName: String? = null,
  state: String? = null,
  zipCode: String? = null,
)

and in the client that is using either expedia or apollo you specify a query like so:

query A {
  firstName
  state
}

Which would generate a model like so for the server.

data class A(firstName: String, state: String)

but then later someone else in your codebase comes in and adds a new query like this to the client (the server does not change. The model is still as listed above):

query B {
 firstName
 lastName
 zipCode
}

which would generate a completely different model, even though they are the same underlying object

data class B(firstName: String, lastName: String?, state: String?, zipCode: String)

Your nullability for the same object has changed because the objects aren't actually the same. I'm not sure it would actually generate like this, I haven't tested it, but I'm just showing the issues with this query first methodology. You have no clue how the queries are going to be used, modified etc, and what the nullability actually is, especially since the query isn't coming from what the server actually supports, but is instead coming from just that one use case. Either you can then share models, or the nullability of the model changes breaking things later.