Skip to content

Commit

Permalink
[v2] Add Integration Tests (#326)
Browse files Browse the repository at this point in the history
Following bugs where found and fixed:

* handling paging correctly
* handling duration and temporal scalars correctly
* fixing union connection issues
* using correct logical operator for `SOME` and `SINGLE` relation predicates

The test-framework is extended by the following features:

* validating query results via JsonPath in asciidoc
* testing custom resolvers via asciidoc
* using neo4j-driver connection for integration tests
  • Loading branch information
Andy2003 authored Dec 13, 2024
1 parent 8aa1548 commit daf4b30
Show file tree
Hide file tree
Showing 426 changed files with 57,506 additions and 33,550 deletions.
35 changes: 35 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threeten-extra</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
Expand Down Expand Up @@ -122,6 +127,13 @@
<version>2.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.12.0</version>
<scope>test</scope>
</dependency>

</dependencies>

<dependencyManagement>
Expand All @@ -148,4 +160,27 @@
</dependency>
</dependencies>
</dependencyManagement>

<profiles>
<profile>
<id>create-test-file-diff</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<systemPropertyVariables>
<neo4j-graphql-java.generate-test-file-diff>true</neo4j-graphql-java.generate-test-file-diff>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
17 changes: 5 additions & 12 deletions core/src/main/kotlin/org/neo4j/graphql/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import graphql.language.TypeName
import org.neo4j.graphql.domain.directives.RelationshipDirective

object Constants {
const val TYPE_NAME = "__typename"

const val JS_COMPATIBILITY: Boolean = true
const val ID_FIELD = "id"
Expand All @@ -26,18 +27,9 @@ object Constants {
const val RELATIONSHIP_FIELD = "relationship"
const val TYPENAME_IN = "typename_IN"

const val RESOLVE_TYPE = "__resolveType"
const val RESOLVE_TYPE = TYPE_NAME
const val RESOLVE_ID = "__id"

const val X = "x"
const val Y = "y"
const val Z = "z"
const val LONGITUDE = "longitude"
const val LATITUDE = "latitude"
const val HEIGHT = "height"
const val CRS = "crs"
const val SRID = "srid"

const val POINT_TYPE = "Point"
const val CARTESIAN_POINT_TYPE = "CartesianPoint"
const val POINT_INPUT_TYPE = "PointInput"
Expand Down Expand Up @@ -68,8 +60,6 @@ object Constants {
RelationshipDirective.NAME,
)

const val TYPE_NAME = "__typename"

const val OPTIONS = "options"
const val WHERE = "where"

Expand All @@ -85,6 +75,9 @@ object Constants {
val SortDirection = TypeName("SortDirection")
val PointDistance = TypeName("PointDistance")
val CartesianPointDistance = TypeName("CartesianPointDistance")

val POINT = TypeName(POINT_TYPE)
val CARTESIAN_POINT = TypeName(CARTESIAN_POINT_TYPE)
}


Expand Down
10 changes: 0 additions & 10 deletions core/src/main/kotlin/org/neo4j/graphql/CypherDataFetcherResult.kt

This file was deleted.

3 changes: 3 additions & 0 deletions core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ fun String.toLowerCase(): String = lowercase(Locale.getDefault())

infix fun Condition?.and(rhs: Condition) = this?.and(rhs) ?: rhs
infix fun Condition?.or(rhs: Condition) = this?.or(rhs) ?: rhs
infix fun Condition?.xor(rhs: Condition) = this?.xor(rhs) ?: rhs

fun Collection<Condition?>.foldWithAnd(): Condition? = this
.filterNotNull()
.takeIf { it.isNotEmpty() }
Expand Down Expand Up @@ -169,3 +171,4 @@ fun Iterable<Any?>.toDict(): List<Dict> = this.mapNotNull { Dict.create(it) }

fun String.toDeprecatedDirective() = Directive("deprecated", listOf(Argument("reason", StringValue(this))))

fun Collection<Statement>.union(): Statement = if (this.size == 1) this.first() else Cypher.union(this)
3 changes: 0 additions & 3 deletions core/src/main/kotlin/org/neo4j/graphql/QueryContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package org.neo4j.graphql
import org.neo4j.cypherdsl.core.Cypher
import org.neo4j.cypherdsl.core.Parameter
import org.neo4j.graphql.domain.fields.RelationField
import org.neo4j.graphql.driver.adapter.Neo4jAdapter.Dialect
import java.util.concurrent.atomic.AtomicInteger

data class QueryContext @JvmOverloads constructor(
var neo4jDialect: Dialect = Dialect.NEO4J_5,

val contextParams: Map<String, Any?>? = emptyMap(),
) {

Expand Down
113 changes: 67 additions & 46 deletions core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import org.neo4j.graphql.domain.directives.Annotations.Companion.LIBRARY_DIRECTI
import org.neo4j.graphql.domain.fields.RelationField
import org.neo4j.graphql.driver.adapter.Neo4jAdapter
import org.neo4j.graphql.handler.ConnectionResolver
import org.neo4j.graphql.handler.ImplementingTypeConnectionFieldResolver
import org.neo4j.graphql.handler.ReadResolver
import org.neo4j.graphql.scalars.BigIntScalar
import org.neo4j.graphql.scalars.DurationScalar
import org.neo4j.graphql.scalars.TemporalScalar
import org.neo4j.graphql.schema.AugmentationContext
import org.neo4j.graphql.schema.AugmentationHandler
import org.neo4j.graphql.schema.model.outputs.InterfaceSelection
Expand All @@ -35,69 +38,90 @@ import org.neo4j.graphql.schema.model.outputs.NodeSelection
*/
class SchemaBuilder @JvmOverloads constructor(
val typeDefinitionRegistry: TypeDefinitionRegistry,
val schemaConfig: SchemaConfig = SchemaConfig()
val schemaConfig: SchemaConfig = SchemaConfig(),
) {

companion object {
/**
* @param sdl the schema to augment
* @param neo4jAdapter the adapter to run the generated cypher queries
* @param config defines how the schema should get augmented
*/

@JvmStatic
@JvmOverloads
fun buildSchema(
sdl: String,
config: SchemaConfig = SchemaConfig(),
neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP,
addLibraryDirectivesToSchema: Boolean = true,
): GraphQLSchema {
fun fromSchema(sdl: String, config: SchemaConfig = SchemaConfig()): SchemaBuilder {
val schemaParser = SchemaParser()
val typeDefinitionRegistry = schemaParser.parse(sdl)
return buildSchema(typeDefinitionRegistry, config, neo4jAdapter, addLibraryDirectivesToSchema)
return SchemaBuilder(typeDefinitionRegistry, config)
}

/**
* @param typeDefinitionRegistry a registry containing all the types, that should be augmented
* @param config defines how the schema should get augmented
* @param sdl the schema to augment
* @param neo4jAdapter the adapter to run the generated cypher queries
* @param config defines how the schema should get augmented
*/
@JvmStatic
@JvmOverloads
fun buildSchema(
typeDefinitionRegistry: TypeDefinitionRegistry,
sdl: String,
config: SchemaConfig = SchemaConfig(),
neo4jAdapter: Neo4jAdapter,
neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP,
addLibraryDirectivesToSchema: Boolean = true,
): GraphQLSchema {

val builder = RuntimeWiring.newRuntimeWiring()
val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
val schemaBuilder = SchemaBuilder(typeDefinitionRegistry, config)
schemaBuilder.augmentTypes(addLibraryDirectivesToSchema)
schemaBuilder.registerScalars(builder)
schemaBuilder.registerTypeNameResolver(builder)
schemaBuilder.registerNeo4jAdapter(codeRegistryBuilder, neo4jAdapter)

return SchemaGenerator().makeExecutableSchema(
typeDefinitionRegistry,
builder.codeRegistry(codeRegistryBuilder).build()
)
}
): GraphQLSchema = fromSchema(sdl, config)
.withNeo4jAdapter(neo4jAdapter)
.addLibraryDirectivesToSchema(addLibraryDirectivesToSchema)
.build()
}

private val handler: List<AugmentationHandler>
private val neo4jTypeDefinitionRegistry: TypeDefinitionRegistry = getNeo4jEnhancements()
private val augmentedFields = mutableListOf<AugmentationHandler.AugmentedField>()
private val ctx = AugmentationContext(schemaConfig, typeDefinitionRegistry)
private var addLibraryDirectivesToSchema: Boolean = false;
private var codeRegistryBuilder: GraphQLCodeRegistry.Builder? = null
private var runtimeWiringBuilder: RuntimeWiring.Builder? = null
private var neo4jAdapter: Neo4jAdapter = Neo4jAdapter.NO_OP

init {
handler = mutableListOf(
ReadResolver.Factory(ctx),
ConnectionResolver.Factory(ctx),
ImplementingTypeConnectionFieldResolver.Factory(ctx)
)
}

fun addLibraryDirectivesToSchema(addLibraryDirectivesToSchema: Boolean): SchemaBuilder {
this.addLibraryDirectivesToSchema = addLibraryDirectivesToSchema
return this
}

fun withCodeRegistryBuilder(codeRegistryBuilder: GraphQLCodeRegistry.Builder): SchemaBuilder {
this.codeRegistryBuilder = codeRegistryBuilder
return this
}

fun withRuntimeWiringBuilder(runtimeWiring: RuntimeWiring.Builder): SchemaBuilder {
this.runtimeWiringBuilder = runtimeWiring
return this
}

fun withNeo4jAdapter(neo4jAdapter: Neo4jAdapter): SchemaBuilder {
this.neo4jAdapter = neo4jAdapter
return this
}

fun build(): GraphQLSchema {
augmentTypes(addLibraryDirectivesToSchema)
val runtimeWiringBuilder = this.runtimeWiringBuilder ?: RuntimeWiring.newRuntimeWiring()
registerScalars(runtimeWiringBuilder)
registerTypeNameResolver(runtimeWiringBuilder)

val codeRegistryBuilder = this.codeRegistryBuilder ?: GraphQLCodeRegistry.newCodeRegistry()
registerNeo4jAdapter(codeRegistryBuilder, neo4jAdapter)

return SchemaGenerator().makeExecutableSchema(
typeDefinitionRegistry,
runtimeWiringBuilder.codeRegistry(codeRegistryBuilder).build()
)

}


/**
* Generated additionally query and mutation fields according to the types present in the [typeDefinitionRegistry].
Expand Down Expand Up @@ -266,6 +290,12 @@ class SchemaBuilder @JvmOverloads constructor(
.forEach { (name, definition) ->
val scalar = when (name) {
Constants.BIG_INT -> BigIntScalar.INSTANCE
Constants.DATE -> TemporalScalar.DATE
Constants.TIME -> TemporalScalar.TIME
Constants.LOCAL_TIME -> TemporalScalar.LOCAL_TIME
Constants.DATE_TIME -> TemporalScalar.DATE_TIME
Constants.LOCAL_DATE_TIME -> TemporalScalar.LOCAL_DATE_TIME
Constants.DURATION -> DurationScalar.INSTANCE
else -> GraphQLScalarType.newScalar()
.name(name)
.description(
Expand Down Expand Up @@ -310,20 +340,11 @@ class SchemaBuilder @JvmOverloads constructor(
neo4jAdapter: Neo4jAdapter,
) {
codeRegistryBuilder.defaultDataFetcher { AliasPropertyDataFetcher() }
augmentedFields.forEach { augmentedField ->
val interceptedDataFetcher: DataFetcher<*> = DataFetcher { env ->
val neo4jDialect = neo4jAdapter.getDialect()
env.graphQlContext.setQueryContext(QueryContext(neo4jDialect = neo4jDialect))
val (cypher, params, type, variable) = augmentedField.dataFetcher.get(env)
val result = neo4jAdapter.executeQuery(cypher, params)
return@DataFetcher if (type?.isList() == true) {
result.map { it[variable] }
} else {
result.map { it[variable] }
.firstOrNull() ?: emptyMap<String, Any>()
}
}
codeRegistryBuilder.dataFetcher(augmentedField.coordinates, interceptedDataFetcher)
augmentedFields.forEach { (coordinates, dataFetcher) ->
codeRegistryBuilder.dataFetcher(coordinates, DataFetcher { env ->
env.graphQlContext.put(Neo4jAdapter.CONTEXT_KEY, neo4jAdapter)
dataFetcher.get(env)
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ class PointField(

enum class CoordinateType(
internal val inputType: TypeName,
internal val selectionFactory: (IResolveTree) -> BasePointSelection
internal val selectionFactory: (IResolveTree) -> BasePointSelection<*>
) {
GEOGRAPHIC(Constants.Types.PointDistance, ::PointSelection),
CARTESIAN(Constants.Types.CartesianPointDistance, ::CartesianPointSelection)
GEOGRAPHIC(Constants.Types.PointDistance, PointSelection::parse),
CARTESIAN(Constants.Types.CartesianPointDistance, CartesianPointSelection::parse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ sealed class RelationshipBaseNames<T : RelationBaseField>(

val relationshipFieldTypename get() = "${prefixForTypenameWithInheritance}Relationship"

val connectionFieldName get() = "${prefixForTypenameWithInheritance}Connection"
val connectionFieldName get() = "${relationship.fieldName}Connection"

fun getConnectionWhereTypename(target: ImplementingType) =
"$prefixForTypenameWithInheritance${target.useNameIfFieldIsUnion()}ConnectionWhere"
Expand Down
28 changes: 19 additions & 9 deletions core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import org.neo4j.cypherdsl.core.Statement
import org.neo4j.cypherdsl.core.renderer.Configuration
import org.neo4j.cypherdsl.core.renderer.Dialect
import org.neo4j.cypherdsl.core.renderer.Renderer
import org.neo4j.graphql.CypherDataFetcherResult
import org.neo4j.graphql.SchemaConfig
import org.neo4j.graphql.driver.adapter.Neo4jAdapter
import org.neo4j.graphql.queryContext
import org.neo4j.graphql.isList

/**
* This is a base class for the implementation of graphql data fetcher used in this project
*/
internal abstract class BaseDataFetcher(protected val schemaConfig: SchemaConfig) :
DataFetcher<CypherDataFetcherResult> {
DataFetcher<Any> {

final override fun get(env: DataFetchingEnvironment): CypherDataFetcherResult {
val variable = "this"
val statement = generateCypher(variable, env)
val dialect = when (env.queryContext().neo4jDialect) {
final override fun get(env: DataFetchingEnvironment): Any {
val statement = generateCypher(env)
val neo4jAdapter = env.graphQlContext.get<Neo4jAdapter?>(Neo4jAdapter.CONTEXT_KEY)
val dialect = when (neo4jAdapter.getDialect()) {
Neo4jAdapter.Dialect.NEO4J_4 -> Dialect.NEO4J_4
Neo4jAdapter.Dialect.NEO4J_5 -> Dialect.NEO4J_5
Neo4jAdapter.Dialect.NEO4J_5_23 -> Dialect.NEO4J_5_23
Expand All @@ -38,8 +37,19 @@ internal abstract class BaseDataFetcher(protected val schemaConfig: SchemaConfig
val params = statement.catalog.parameters.mapValues { (_, value) ->
(value as? VariableReference)?.let { env.variables[it.name] } ?: value
}
return CypherDataFetcherResult(query, params, env.fieldDefinition.type, variable = variable)

val result = neo4jAdapter.executeQuery(query, params)
return if (env.fieldDefinition.type?.isList() == true) {
result.map { it[RESULT_VARIABLE] }
} else {
result.map { it[RESULT_VARIABLE] }
.firstOrNull() ?: emptyMap<String, Any>()
}
}

protected abstract fun generateCypher(variable: String, env: DataFetchingEnvironment): Statement
protected abstract fun generateCypher(env: DataFetchingEnvironment): Statement

companion object {
const val RESULT_VARIABLE = "this"
}
}
Loading

0 comments on commit daf4b30

Please sign in to comment.