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

problems using DataFetcherInterceptorDemo

claymccoy opened this issue · comments

I tried using something based on DataFetcherInterceptorDemo and it didn't seem to work for me.

One difference is that I am using org.neo4j.ogm.session.Session because that is what org.springframework.boot:spring-boot-starter-data-neo4j easily exposes. But it still seems like it should work. Here is my impl:

import graphql.language.VariableReference
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLType
import org.neo4j.graphql.Cypher
import org.neo4j.graphql.DataFetchingInterceptor
import org.neo4j.ogm.session.SessionFactory
import java.math.BigDecimal
import java.math.BigInteger

class CypherDataFetchingInterceptor(val sessionFactory: SessionFactory) : DataFetchingInterceptor {

    override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher<Cypher>): Any? {
        val cypher = delegate.get(env)
        val session = sessionFactory.openSession()
        val result = session.query(cypher.query, cypher.params.mapValues { toBoltValue(it.value, env.variables) })
        val data = result.queryResults().map { it.entries }.flatten()
        return if (isListType(cypher.type)) {
            data
        } else {
            data.firstOrNull() ?: emptyMap<String, Any>()
        }
    }

    fun toBoltValue(value: Any?, params: Map<String, Any?>) = when (value) {
        is VariableReference -> params[value.name]
        is BigInteger -> value.longValueExact()
        is BigDecimal -> value.toDouble()
        else -> value
    }

    private fun isListType(type: GraphQLType?): Boolean {
        return when (type) {
            is GraphQLType -> true
            is GraphQLNonNull -> isListType(type.wrappedType)
            else -> false
        }
    }
}

This actually works in itself, though I am not sure if I am returning the right kind of Any in both the list and nonlist cases. The problem is that it continues through to graphql-java's AsyncExecutionStrategy.completeValue() which then tries to gather subfields of the result and ends up returning null.

The way I ultimately made querying work was implementing a subclass of AsyncExecutionStrategy and basically skipping further evaluation once the Cypher is executed.

import graphql.ExecutionResultImpl
import graphql.execution.*
import graphql.execution.FieldValueInfo.CompleteValueType
import io.armory.astrolabe.neo4j.Neo4jService
import org.neo4j.graphql.Cypher
import java.util.concurrent.CompletableFuture

class CypherExecutionStrategy(val neo4jService: Neo4jService) : AsyncExecutionStrategy() {

    override fun completeValue(executionContext: ExecutionContext, parameters: ExecutionStrategyParameters): FieldValueInfo {
        val result = executionContext.valueUnboxer.unbox(parameters.source)
        return if (result is Cypher) {
            completeValueForCypher(executionContext, parameters, result)
        }
        else {
            super.completeValue(executionContext, parameters)
        }
    }

    fun completeValueForCypher(executionContext: ExecutionContext?, parameters: ExecutionStrategyParameters, cypher: Cypher): FieldValueInfo {
        val fieldValueInfoBuilder = FieldValueInfo.newFieldValueInfo(CompleteValueType.LIST)
        val neo4jResult = neo4jService.query(cypher)
        val data = neo4jResult.queryResults().map { it.values }.flatten()
        try {
            parameters.nonNullFieldValidator.validate(parameters.path, data)
        } catch (e: NonNullableFieldWasNullException) {
            return fieldValueInfoBuilder.fieldValue(Async.exceptionallyCompletedFuture(e)).build()
        }
        return fieldValueInfoBuilder
                .fieldValue(CompletableFuture.completedFuture(ExecutionResultImpl(data, null)))
                .build()
    }
}
import org.neo4j.graphql.Cypher
import org.neo4j.ogm.model.Result
import org.neo4j.ogm.session.SessionFactory
import org.springframework.stereotype.Component

@Component
class Neo4jService(val sessionFactory: SessionFactory) {
    val session = sessionFactory.openSession()

    fun query(cypher: Cypher): Result {
        return session.query(cypher.query, cypher.params)
    }
}

Is there a better way for me to handle this that is more inline with your documentation?

@Andy2003 could you look into this?

What I've done so far is using this library in combination with:

  • org.springframework.boot:spring-boot-starter-data-neo4j (Neo4j-OGM)
  • io.leangen.graphql:graphql-spqr-spring-boot-starter (an alternative graphql library using graphql-java 13.0, I thing you are on 15.0 which is currently not supported by this lib)

The things I configured:

@Bean
@Throws(IOException::class)
fun neo4jSchema(
	@Value("classpath:graphql/neo4j/*.graphql") graphQls: Array<Resource>,
	@Autowired(required = false) dataFetchingInterceptor: DataFetchingInterceptor): GraphQLSchema {
	val idl = StringBuilder()
	for (resource in graphQls) {
		resource.inputStream.use { inputStream -> idl.append(IOUtils.toString(inputStream, StandardCharsets.UTF_8)) }
	}
	val schemaParser = SchemaParser()
	val typeDefinitionRegistry = schemaParser.parse(idl.toString())
	return SchemaBuilder.buildSchema(typeDefinitionRegistry, SchemaConfig(), dataFetchingInterceptor)
}

Can you please provide me with your configuration so I can take a look at it to create an example suitable for your setup.

As far as I remember, the

  • com.graphql-java:graphql-spring-boot-starter
  • com.graphql-java:graphql-java-tools

were not so easy to integrate with this library, especially if you want to use some custom additional java resolver

Okay, makes sense. I looked at graphql-spqr and graphql-spqr-spring-boot-starter first. What kept me from using it was that there are documented issues with using it with Kotlin, and the projects don't seem to be very active when looking at the commit history, releases, or dependency updates.
So I'm trying to use graphql-kotlin instead of graphql-spqr to generate the GraphQL schema from my model and wrote my own GraphQL controller by hand. The above code is how I got your library integrated with graph-java in that app. Just supplying a DataFetchingInterceptor wasn't enough for my situation.
Somehow your QueryHandler was available without me having to do anything, so the GraphQL automatically got converted to Cypher and I just had to make the actual call to neo4j and route the results back out shortcutting some of the stuff AsyncExecutionStrategy would do.

import graphql.ExecutionInput
import graphql.ExecutionResult
import graphql.GraphQL
import io.armory.astrolabe.neo4j.Neo4jService
import org.neo4j.graphql.*
import org.springframework.stereotype.Component

@Component
class GraphqlService(val neo4jService: Neo4jService) {
    val idl = """
        ...
        """
    val schema = SchemaBuilder.buildSchema(idl, SchemaConfig(SchemaConfig.CRUDConfig(), SchemaConfig.CRUDConfig(false)))
    val ctx = QueryContext()
    val translator = Translator(schema)
    val graphQL = GraphQL
            .newGraphQL(schema)
            .queryExecutionStrategy(CypherExecutionStrategy(neo4jService))
            .build()

    fun query(query: String, variables: Map<String, Any?> = emptyMap()): ExecutionResult {
        val executionInput: ExecutionInput = ExecutionInput.newExecutionInput()
                .query(query)
                .variables(variables)
                .build()
        return graphQL.execute(executionInput)
    }
}