neo4j-graphql / neo4j-graphql-java

Neo4j Labs Project: Pure JVM translation for GraphQL queries and mutations to Neo4j's Cypher

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.