https://kotlinlang.org logo
#graphql-kotlin
Title
# graphql-kotlin
s

S.

11/13/2023, 7:37 PM
Using the Ktor Server Plugin, where is the place to add additional types like here ?
d

Dariusz Kuc

11/13/2023, 8:06 PM
👋 currently i don't think it is possible it is not exposed for spring one either (but there I guess you could provide your own schema instance) whats your use case? (those extensions were primarily added for federation support so the referenced types may not be accessible through regular query/mutation paths)
s

S.

11/13/2023, 8:08 PM
I see. I was trying to reference a GraphQL type from SchemaGeneratorHooks
d

Dariusz Kuc

11/13/2023, 8:11 PM
fun didBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder = builder
is the last hook that gives you access to the schema builder up to that point. There is no way to read out of the builder but you could build a schema and then transform it to add more stuff into it (thats what is done for federation) *unsure whats your use case or what you are trying to do there
s

S.

11/13/2023, 8:20 PM
My grand goal is to map Arrow's Either to union types in accordance to the "Error as Data" approach. I hoped to just be able to add the union interfaces to my classes and use something like GraphQLTypeReference in hooks. but I suppose I can achieve the same with GraphQLUnionType.newUnionType() and go this way.
d

Dariusz Kuc

11/13/2023, 8:33 PM
haven't tried adding the support for the
Either
type but it should be doable through the hooks .... but most likely you will need to manually provide the mappings first thing that pops to mind is the
willGenerateGraphQLType
hook -> https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotli[…]iagroup/graphql/generator/internal/types/generateGraphQLType.kt or
willResolveMonad
hook (https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotli[…]pediagroup/graphql/generator/internal/types/generateProperty.kt)
👍🏻 1
s

S.

11/14/2023, 12:24 PM
but is there any way to get my already defined classes as GraphQLObjectType so I can use them as possible types for the new union type? Redefining everything like this does not sound feasible
d

Dariusz Kuc

11/14/2023, 3:12 PM
arent you trying to return
Either
? you would have to map those left and right values i don't think there is anything automated atm that you could use
s

S.

11/14/2023, 9:27 PM
yes, but I want both to be subtypes of the same union type. so for example Either<NotFound, User> to union UserResponse = User | NotFound whereas User is a predefined class with a bunch of fields/functions to query user's items
d

Dariusz Kuc

11/14/2023, 9:38 PM
if
User
type is already defined in the schema then I think you could just use
GraphQLTypeReference
and
graphql-java
will update the ref before providing final schema
s

S.

11/14/2023, 9:47 PM
When I tried that I got an NPE from some internal CheckNotNull method. I have to take a closer look at graphql-java itself
d

Dariusz Kuc

11/14/2023, 9:48 PM
unsure
s

S.

11/14/2023, 10:14 PM
Alright, NPE's gone though I'm not even sure what the issue was.
Copy code
val graphqlDeckEither: GraphQLUnionType = GraphQLUnionType.newUnionType()
    .name("DeckEither")
    .possibleType(GraphQLTypeReference("Deck"))
    .possibleType(GraphQLTypeReference("NotFound"))
    .possibleType(GraphQLTypeReference("Unauthorized"))
    .typeResolver { env ->
        when (val value = env.getObject<Either<DomainError, Deck>>()) {
            is arrow.core.Either.Left -> {
                when (value.value) {
                    NotFound -> env.schema.getObjectType("NotFound")
                    Unauthorized -> env.schema.getObjectType("Unauthorized")
                    is Duplicate -> env.schema.getObjectType("Duplicate")
                    is InvalidInput -> env.schema.getObjectType("InvalidInput")
                }
            }

            is arrow.core.Either.Right -> env.schema.getObjectType("Deck")
        }
    }
    .build()
with this a
union DeckEither = Deck | NotFound | Unauthorized
is created as expected. querying returns the correct types however I'm getting
Exception while fetching data (/getDeck/title) : object is not an instance of declaring class
when trying to access any properties of returned objects
d

Dariusz Kuc

11/14/2023, 10:15 PM
Yeah for that i'm pretty sure you would need custom data fetcher to unwrap your results
👌🏻 1
s

S.

11/15/2023, 4:24 PM
With this it works
Copy code
class CustomDataFetcher(private val propertyGetter: KProperty.Getter<*>) : DataFetcher<Any?> {
    override fun get(environment: DataFetchingEnvironment): Any? {
        environment.getSource<Any?>().let { source ->
            return propertyGetter.call(getSourceValue(source))
        }
    }

    private fun getSourceValue(source: Any?): Any? = when (source) {
        is Either.Right<*> -> source.value
        is Either.Left<*> -> source.value
        else -> source
    }
}

class DataFetcherFactoryProvider : SimpleKotlinDataFetcherFactoryProvider() {
    override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any?> =
        DataFetcherFactory {
            CustomDataFetcher(kProperty.getter)
        }
}
Thank you very much for your help!
d

Dariusz Kuc

11/15/2023, 4:27 PM
🚀
s

S.

11/15/2023, 5:49 PM
No, back at this mysterious npe blob upside down
Copy code
Caused by: java.lang.NullPointerException
	at graphql.com.google.common.base.Preconditions.checkNotNull(Preconditions.java:903)
	at graphql.com.google.common.collect.ImmutableList$Builder.add(ImmutableList.java:815)
	at graphql.collect.ImmutableKit.map(ImmutableKit.java:56)
	at graphql.schema.GraphQLTypeResolvingVisitor.visitGraphQLUnionType(GraphQLTypeResolvingVisitor.java:38)
	at graphql.schema.GraphQLUnionType.accept(GraphQLUnionType.java:185)
	at graphql.schema.SchemaTraverser$TraverserDelegateVisitor.enter(SchemaTraverser.java:111)
	at graphql.util.Traverser.traverse(Traverser.java:144)
	at graphql.schema.SchemaTraverser.doTraverse(SchemaTraverser.java:98)
	at graphql.schema.SchemaTraverser.depthFirst(SchemaTraverser.java:88)
	at graphql.schema.SchemaTraverser.depthFirst(SchemaTraverser.java:81)
	at graphql.schema.impl.SchemaUtil.replaceTypeReferences(SchemaUtil.java:105)
	at graphql.schema.GraphQLSchema$Builder.buildImpl(GraphQLSchema.java:938)
	at graphql.schema.GraphQLSchema$Builder.build(GraphQLSchema.java:904)
	at com.expediagroup.graphql.generator.SchemaGenerator.generateSchema(SchemaGenerator.kt:88)
	at com.expediagroup.graphql.generator.SchemaGenerator.generateSchema$default(SchemaGenerator.kt:58)
	at com.expediagroup.graphql.generator.ToSchemaKt.toSchema(toSchema.kt:43)
	at com.expediagroup.graphql.plugin.schema.GenerateSDLKt.generateSDL(GenerateSDL.kt:97)
	at com.expediagroup.graphql.plugin.gradle.actions.GenerateSDLAction.execute(GenerateSDLAction.kt:38)
d

Dariusz Kuc

11/15/2023, 5:51 PM
guessing it is caused by the order in which the types got processed
s

S.

11/15/2023, 5:53 PM
so likely this custom union type is being processed before one of the ones I'm trying to reference with GraphQLTypeReference?
d

Dariusz Kuc

11/15/2023, 5:54 PM
so those type visitors should be applied at the very end when creating the schema (at which point all the types should be available)
unsure whats happening there
s

S.

11/15/2023, 6:42 PM
I can see a typeMap which contains some of my custom types however is missing all regular types. I tried to declare additionalTypes with the
willBuildSchema
hook (https://github.com/ExpediaGroup/graphql-kotlin/blob/716809e6a1842e36516c093cd848cd[…]om/expediagroup/graphql/generator/hooks/SchemaGeneratorHooks.kt) but it doesn't seem to be picked up either
d

Dariusz Kuc

11/15/2023, 6:49 PM
Are you providing your custom hooks to the plugin?
s

S.

11/15/2023, 6:50 PM
it's definitely there, yes
d

Dariusz Kuc

11/15/2023, 7:01 PM
otherwise plugin wont be able to locate your custom hooks
s

S.

11/15/2023, 7:06 PM
oh yes, I did that. and I can also see my class in the config's hooks property earlier with only UUID and LocalDateTime custom types/hooks it was working fine too
d

Dariusz Kuc

11/15/2023, 7:06 PM
then unsure it "should" work the same
s

S.

11/15/2023, 7:20 PM
so I just made a few debug queries (`
Copy code
fun debugUser(): User? = null
`) for all possible union types - so doing this it works
so I guess a possible workaround is to do this and find a hook at the very end where I can remove those queries again
d

Dariusz Kuc

11/15/2023, 7:35 PM
If it works when you add those debug queries then it sounds like some ordering issue
s

S.

11/15/2023, 8:10 PM
yep, presumably also explains why it "just disappeared" before
no luck with removing those queries at the end of the building process. I tried to remove them in
didGenerateQueryObject
as well as in
didBuildSchema
but it always results in the same NPE
there are no introspection configs exposed by any chance? I cannot find anything other than the enabled boolean
d

Dariusz Kuc

11/15/2023, 9:09 PM
what sort of config are you looking for? introspection is either enabled or not
s

S.

11/15/2023, 9:10 PM
something like a schema object it uses so I can rip out the debug queries from there
d

Dariusz Kuc

11/15/2023, 9:11 PM
schema modifications are enabled through various hooks, other than that you can build a schema and transform it afterwards
if you can provide a repro repository with the NPE i may be able to take a look over the weekend
it sounds like some ordering problem
s

S.

11/15/2023, 9:16 PM
the issue is that up to the didBuildSchema hook, it seems to need those debug queries to reference the types correctly. so I cannot remove those. If I simply could remove those queries before passing the schema on to the user (introspection or sdl) I could live with it
I will try to set up a repro
d

Dariusz Kuc

11/15/2023, 9:19 PM
If I simply could remove those queries before passing the schema on to the user (introspection or sdl) I could live with it
you definitely should be able to do it in
didBuildSchema
-> pseudocode
Copy code
val originalSchema = builder.build()
return originalSchema.transform {
  // remove the fields
}
s

S.

11/15/2023, 9:25 PM
I tried this but it also results in the same npe
Copy code
private var queryType: GraphQLObjectType? = null

    override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType {
        queryType = GraphQLObjectType.newObject()
            .name(type.name)
            .withAppliedDirectives(*type.appliedDirectives.toTypedArray())
            .fields(type.fieldDefinitions.filter { !it.name.contains("debug") })
            .build()

        return super.didGenerateQueryObject(type)
    }

    override fun didBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder {
        builder.query(queryType)
        return super.didBuildSchema(builder)
    }
(didGenerateQueryObject because I don't see a way to retrieve queryType from the Builder)
d

Dariusz Kuc

11/15/2023, 9:30 PM
pretty sure this should work
Copy code
override fun didBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder {
    val originalSchema = builder.build()
    val filteredQueries = originalSchema.queryType.fields.filter { it.name != "foo" }
    builder.query(originalSchema.queryType.transform {
        it.replaceFields(filteredQueries)
    })
    return builder
}
i.e.
val originalSchema = builder.build()
<=== this would fail if there are some unresolved type references that being said even with your filtering on the
didGenerateQueryObject
the result "should" be the same (as the types should already be processed at that time) -> there is some weird ordering issue going on there
s

S.

11/15/2023, 9:38 PM
yeah, the second build() fails with the exception
d

Dariusz Kuc

11/15/2023, 9:41 PM
without seeing the codebase hard to say -> definitely some ordering issue
if the second build fails it means that you got some dangling type references that don't get correctly resolved
so there is something going on with how the types are added to the schema, i.e. if you add debug queries => you get concrete type implementations if you don't and only use your
Either
queries then you end up with references only
what is odd though is that by processing the
debug
queries you should get the types registered so it "should" be safe to remove those queries ... yet it blows up so something is off in how those types are added
so this "should" work
Copy code
override fun didBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder {
    val originalSchema = builder.build()
    val filteredQueries = originalSchema.queryType.fields.filter { it.name != "foo" }
    val modifiedQuery = originalSchema.queryType.transform {
        it.replaceFields(filteredQueries)
    }
    return GraphQLSchema.newSchema(originalSchema).query(modifiedQuery)
}
but it doesn't solve the underlying issue
s

S.

11/15/2023, 9:50 PM
the type reference issue makes sense. I will try again if providing additionalTypes could somehow help. it certainly sounded like I can simply remove the debug queries afterwards but yeah. already working on a repro
d

Dariusz Kuc

11/15/2023, 9:55 PM
so the diff in the last code is that I modify the already build schema (so it shouldnt have any type references)
if that doesnt work then I have no idea and would need to do some debugging to figure it out
s

S.

11/15/2023, 9:56 PM
still fails with the buildImpl call afterwards
d

Dariusz Kuc

11/15/2023, 9:57 PM
then something is really fishy in how those types are added/processed
(un)commenting the two
debug..
queries in BookQueryService is what breaks or fixes it
d

Dariusz Kuc

11/16/2023, 5:44 AM
the reason why it doesn't work is that you are just creating references to something that doesn't exist -> i.e. nothing else in the schema references those types so they won't be automatically created if you add debug queries then the underlying types get created hence it works (i'd still say that transform of a build schema to drop those debug queries "should" work -> haven't looked into that piece but if would be something with the graphql-java itself)
so while various workarounds might be possible I think the "simplest" way would be to provide ktor server ability to add the additional types -> please open an issue to track it
3 Views