From e2a5f28d864f5815cf26f588f3bb22d697c52bd5 Mon Sep 17 00:00:00 2001 From: Andreas Berger Date: Thu, 1 Apr 2021 19:31:34 +0200 Subject: [PATCH] update to neo4j 4.2 this requires also an update to java 11 additionally I cleaned up the dependencies --- core/pom.xml | 61 +++------ .../neo4j/graphql/handler/BaseDataFetcher.kt | 2 +- .../test/kotlin/DataFetcherInterceptorDemo.kt | 23 ++-- core/src/test/kotlin/GraphQLServer.kt | 126 ++++++++++-------- .../neo4j/graphql/utils/AsciiDocTestSuite.kt | 4 +- .../neo4j/graphql/utils/CypherTestSuite.kt | 38 +++--- core/src/test/resources/custom-fields.adoc | 16 +-- core/src/test/resources/filter-tests.adoc | 3 - .../config/Neo4jConfiguration.kt | 15 +-- pom.xml | 10 +- 10 files changed, 143 insertions(+), 155 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index d4d86d44..b1ff5d6f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -15,45 +15,23 @@ GraphQL to Cypher Mapping - - org.neo4j.driver - neo4j-java-driver - test - - - com.sparkjava - spark-core - 2.7.2 - test - - - com.google.code.gson - gson - 2.8.5 - test - org.neo4j.test neo4j-harness ${neo4j.version} test + + + slf4j-nop + org.slf4j + + org.neo4j.procedure apoc ${neo4j-apoc.version} - test - - - org.neo4j - server-api - ${neo4j.version} - test - - - org.codehaus.jackson - jackson-mapper-asl - 1.9.13 + all test @@ -66,16 +44,9 @@ junit-jupiter test - - junit - junit - 4.13.1 - test - org.assertj assertj-core - 3.12.2 test @@ -94,19 +65,19 @@ - org.neo4j.driver - neo4j-java-driver - ${driver.version} + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} - org.neo4j - neo4j-kernel - ${neo4j.version} + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} - org.junit.jupiter - junit-jupiter - 5.5.1 + org.assertj + assertj-core + 3.19.0 org.slf4j diff --git a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt index 77affd9c..35459824 100644 --- a/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt +++ b/core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt @@ -29,7 +29,7 @@ abstract class BaseDataFetcher(val fieldDefinition: GraphQLFieldDefinition) : Pr .withPrettyPrint(true) .build() ).render(statement) - return Cypher(query, statement.parameters, fieldDefinition.type, variable = variable) + return Cypher(query, statement.parameters, fieldDefinition.type, variable = field.aliasOrName()) } protected abstract fun generateCypher(variable: String, field: Field, env: DataFetchingEnvironment): Statement diff --git a/core/src/test/kotlin/DataFetcherInterceptorDemo.kt b/core/src/test/kotlin/DataFetcherInterceptorDemo.kt index d0c5c188..ad968597 100644 --- a/core/src/test/kotlin/DataFetcherInterceptorDemo.kt +++ b/core/src/test/kotlin/DataFetcherInterceptorDemo.kt @@ -1,11 +1,11 @@ package demo import graphql.GraphQL -import graphql.language.VariableReference import graphql.schema.* import org.intellij.lang.annotations.Language -import org.neo4j.driver.v1.AuthTokens -import org.neo4j.driver.v1.GraphDatabase +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.Driver +import org.neo4j.driver.GraphDatabase import org.neo4j.graphql.Cypher import org.neo4j.graphql.DataFetchingInterceptor import org.neo4j.graphql.SchemaBuilder @@ -14,19 +14,19 @@ import java.math.BigInteger fun initBoundSchema(schema: String): GraphQLSchema { - val driver = GraphDatabase.driver("bolt://localhost", AuthTokens.basic("neo4j", "test")) + val driver: Driver = GraphDatabase.driver("bolt://localhost", AuthTokens.basic("neo4j", "test")) val dataFetchingInterceptor = object : DataFetchingInterceptor { override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any { - val cypher = delegate.get(env) + val (cypher, params, type, variable) = delegate.get(env) return driver.session().use { session -> - val result = session.run(cypher.query, cypher.params.mapValues { toBoltValue(it.value, env.variables) }) - if (isListType(cypher.type)) { - result.list().map { record -> record.get(cypher.variable).asObject() } + val result = session.run(cypher, params.mapValues { toBoltValue(it.value) }) + if (isListType(type)) { + result.list().map { record -> record.get(variable).asObject() } } else { - result.list().map { record -> record.get(cypher.variable).asObject() } - .firstOrNull() ?: emptyMap() + result.list().map { record -> record.get(variable).asObject() }.firstOrNull() + ?: emptyMap() } } } @@ -45,8 +45,7 @@ fun main() { val movies = graphql.execute("{ movie { title }}") } -fun toBoltValue(value: Any?, params: Map) = when (value) { - is VariableReference -> params[value.name] +fun toBoltValue(value: Any?) = when (value) { is BigInteger -> value.longValueExact() is BigDecimal -> value.toDouble() else -> value diff --git a/core/src/test/kotlin/GraphQLServer.kt b/core/src/test/kotlin/GraphQLServer.kt index 2d5ef455..e1ee63e2 100644 --- a/core/src/test/kotlin/GraphQLServer.kt +++ b/core/src/test/kotlin/GraphQLServer.kt @@ -4,17 +4,21 @@ package demo // curl -H content-type:application/json -d'{"query": "{ movie { title, released }}"}' http://localhost:4567/graphql // GraphiQL: https://neo4j-graphql.github.io/graphiql4all/index.html?graphqlEndpoint=http%3A%2F%2Flocalhost%3A4567%2Fgraphql&query=query%20%7B%0A%20%20movie%20%7B%20title%7D%0A%7D -import com.google.gson.Gson +import com.fasterxml.jackson.databind.ObjectMapper +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import graphql.ExecutionInput import graphql.GraphQL -import org.neo4j.driver.v1.AuthTokens -import org.neo4j.driver.v1.Config -import org.neo4j.driver.v1.GraphDatabase -import org.neo4j.driver.v1.Values +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.neo4j.driver.AuthTokens +import org.neo4j.driver.Config +import org.neo4j.driver.GraphDatabase +import org.neo4j.driver.Values import org.neo4j.graphql.* -import spark.Request -import spark.Response -import spark.Spark -import java.util.* +import java.net.InetSocketAddress +import kotlin.streams.toList + const val schema = """ type Person { @@ -29,69 +33,85 @@ type Movie { } """ +val mapper = ObjectMapper() + fun main() { - val gson = Gson() - fun render(value: Any) = gson.toJson(value) - fun parseBody(value: String) = gson.fromJson(value, Map::class.java) fun query(payload: Map<*, *>) = (payload["query"]!! as String).also { println(it) } fun params(payload: Map<*, *>): Map = payload["variables"] .let { @Suppress("UNCHECKED_CAST") when (it) { - is String -> if (it.isBlank()) emptyMap() else gson.fromJson(it, Map::class.java) + is String -> if (it.isBlank()) emptyMap() else mapper.readValue(it, Map::class.java) is Map<*, *> -> it else -> emptyMap() } as Map }.also { println(it) } + val driver = GraphDatabase.driver("bolt://localhost", AuthTokens.basic("neo4j", "test"), Config.builder().withoutEncryption().build()) + + val graphQLSchema = SchemaBuilder.buildSchema(schema, dataFetchingInterceptor = object : DataFetchingInterceptor { + override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? { + val (cypher, params, type, variable) = delegate.get(env) + println(cypher) + println(params) + return driver.session().use { session -> + try { + val result = session.run(cypher, Values.value(params)) + when { + type?.isList() == true -> result.stream().map { it[variable].asObject() }.toList() + else -> result.stream().map { it[variable].asObject() }.findFirst().orElse(emptyMap()) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + }) - val graphQLSchema = SchemaBuilder.buildSchema(schema) - println(graphQLSchema) val schema = GraphQL.newGraphQL(graphQLSchema).build() - val translator = Translator(graphQLSchema) - fun translate(query: String, params: Map) = try { - val ctx = QueryContext(optimizedQuery = setOf(QueryContext.OptimizationStrategy.FILTER_AS_MATCH)) - translator.translate(query, params, ctx) - } catch (e: OptimizedQueryException) { - translator.translate(query, params) - } - val driver = GraphDatabase.driver("bolt://localhost", AuthTokens.basic("neo4j", "test"), Config.build().withoutEncryption().build()) - fun run(cypher: Cypher) = driver.session().use { - println(cypher.query) - println(cypher.params) - try { - // todo fix parameter mapping in translator - val result = it.run(cypher.query, Values.value(cypher.params)) - val value = if (cypher.type?.isList() == true) { - result.list().map { row -> row.get(cypher.variable).asObject() } - } else { - result.list().map { record -> record.get(cypher.variable).asObject() } - .firstOrNull() ?: emptyMap() + val server: HttpServer = HttpServer.create(InetSocketAddress(4567), 0) + + server.createContext("/graphql") { req -> + when { + req.requestMethod == "OPTIONS" -> req.sendResponse(null) + req.requestMethod == "POST" && req.requestHeaders["Content-Type"]?.contains("application/json") == true -> { + val payload = mapper.readValue(req.requestBody, Map::class.java) + val query = query(payload) + val response = if (query.contains("__schema")) { + schema.execute(query).let { println(mapper.writeValueAsString(it));it } + } else { + try { + schema.execute(ExecutionInput + .newExecutionInput() + .query(query) + .context(QueryContext(optimizedQuery = setOf(QueryContext.OptimizationStrategy.FILTER_AS_MATCH))) + .variables(params(payload)) + .build()) + } catch (e: OptimizedQueryException) { + schema.execute(ExecutionInput + .newExecutionInput() + .query(query) + .variables(params(payload)) + .build()) + } + } + req.sendResponse(response) } - Collections.singletonMap(cypher.variable, value) - } catch (e: Exception) { - e.printStackTrace() } } + server.start() - // CORS - Spark.before("/*") { req, res -> - res.header("Access-Control-Allow-Origin", "*") - res.header("Access-Control-Allow-Headers", "*") - res.type("application/json") - } - - fun handler(req: Request, @Suppress("UNUSED_PARAMETER") res: Response) = req.body().let { body -> - val payload = parseBody(body) - val query = query(payload) - if (query.contains("__schema")) - schema.execute(query).let { println(render(it));it } - else run(translate(query, params(payload)).first()) - } +} - Spark.options("/graphql") { _, _ -> "OK" } - Spark.post("/graphql", "application/json", ::handler, ::render) +private fun HttpExchange.sendResponse(data: Any?) { + val responseString = data?.let { mapper.writeValueAsString(it) } + // CORS + this.responseHeaders.add("Access-Control-Allow-Origin", "*") + this.responseHeaders.add("Access-Control-Allow-Headers", "*") + this.responseHeaders.add("Content-Type", "application/json") + this.sendResponseHeaders(200, responseString?.length?.toLong() ?: 0) + if (responseString != null) this.responseBody.use { it.write(responseString.toByteArray()) } } diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt index ee9508dc..4e0f2620 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/AsciiDocTestSuite.kt @@ -1,7 +1,7 @@ package org.neo4j.graphql.utils +import com.fasterxml.jackson.databind.ObjectMapper import com.intellij.rt.execution.junit.FileComparisonFailure -import org.codehaus.jackson.map.ObjectMapper import org.junit.jupiter.api.DynamicContainer import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest @@ -269,7 +269,7 @@ open class AsciiDocTestSuite( } } - private fun fixNumber(v: Any?): Any? = when (v) { + fun fixNumber(v: Any?): Any? = when (v) { is Float -> v.toDouble() is Int -> v.toLong() is Iterable<*> -> v.map { fixNumber(it) } diff --git a/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt index e5ff9500..e2a937ae 100644 --- a/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt +++ b/core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt @@ -1,13 +1,14 @@ package org.neo4j.graphql.utils +import apoc.cypher.CypherFunctions import org.assertj.core.api.Assertions import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.DynamicNode import org.junit.jupiter.api.DynamicTest import org.neo4j.graphql.* -import org.neo4j.harness.TestServerBuilders +import org.neo4j.harness.Neo4jBuilders import org.opentest4j.AssertionFailedError -import java.io.File +import java.nio.file.Path import java.util.* import java.util.concurrent.FutureTask import kotlin.streams.toList @@ -141,35 +142,36 @@ class CypherTestSuite(fileName: String) : AsciiDocTestSuite( } private fun integrationTest(testData: ParsedBlock, response: ParsedBlock, result: () -> Cypher): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) { - TestServerBuilders - .newInProcessBuilder(File("target/test-db")) + Neo4jBuilders + .newInProcessBuilder(Path.of("target/test-db")) .withProcedure(apoc.cypher.Cypher::class.java) - .withFunction(apoc.cypher.CypherFunctions::class.java) - .newServer() - .use { server -> + .withFunction(CypherFunctions::class.java) + .also { builder -> if (testData.code().isNotBlank()) { testData.code() .split(";") .filter { it.isNotBlank() } - .forEach { server.graph().execute(it) } + .forEach { builder.withFixture(it) } } - + } + .build() + .use { neo4j -> val (cypher, params, type, variable) = result() - val dbResult = server.graph().execute(cypher, params) - - val values = mutableMapOf(variable to dbResult.stream().map { it[variable] }.let { - when { - type?.isList() == true -> it.toList() - else -> it.findFirst().orElse(null) - } - }) + val values = neo4j.defaultDatabaseService().executeTransactionally(cypher, params) { result -> + mutableMapOf(variable to result.stream().map { it[variable] }.let { + when { + type?.isList() == true -> it.toList() + else -> it.findFirst().orElse(null) + } + }) + } if (response.code.isEmpty()) { val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) response.adjustedCode = actualCode } else { val expected = fixNumbers(response.code().parseJsonMap()) - val actual = fixNumbers(values) + val actual = fixNumber(values) if (!Objects.equals(expected, actual)) { val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values) response.adjustedCode = actualCode diff --git a/core/src/test/resources/custom-fields.adoc b/core/src/test/resources/custom-fields.adoc index 4066711b..132d86d4 100644 --- a/core/src/test/resources/custom-fields.adoc +++ b/core/src/test/resources/custom-fields.adoc @@ -31,13 +31,13 @@ schema { query: QueryType } type QueryType { - Person(name:String) : Person @cypher(statement:"MATCH (p:Person { name:{name} }) RETURN p") - getOrderPosition(sid:ID!) : [OrderPosition] @cypher(statement:"MATCH (op:OrderPosition {sid: {sid}}) RETURN op") + Person(name:String) : Person @cypher(statement:"MATCH (p:Person { name: $name }) RETURN p") + getOrderPosition(sid:ID!) : [OrderPosition] @cypher(statement:"MATCH (op:OrderPosition {sid: $sid}) RETURN op") } type MutationType { - createPerson(name:String) : Person @cypher(statement:"CREATE (p:Person { name: \"Test\"+{name} }) RETURN p") - setOrderPositionAmount(sid:ID!, amount:Int) : [OrderPosition] @cypher(statement:"MATCH (op:OrderPosition {sid:{sid}}) SET op.amount = {amount} RETURN op") + createPerson(name:String) : Person @cypher(statement:"CREATE (p:Person { name: \"Test\"+$name }) RETURN p") + setOrderPositionAmount(sid:ID!, amount:Int) : [OrderPosition] @cypher(statement:"MATCH (op:OrderPosition {sid:{sid}}) SET op.amount = $amount RETURN op") } ---- @@ -86,7 +86,7 @@ mutation { createPerson(name:"Jill") {name} } .Cypher [source,cypher] ---- -CALL apoc.cypher.doIt('WITH $name AS name CREATE (p:Person { name: \"Test\"+{name} }) RETURN p', { +CALL apoc.cypher.doIt('WITH $name AS name CREATE (p:Person { name: \"Test\"+$name }) RETURN p', { name: $createPersonName }) YIELD value WITH value[head(keys(value))] AS createPerson @@ -131,7 +131,7 @@ query { Person(name:"Jane") {name} } .Cypher [source,cypher] ---- -UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name:{name} }) RETURN p', { +UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name: $name }) RETURN p', { name: $personName }) AS person RETURN person { @@ -169,7 +169,7 @@ query { Person(name:"Jane") {name, label} } .Cypher [source,cypher] ---- -UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name:{name} }) RETURN p', { +UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name: $name }) RETURN p', { name: $personName }) AS person RETURN person { @@ -210,7 +210,7 @@ query { Person(name:"Jane") {name, nullable} } .Cypher [source,cypher] ---- -UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name:{name} }) RETURN p', { +UNWIND apoc.cypher.runFirstColumnSingle('WITH $name AS name MATCH (p:Person { name: $name }) RETURN p', { name: $personName }) AS person RETURN person { diff --git a/core/src/test/resources/filter-tests.adoc b/core/src/test/resources/filter-tests.adoc index 1505d5f9..3710d72f 100644 --- a/core/src/test/resources/filter-tests.adoc +++ b/core/src/test/resources/filter-tests.adoc @@ -22,9 +22,6 @@ type Company { name: String employees: [Person] @relation(name:"WORKS_AT", direction: IN) } -type Query { - person : [Person] -} ---- == Test Data diff --git a/examples/graphql-spring-boot/src/main/kotlin/org/neo4j/graphql/examples/graphqlspringboot/config/Neo4jConfiguration.kt b/examples/graphql-spring-boot/src/main/kotlin/org/neo4j/graphql/examples/graphqlspringboot/config/Neo4jConfiguration.kt index 5ebfc196..e41cbde9 100644 --- a/examples/graphql-spring-boot/src/main/kotlin/org/neo4j/graphql/examples/graphqlspringboot/config/Neo4jConfiguration.kt +++ b/examples/graphql-spring-boot/src/main/kotlin/org/neo4j/graphql/examples/graphqlspringboot/config/Neo4jConfiguration.kt @@ -25,17 +25,17 @@ open class Neo4jConfiguration { return object : DataFetchingInterceptor { override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher): Any? { - val cypher = delegate.get(env) + val (cypher, params, type, variable) = delegate.get(env) return driver.session().writeTransaction { tx -> - val boltParams = cypher.params.mapValues { toBoltValue(it.value, env.variables) } - val result = tx.run(cypher.query, boltParams) - if (isListType(cypher.type)) { + val boltParams = params.mapValues { toBoltValue(it.value) } + val result = tx.run(cypher, boltParams) + if (isListType(type)) { result.list() - .map { record -> record.get(cypher.variable).asObject() } + .map { record -> record.get(variable).asObject() } } else { result.list() - .map { record -> record.get(cypher.variable).asObject() } + .map { record -> record.get(variable).asObject() } .firstOrNull() ?: emptyMap() } } @@ -44,8 +44,7 @@ open class Neo4jConfiguration { } companion object { - private fun toBoltValue(value: Any?, params: Map) = when (value) { - is VariableReference -> params[value.name] + private fun toBoltValue(value: Any?) = when (value) { is BigInteger -> value.longValueExact() is BigDecimal -> value.toDouble() else -> value diff --git a/pom.xml b/pom.xml index 5ab50b6f..2f13afc9 100755 --- a/pom.xml +++ b/pom.xml @@ -22,12 +22,12 @@ UTF-8 - 1.8 - 1.4.30 + 11 + 1.4.31 ${java.version} - 3.5.23 - 3.5.0.15 - 1.7.5 + 4.2.4 + 4.2.0.2 + 5.7.1