Skip to content

Commit

Permalink
Add ability to ignore fields (via @ignore) so they can be handled by …
Browse files Browse the repository at this point in the history
…custom data fetcher (neo4j-graphql#226)
  • Loading branch information
Andy2003 authored May 20, 2021
1 parent c0faaf1 commit 329a7d7
Show file tree
Hide file tree
Showing 20 changed files with 502 additions and 61 deletions.
11 changes: 8 additions & 3 deletions core/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ abstract class AugmentationHandler(
.build())
}
type.fieldDefinitions
.filterNot { it.isIgnored() }
.filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
.forEach { field ->
val typeDefinition = field.type.resolve()
Expand Down Expand Up @@ -272,11 +273,16 @@ abstract class AugmentationHandler(
fun ImplementingTypeDefinition<*>.relationship(): RelationshipInfo<ImplementingTypeDefinition<*>>? = RelationshipInfo.create(this, neo4jTypeDefinitionRegistry)

fun ImplementingTypeDefinition<*>.getScalarFields(): List<FieldDefinition> = fieldDefinitions
.filterNot { it.isIgnored() }
.filter { it.type.inner().isScalar() || it.type.inner().isNeo4jType() }
.sortedByDescending { it.type.inner().isID() }

fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions.find { it.name == name }
fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions.find { it.type.inner().isID() }
fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions
.filterNot { it.isIgnored() }
.find { it.name == name }
fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions
.filterNot { it.isIgnored() }
.find { it.type.inner().isID() }

fun Type<*>.resolve(): TypeDefinition<*>? = getTypeFromAnyRegistry(name())
fun Type<*>.isScalar(): Boolean = resolve() is ScalarTypeDefinition
Expand All @@ -296,7 +302,6 @@ abstract class AugmentationHandler(
fun FieldDefinition.isRelationship(): Boolean =
!type.inner().isNeo4jType() && type.resolve() is ImplementingTypeDefinition<*>


fun TypeDefinitionRegistry.getUnwrappedType(name: String?): TypeDefinition<TypeDefinition<*>>? = getType(name)?.unwrap()

fun DirectivesContainer<*>.cypherDirective(): CypherDirective? = if (hasDirective(DirectiveConstants.CYPHER)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class DirectiveConstants {

companion object {

const val IGNORE = "ignore"
const val RELATION = "relation"
const val RELATION_NAME = "name"
const val RELATION_DIRECTION = "direction"
Expand Down
20 changes: 17 additions & 3 deletions core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fun GraphQLFieldDefinition.isRelationship() = !type.isNeo4jType() && this.type.i

fun GraphQLFieldsContainer.isRelationType() = (this as? GraphQLDirectiveContainer)?.getDirective(DirectiveConstants.RELATION) != null
fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo<GraphQLFieldsContainer>? {
val field = getFieldDefinition(name)
val field = getRelevantFieldDefinition(name)
?: throw IllegalArgumentException("$name is not defined on ${this.name}")
val fieldObjectType = field.type.inner() as? GraphQLImplementingType ?: return null

Expand All @@ -62,7 +62,7 @@ fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo<Graph
(this as? GraphQLDirectiveContainer)
?.getDirective(DirectiveConstants.RELATION)?.let {
// do inverse mapping, if the current type is the `to` mapping of the relation
it to (fieldObjectType.getFieldDefinition(it.getArgument(RELATION_TO, null))?.name == typeName)
it to (fieldObjectType.getRelevantFieldDefinition(it.getArgument(RELATION_TO, null))?.name == typeName)
}
?: throw IllegalStateException("Type ${this.name} needs an @relation directive")
} else {
Expand Down Expand Up @@ -184,7 +184,21 @@ fun Value<*>.toJavaValue(): Any? = when (this) {

fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
fun GraphQLFieldsContainer.getIdField() = this.fieldDefinitions.find { it.isID() }
fun GraphQLFieldDefinition.isIgnored() = getDirective(DirectiveConstants.IGNORE) != null
fun FieldDefinition.isIgnored(): Boolean = hasDirective(DirectiveConstants.IGNORE)

fun GraphQLFieldsContainer.getIdField() = this.getRelevantFieldDefinitions().find { it.isID() }

/**
* Returns the field definitions which are not ignored
*/
fun GraphQLFieldsContainer.getRelevantFieldDefinitions() = this.fieldDefinitions.filterNot { it.isIgnored() }

/**
* Returns the field definition if it is not ignored
*/
fun GraphQLFieldsContainer.getRelevantFieldDefinition(name: String?) = this.getFieldDefinition(name)?.takeIf { !it.isIgnored() }


fun InputObjectTypeDefinition.Builder.addFilterField(fieldName: String, isList: Boolean, filterType: String, description: Description? = null) {
val wrappedType: Type<*> = when {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class SchemaBuilder(
typeDefinitionRegistry.getType(parentType)?.unwrap()
?.let { it as? ObjectTypeDefinition }
?.fieldDefinitions
?.filterNot { it.isIgnored() }
?.forEach { field ->
handler.forEach { h ->
h.createDataFetcher(operationType, field)?.let { dataFetcher ->
Expand Down
8 changes: 4 additions & 4 deletions core/src/main/kotlin/org/neo4j/graphql/Translator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ class Translator(val schema: GraphQLSchema) {
when (op) {
QUERY -> {
operationObjectType = schema.queryType
fieldDefinition = operationObjectType.getFieldDefinition(name)
?: throw IllegalArgumentException("Unknown Query $name available queries: " + (operationObjectType.fieldDefinitions).joinToString { it.name })
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
?: throw IllegalArgumentException("Unknown Query $name available queries: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
}
MUTATION -> {
operationObjectType = schema.mutationType
fieldDefinition = operationObjectType.getFieldDefinition(name)
?: throw IllegalArgumentException("Unknown Mutation $name available mutations: " + (operationObjectType.fieldDefinitions).joinToString { it.name })
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
?: throw IllegalArgumentException("Unknown Mutation $name available mutations: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
}
else -> throw IllegalArgumentException("$op is not supported")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AugmentFieldHandler(
}

private fun augmentRelation(fieldBuilder: FieldDefinition.Builder, field: FieldDefinition) {
if (!field.isRelationship() || !field.type.isList()) {
if (!field.isRelationship() || !field.type.isList() || field.isIgnored()) {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ abstract class BaseDataFetcherForContainer(schemaConfig: SchemaConfig) : BaseDat
defaultFields[arg.name] = arg.defaultValue
}
}
.mapNotNull { type.getFieldDefinition(it.name) }
.mapNotNull { type.getRelevantFieldDefinition(it.name) }
.forEach { field ->
val dynamicPrefix = field.dynamicPrefix()
propertyFields[field.name] = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ class QueryHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFet
return true
}

private fun hasRelationships(type: ImplementingTypeDefinition<*>): Boolean = type.fieldDefinitions.any { it.isRelationship() }
private fun hasRelationships(type: ImplementingTypeDefinition<*>): Boolean = type.fieldDefinitions
.filterNot { it.isIgnored() }
.any { it.isRelationship() }

private fun getRelevantFields(type: ImplementingTypeDefinition<*>): List<FieldDefinition> {
return type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,9 @@ open class ProjectionBase(

private fun projectField(propertyContainer: PropertyContainer, variable: SymbolicName, field: Field, type: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?, propertiesToSkipDeepProjection: Set<String> = emptySet()): List<Any> {
val projections = mutableListOf<Any>()
projections += field.aliasOrName()

if (field.name == TYPE_NAME) {
projections += field.aliasOrName()
if (type.isRelationType()) {
projections += literalOf<Any>(type.name)
} else {
Expand All @@ -250,8 +251,13 @@ open class ProjectionBase(
}
return projections
}

val fieldDefinition = type.getFieldDefinition(field.name)
?: throw IllegalStateException("No field ${field.name} in ${type.name}")
if (fieldDefinition.isIgnored()) {
return projections
}
projections += field.aliasOrName()
val cypherDirective = fieldDefinition.cypherDirective()
val isObjectField = fieldDefinition.type.inner() is GraphQLFieldsContainer
if (cypherDirective != null) {
Expand Down Expand Up @@ -379,8 +385,8 @@ open class ProjectionBase(
parent: GraphQLFieldsContainer,
relDirectiveField: RelationshipInfo<GraphQLFieldsContainer>?
): RelationshipInfo<GraphQLFieldsContainer> {
val startField = fieldObjectType.getFieldDefinition(relInfo0.startField)!!
val endField = fieldObjectType.getFieldDefinition(relInfo0.endField)!!
val startField = fieldObjectType.getRelevantFieldDefinition(relInfo0.startField)!!
val endField = fieldObjectType.getRelevantFieldDefinition(relInfo0.endField)!!
val startFieldTypeName = startField.type.innerName()
val inverse = startFieldTypeName != parent.name
|| startFieldTypeName == endField.type.innerName()
Expand Down Expand Up @@ -418,7 +424,7 @@ open class ProjectionBase(
relInfo.endField -> Triple(anyNode(), node, node)
else -> throw IllegalArgumentException("type ${parent.name} does not have a matching field with name ${fieldDefinition.name}")
}
val rel = relInfo.createRelation(start, end, false,variable)
val rel = relInfo.createRelation(start, end, false, variable)
return head(CypherDSL.listBasedOn(rel).returning(target.project(projectFields(target, field, fieldDefinition.type as GraphQLFieldsContainer, env))))
}

Expand All @@ -442,7 +448,7 @@ open class ProjectionBase(

val (endNodePattern, variableSuffix) = when {
isRelFromType -> {
val label = nodeType.getFieldDefinition(relInfo.endField)!!.type.innerName()
val label = nodeType.getRelevantFieldDefinition(relInfo.endField)!!.type.innerName()
node(label).named("$childVariable${relInfo.endField.capitalize()}") to relInfo.endField
}
else -> node(nodeType.name).named(childVariableName) to null
Expand All @@ -451,7 +457,7 @@ open class ProjectionBase(
val skipLimit = SkipLimit(childVariable, field.arguments, fieldDefinition)
val orderBy = getOrderByArgs(field.arguments, fieldDefinition, env.variables)
val sortByNeo4jTypeFields = orderBy
.filter { (property, _) -> nodeType.getFieldDefinition(property)?.isNeo4jType() == true }
.filter { (property, _) -> nodeType.getRelevantFieldDefinition(property)?.isNeo4jType() == true }
.map { (property, _) -> property }
.toSet()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ abstract class BaseRelationHandler(val prefix: String, schemaConfig: SchemaConfi
if (!targetField.hasDirective(DirectiveConstants.RELATION)) {
return false
}
if (targetField.isIgnored()) {
return false
}
if (type.getIdField() == null) {
return false
}
Expand Down Expand Up @@ -156,7 +159,7 @@ abstract class BaseRelationHandler(val prefix: String, schemaConfig: SchemaConfi
.removePrefix(p)
.decapitalize()
.let {
type.getFieldDefinition(it) ?: throw IllegalStateException("Cannot find field $it on type ${type.name}")
type.getRelevantFieldDefinition(it) ?: throw IllegalStateException("Cannot find field $it on type ${type.name}")
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class CreateRelationHandler private constructor(schemaConfig: SchemaConfig) : Ba

relationType
?.fieldDefinitions
?.filterNot { it.isIgnored() }
?.filter { it.type.inner().isScalar() && !it.type.inner().isID() }
?.forEach { builder.inputValueDefinition(input(it.name, it.type)) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,22 @@ class CreateRelationTypeHandler private constructor(schemaConfig: SchemaConfig)

val relType = relFieldDefinition.type.inner().resolve() as? ImplementingTypeDefinition<*>
?: throw IllegalArgumentException("type ${relFieldDefinition.type.name()} not found")
return relType.fieldDefinitions.filter { it.type.inner().isID() }
return relType.fieldDefinitions
.filterNot { it.isIgnored() }
.filter { it.type.inner().isID() }
.map { RelatedField(normalizeFieldName(relFieldName, it.name), it) }
.firstOrNull()
}

}

private fun getRelatedIdField(info: RelationshipInfo<GraphQLFieldsContainer>, relFieldName: String): RelatedField {
val relFieldDefinition = info.type.getFieldDefinition(relFieldName)
val relFieldDefinition = info.type.getRelevantFieldDefinition(relFieldName)
?: throw IllegalArgumentException("field $relFieldName does not exists on ${info.typeName}")

val relType = relFieldDefinition.type.inner() as? GraphQLImplementingType
?: throw IllegalArgumentException("type ${relFieldDefinition.type.name()} not found")
return relType.fieldDefinitions.filter { it.isID() }
return relType.getRelevantFieldDefinitions().filter { it.isID() }
.map { RelatedField(normalizeFieldName(relFieldName, it.name), it, relType) }
.firstOrNull()
?: throw IllegalStateException("Cannot find id field for type ${info.typeName}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ object QueryParser {
// find all matching fields
val fieldPredicates = mutableListOf<FieldPredicate>()
val relationPredicates = mutableListOf<RelationPredicate>()
for (definedField in type.fieldDefinitions) {
for (definedField in type.getRelevantFieldDefinitions()) {
if (definedField.isRelationship()) {
RelationOperator.values()
.map { it to definedField.name + it.suffix }
Expand Down
1 change: 1 addition & 0 deletions core/src/main/resources/lib_directives.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ directive @cypher(

directive @property(name:String) on FIELD_DEFINITION
directive @dynamic(prefix:String = "properties.") on FIELD_DEFINITION
directive @ignore on FIELD_DEFINITION
16 changes: 2 additions & 14 deletions core/src/test/kotlin/org/neo4j/graphql/AugmentationTests.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package org.neo4j.graphql

import org.junit.jupiter.api.DynamicContainer
import org.junit.jupiter.api.DynamicNode
import demo.org.neo4j.graphql.utils.TestUtils.createTestsInPath
import org.junit.jupiter.api.TestFactory
import org.neo4j.graphql.utils.GraphQLSchemaTestSuite
import java.nio.file.Files
import java.nio.file.Paths
import java.util.stream.Stream

class AugmentationTests {

Expand All @@ -17,13 +13,5 @@ class AugmentationTests {
fun `schema-operations-tests`() = GraphQLSchemaTestSuite("schema-operations-tests.adoc").generateTests()

@TestFactory
fun `schema augmentation tests`(): Stream<DynamicNode>? = Files
.list(Paths.get("src/test/resources/tck-test-files/schema"))
.map {
DynamicContainer.dynamicContainer(
it.fileName.toString(),
it.toUri(),
GraphQLSchemaTestSuite("tck-test-files/schema/${it.fileName}").generateTests()
)
}
fun `schema augmentation tests`() = createTestsInPath("tck-test-files/schema", { GraphQLSchemaTestSuite(it).generateTests() })
}
30 changes: 7 additions & 23 deletions core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package org.neo4j.graphql

import apoc.coll.Coll
import apoc.cypher.CypherFunctions
import org.junit.jupiter.api.*
import demo.org.neo4j.graphql.utils.TestUtils.createTestsInPath
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.TestInstance
import org.neo4j.graphql.utils.CypherTestSuite
import org.neo4j.harness.Neo4j
import org.neo4j.harness.Neo4jBuilders
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.stream.Stream

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CypherTests {
Expand Down Expand Up @@ -71,27 +72,10 @@ class CypherTests {
fun `custom-fields`() = CypherTestSuite("custom-fields.adoc", neo4j).generateTests()

@TestFactory
fun `test issues`(): Stream<DynamicNode>? = Files
.list(Paths.get("src/test/resources/issues"))
.map {
DynamicContainer.dynamicContainer(
it.fileName.toString(),
it.toUri(),
CypherTestSuite("issues/${it.fileName}", neo4j).generateTests()
)
}

fun `test issues`() = createTestsInPath("issues", { CypherTestSuite(it, neo4j).generateTests() })

@TestFactory
fun `new cypher tck tests`(): Stream<DynamicNode>? = Files
.list(Paths.get("src/test/resources/tck-test-files/cypher"))
.map {
DynamicContainer.dynamicContainer(
it.fileName.toString(),
it.toUri(),
CypherTestSuite("tck-test-files/cypher/${it.fileName}", neo4j).generateTests()
)
}
fun `new cypher tck tests`() = createTestsInPath("tck-test-files/cypher", { CypherTestSuite(it, neo4j).generateTests() })

companion object {
private val INTEGRATION_TESTS = System.getProperty("neo4j-graphql-java.integration-tests", "false") == "true"
Expand Down
22 changes: 22 additions & 0 deletions core/src/test/kotlin/org/neo4j/graphql/utils/TestUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package demo.org.neo4j.graphql.utils

import org.junit.jupiter.api.DynamicContainer
import org.junit.jupiter.api.DynamicNode
import java.nio.file.Files
import java.nio.file.Paths
import java.util.stream.Stream

object TestUtils {

fun createTestsInPath(path: String, factory: (testFile: String) -> Stream<DynamicNode>): Stream<DynamicNode>? = Files
.list(Paths.get("src/test/resources/$path"))
.sorted()
.map {
val tests = if (Files.isDirectory(it)) {
createTestsInPath("$path/${it.fileName}", factory)
} else {
factory("$path/${it.fileName}")
}
DynamicContainer.dynamicContainer(it.fileName.toString(), it.toUri(), tests)
}
}
Loading

0 comments on commit 329a7d7

Please sign in to comment.