query with nested filters
claymccoy opened this issue · comments
I like the way that top level queries are generated with attributes and the filter
capability (https://grandstack.io/docs/graphql-filtering/). I'd like to have this available at lower levels in the query tree. Currently, the places where I return a list of objects I am allowed to specify first
and offset
, but I'd like them all to have the same attribute and filter args that are at the top level.
I know that this behavior is possible because I can explicitly add the query and args to my type in the idl and it works. But it would seem like there would be some way that I could just have it generated (like the top level queries) rather than having to add it explicitly to every type. I don't know how I can get filter
to show up at nested levels though, even explicitly changing the idl.
I'm using onRewireGraphQLType to add arguments to the field based on the Kotlin class field type, and it works! This gives me the ability to filter any list in my model by attributes of the generic class.
For example with these classes:
@NodeEntity
data class CodeStore (
val orgName: String,
val repoName: String
) {
@Id val id: String = "$orgName:$repoName"
@Relationship(type = "STORES_COMMIT", direction = Relationship.INCOMING) val commits: List<Commit> = emptyList()
}
@NodeEntity
data class Commit (
var sha: String,
var committedDate: String,
var url: String,
@Relationship(type = "STORES_COMMIT", direction = Relationship.OUTGOING) var repo: CodeStore
) {
@Id
var id: String = "${repo.id}:$sha"
}
I can now write queries like this where I can filter on attributes in subqueries rather than just first
and offset
.
{
codeStore {
commits(sha: "b7cfb02f8dc5d4e9d3653424669746eb37ebfe9e") {
url
}
}
}
Here is the SchemaGeneratorHooks that makes it happen.
override fun onRewireGraphQLType(generatedType: GraphQLSchemaElement, coordinates: FieldCoordinates?, codeRegistry: GraphQLCodeRegistry.Builder?): GraphQLSchemaElement {
val rewiredType = super.onRewireGraphQLType(generatedType, coordinates, codeRegistry)
if (rewiredType is GraphQLFieldDefinition) {
val typeBuilder = GraphQLFieldDefinition.newFieldDefinition(rewiredType)
val field = coordinates?.let { fieldCoordinates ->
modelClassesByName[fieldCoordinates.typeName]?.let { klass ->
klass.java.declaredFields.find { it.name == fieldCoordinates.fieldName }
}
}
field?.let {
field.getAnnotation(Relationship::class.java)?.let {
typeBuilder.withDirective(constructRelationDirective(it))
if (field.type.isAssignableFrom(List::class.java)) {
val fieldGenericType = (field.genericType as ParameterizedType).actualTypeArguments[0] as Class<*>
val fieldArgs = fieldGenericType.declaredFields.filter {
!it.declaredAnnotations.any { it.annotationClass == Relationship::class } && !it.type.isAssignableFrom(List::class.java)
}
fieldArgs.forEach {
typeBuilder.argument(
GraphQLArgument.Builder()
.name(it.name)
.type(graphqlTypeFor(it.type))
.build()
)
}
}
}
field.getAnnotation(Id::class.java)?.let {
var idType: GraphQLOutputType = Scalars.GraphQLID
if (rewiredType.type is GraphQLNonNull) {
idType = GraphQLNonNull.nonNull(idType)
}
typeBuilder.type(idType)
}
}
return typeBuilder.build()
}
return rewiredType
}
val graphqlUUIDType = GraphQLScalarType.newScalar()
.name("Long")
.description("64 bit number")
.coercing(LongCoercing)
.build()
fun graphqlTypeFor(type: Class<*>): GraphQLInputType? {
return when (type) {
java.lang.Long::class.java -> graphqlUUIDType
Long::class.java -> graphqlUUIDType
Int::class.java -> Scalars.GraphQLInt
String::class.java -> Scalars.GraphQLString
Boolean::class.java -> Scalars.GraphQLBoolean
else -> throw IllegalArgumentException("Handle GraphQL translation for Java type: ${type}")
}
}
override fun willGenerateGraphQLType(type: KType): GraphQLType? {
return when (type.classifier as? KClass<*>) {
Long::class -> graphqlUUIDType
else -> null
}
}
}
object LongCoercing : Coercing<Long, String> {
override fun parseValue(input: Any?): Long = serialize(input).toLong()
override fun parseLiteral(input: Any?): Long? {
val uuidString = (input as? StringValue)?.value
return uuidString?.toLong()
}
override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString()
}
This is nice, but I was hoping there was a setting to have this done for me as it does at the top level. I'd also like to add filter
but I don't see a clear way to do that. And I suspect I am doing some things the hard way.
Specifically, graphqlTypeFor() where I take a java class and map it to the proper Scalar. I would assume that I would be able to use something that already exists in the lib to do this. I may not have had to write graphqlUUIDType. I see GraphQLLong now, but when I originally wrote this I got errors using Long in my model. It is weird that I have to deal with both java.lang.Long
and kotling.Long
but I see them both in my model even though I only use kotlin Long.