Skip to content

Commit 329a7d7

Browse files
authored
Add ability to ignore fields (via @ignore) so they can be handled by custom data fetcher (#226)
1 parent c0faaf1 commit 329a7d7

File tree

20 files changed

+502
-61
lines changed

20 files changed

+502
-61
lines changed

core/src/main/kotlin/org/neo4j/graphql/AugmentationHandler.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ abstract class AugmentationHandler(
125125
.build())
126126
}
127127
type.fieldDefinitions
128+
.filterNot { it.isIgnored() }
128129
.filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
129130
.forEach { field ->
130131
val typeDefinition = field.type.resolve()
@@ -272,11 +273,16 @@ abstract class AugmentationHandler(
272273
fun ImplementingTypeDefinition<*>.relationship(): RelationshipInfo<ImplementingTypeDefinition<*>>? = RelationshipInfo.create(this, neo4jTypeDefinitionRegistry)
273274

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

278-
fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions.find { it.name == name }
279-
fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions.find { it.type.inner().isID() }
280+
fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions
281+
.filterNot { it.isIgnored() }
282+
.find { it.name == name }
283+
fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions
284+
.filterNot { it.isIgnored() }
285+
.find { it.type.inner().isID() }
280286

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

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

302307
fun DirectivesContainer<*>.cypherDirective(): CypherDirective? = if (hasDirective(DirectiveConstants.CYPHER)) {

core/src/main/kotlin/org/neo4j/graphql/DirectiveConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class DirectiveConstants {
44

55
companion object {
66

7+
const val IGNORE = "ignore"
78
const val RELATION = "relation"
89
const val RELATION_NAME = "name"
910
const val RELATION_DIRECTION = "direction"

core/src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fun GraphQLFieldDefinition.isRelationship() = !type.isNeo4jType() && this.type.i
5353

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

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

185185
fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
186186
fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
187-
fun GraphQLFieldsContainer.getIdField() = this.fieldDefinitions.find { it.isID() }
187+
fun GraphQLFieldDefinition.isIgnored() = getDirective(DirectiveConstants.IGNORE) != null
188+
fun FieldDefinition.isIgnored(): Boolean = hasDirective(DirectiveConstants.IGNORE)
189+
190+
fun GraphQLFieldsContainer.getIdField() = this.getRelevantFieldDefinitions().find { it.isID() }
191+
192+
/**
193+
* Returns the field definitions which are not ignored
194+
*/
195+
fun GraphQLFieldsContainer.getRelevantFieldDefinitions() = this.fieldDefinitions.filterNot { it.isIgnored() }
196+
197+
/**
198+
* Returns the field definition if it is not ignored
199+
*/
200+
fun GraphQLFieldsContainer.getRelevantFieldDefinition(name: String?) = this.getFieldDefinition(name)?.takeIf { !it.isIgnored() }
201+
188202

189203
fun InputObjectTypeDefinition.Builder.addFilterField(fieldName: String, isList: Boolean, filterType: String, description: Description? = null) {
190204
val wrappedType: Type<*> = when {

core/src/main/kotlin/org/neo4j/graphql/SchemaBuilder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ class SchemaBuilder(
187187
typeDefinitionRegistry.getType(parentType)?.unwrap()
188188
?.let { it as? ObjectTypeDefinition }
189189
?.fieldDefinitions
190+
?.filterNot { it.isIgnored() }
190191
?.forEach { field ->
191192
handler.forEach { h ->
192193
h.createDataFetcher(operationType, field)?.let { dataFetcher ->

core/src/main/kotlin/org/neo4j/graphql/Translator.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ class Translator(val schema: GraphQLSchema) {
5454
when (op) {
5555
QUERY -> {
5656
operationObjectType = schema.queryType
57-
fieldDefinition = operationObjectType.getFieldDefinition(name)
58-
?: throw IllegalArgumentException("Unknown Query $name available queries: " + (operationObjectType.fieldDefinitions).joinToString { it.name })
57+
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
58+
?: throw IllegalArgumentException("Unknown Query $name available queries: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
5959
}
6060
MUTATION -> {
6161
operationObjectType = schema.mutationType
62-
fieldDefinition = operationObjectType.getFieldDefinition(name)
63-
?: throw IllegalArgumentException("Unknown Mutation $name available mutations: " + (operationObjectType.fieldDefinitions).joinToString { it.name })
62+
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
63+
?: throw IllegalArgumentException("Unknown Mutation $name available mutations: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
6464
}
6565
else -> throw IllegalArgumentException("$op is not supported")
6666
}

core/src/main/kotlin/org/neo4j/graphql/handler/AugmentFieldHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class AugmentFieldHandler(
2929
}
3030

3131
private fun augmentRelation(fieldBuilder: FieldDefinition.Builder, field: FieldDefinition) {
32-
if (!field.isRelationship() || !field.type.isList()) {
32+
if (!field.isRelationship() || !field.type.isList() || field.isIgnored()) {
3333
return
3434
}
3535

core/src/main/kotlin/org/neo4j/graphql/handler/BaseDataFetcherForContainer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ abstract class BaseDataFetcherForContainer(schemaConfig: SchemaConfig) : BaseDat
3030
defaultFields[arg.name] = arg.defaultValue
3131
}
3232
}
33-
.mapNotNull { type.getFieldDefinition(it.name) }
33+
.mapNotNull { type.getRelevantFieldDefinition(it.name) }
3434
.forEach { field ->
3535
val dynamicPrefix = field.dynamicPrefix()
3636
propertyFields[field.name] = when {

core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ class QueryHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFet
8989
return true
9090
}
9191

92-
private fun hasRelationships(type: ImplementingTypeDefinition<*>): Boolean = type.fieldDefinitions.any { it.isRelationship() }
92+
private fun hasRelationships(type: ImplementingTypeDefinition<*>): Boolean = type.fieldDefinitions
93+
.filterNot { it.isIgnored() }
94+
.any { it.isRelationship() }
9395

9496
private fun getRelevantFields(type: ImplementingTypeDefinition<*>): List<FieldDefinition> {
9597
return type

core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,9 @@ open class ProjectionBase(
238238

239239
private fun projectField(propertyContainer: PropertyContainer, variable: SymbolicName, field: Field, type: GraphQLFieldsContainer, env: DataFetchingEnvironment, variableSuffix: String?, propertiesToSkipDeepProjection: Set<String> = emptySet()): List<Any> {
240240
val projections = mutableListOf<Any>()
241-
projections += field.aliasOrName()
241+
242242
if (field.name == TYPE_NAME) {
243+
projections += field.aliasOrName()
243244
if (type.isRelationType()) {
244245
projections += literalOf<Any>(type.name)
245246
} else {
@@ -250,8 +251,13 @@ open class ProjectionBase(
250251
}
251252
return projections
252253
}
254+
253255
val fieldDefinition = type.getFieldDefinition(field.name)
254256
?: throw IllegalStateException("No field ${field.name} in ${type.name}")
257+
if (fieldDefinition.isIgnored()) {
258+
return projections
259+
}
260+
projections += field.aliasOrName()
255261
val cypherDirective = fieldDefinition.cypherDirective()
256262
val isObjectField = fieldDefinition.type.inner() is GraphQLFieldsContainer
257263
if (cypherDirective != null) {
@@ -379,8 +385,8 @@ open class ProjectionBase(
379385
parent: GraphQLFieldsContainer,
380386
relDirectiveField: RelationshipInfo<GraphQLFieldsContainer>?
381387
): RelationshipInfo<GraphQLFieldsContainer> {
382-
val startField = fieldObjectType.getFieldDefinition(relInfo0.startField)!!
383-
val endField = fieldObjectType.getFieldDefinition(relInfo0.endField)!!
388+
val startField = fieldObjectType.getRelevantFieldDefinition(relInfo0.startField)!!
389+
val endField = fieldObjectType.getRelevantFieldDefinition(relInfo0.endField)!!
384390
val startFieldTypeName = startField.type.innerName()
385391
val inverse = startFieldTypeName != parent.name
386392
|| startFieldTypeName == endField.type.innerName()
@@ -418,7 +424,7 @@ open class ProjectionBase(
418424
relInfo.endField -> Triple(anyNode(), node, node)
419425
else -> throw IllegalArgumentException("type ${parent.name} does not have a matching field with name ${fieldDefinition.name}")
420426
}
421-
val rel = relInfo.createRelation(start, end, false,variable)
427+
val rel = relInfo.createRelation(start, end, false, variable)
422428
return head(CypherDSL.listBasedOn(rel).returning(target.project(projectFields(target, field, fieldDefinition.type as GraphQLFieldsContainer, env))))
423429
}
424430

@@ -442,7 +448,7 @@ open class ProjectionBase(
442448

443449
val (endNodePattern, variableSuffix) = when {
444450
isRelFromType -> {
445-
val label = nodeType.getFieldDefinition(relInfo.endField)!!.type.innerName()
451+
val label = nodeType.getRelevantFieldDefinition(relInfo.endField)!!.type.innerName()
446452
node(label).named("$childVariable${relInfo.endField.capitalize()}") to relInfo.endField
447453
}
448454
else -> node(nodeType.name).named(childVariableName) to null
@@ -451,7 +457,7 @@ open class ProjectionBase(
451457
val skipLimit = SkipLimit(childVariable, field.arguments, fieldDefinition)
452458
val orderBy = getOrderByArgs(field.arguments, fieldDefinition, env.variables)
453459
val sortByNeo4jTypeFields = orderBy
454-
.filter { (property, _) -> nodeType.getFieldDefinition(property)?.isNeo4jType() == true }
460+
.filter { (property, _) -> nodeType.getRelevantFieldDefinition(property)?.isNeo4jType() == true }
455461
.map { (property, _) -> property }
456462
.toSet()
457463

core/src/main/kotlin/org/neo4j/graphql/handler/relation/BaseRelationHandler.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ abstract class BaseRelationHandler(val prefix: String, schemaConfig: SchemaConfi
7474
if (!targetField.hasDirective(DirectiveConstants.RELATION)) {
7575
return false
7676
}
77+
if (targetField.isIgnored()) {
78+
return false
79+
}
7780
if (type.getIdField() == null) {
7881
return false
7982
}
@@ -156,7 +159,7 @@ abstract class BaseRelationHandler(val prefix: String, schemaConfig: SchemaConfi
156159
.removePrefix(p)
157160
.decapitalize()
158161
.let {
159-
type.getFieldDefinition(it) ?: throw IllegalStateException("Cannot find field $it on type ${type.name}")
162+
type.getRelevantFieldDefinition(it) ?: throw IllegalStateException("Cannot find field $it on type ${type.name}")
160163
}
161164

162165

core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationHandler.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class CreateRelationHandler private constructor(schemaConfig: SchemaConfig) : Ba
4545

4646
relationType
4747
?.fieldDefinitions
48+
?.filterNot { it.isIgnored() }
4849
?.filter { it.type.inner().isScalar() && !it.type.inner().isID() }
4950
?.forEach { builder.inputValueDefinition(input(it.name, it.type)) }
5051

core/src/main/kotlin/org/neo4j/graphql/handler/relation/CreateRelationTypeHandler.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,22 @@ class CreateRelationTypeHandler private constructor(schemaConfig: SchemaConfig)
104104

105105
val relType = relFieldDefinition.type.inner().resolve() as? ImplementingTypeDefinition<*>
106106
?: throw IllegalArgumentException("type ${relFieldDefinition.type.name()} not found")
107-
return relType.fieldDefinitions.filter { it.type.inner().isID() }
107+
return relType.fieldDefinitions
108+
.filterNot { it.isIgnored() }
109+
.filter { it.type.inner().isID() }
108110
.map { RelatedField(normalizeFieldName(relFieldName, it.name), it) }
109111
.firstOrNull()
110112
}
111113

112114
}
113115

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

118120
val relType = relFieldDefinition.type.inner() as? GraphQLImplementingType
119121
?: throw IllegalArgumentException("type ${relFieldDefinition.type.name()} not found")
120-
return relType.fieldDefinitions.filter { it.isID() }
122+
return relType.getRelevantFieldDefinitions().filter { it.isID() }
121123
.map { RelatedField(normalizeFieldName(relFieldName, it.name), it, relType) }
122124
.firstOrNull()
123125
?: throw IllegalStateException("Cannot find id field for type ${info.typeName}")

core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ object QueryParser {
145145
// find all matching fields
146146
val fieldPredicates = mutableListOf<FieldPredicate>()
147147
val relationPredicates = mutableListOf<RelationPredicate>()
148-
for (definedField in type.fieldDefinitions) {
148+
for (definedField in type.getRelevantFieldDefinitions()) {
149149
if (definedField.isRelationship()) {
150150
RelationOperator.values()
151151
.map { it to definedField.name + it.suffix }

core/src/main/resources/lib_directives.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ directive @cypher(
1313

1414
directive @property(name:String) on FIELD_DEFINITION
1515
directive @dynamic(prefix:String = "properties.") on FIELD_DEFINITION
16+
directive @ignore on FIELD_DEFINITION
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package org.neo4j.graphql
22

3-
import org.junit.jupiter.api.DynamicContainer
4-
import org.junit.jupiter.api.DynamicNode
3+
import demo.org.neo4j.graphql.utils.TestUtils.createTestsInPath
54
import org.junit.jupiter.api.TestFactory
65
import org.neo4j.graphql.utils.GraphQLSchemaTestSuite
7-
import java.nio.file.Files
8-
import java.nio.file.Paths
9-
import java.util.stream.Stream
106

117
class AugmentationTests {
128

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

1915
@TestFactory
20-
fun `schema augmentation tests`(): Stream<DynamicNode>? = Files
21-
.list(Paths.get("src/test/resources/tck-test-files/schema"))
22-
.map {
23-
DynamicContainer.dynamicContainer(
24-
it.fileName.toString(),
25-
it.toUri(),
26-
GraphQLSchemaTestSuite("tck-test-files/schema/${it.fileName}").generateTests()
27-
)
28-
}
16+
fun `schema augmentation tests`() = createTestsInPath("tck-test-files/schema", { GraphQLSchemaTestSuite(it).generateTests() })
2917
}

core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package org.neo4j.graphql
22

33
import apoc.coll.Coll
44
import apoc.cypher.CypherFunctions
5-
import org.junit.jupiter.api.*
5+
import demo.org.neo4j.graphql.utils.TestUtils.createTestsInPath
6+
import org.junit.jupiter.api.AfterAll
7+
import org.junit.jupiter.api.BeforeAll
8+
import org.junit.jupiter.api.TestFactory
9+
import org.junit.jupiter.api.TestInstance
610
import org.neo4j.graphql.utils.CypherTestSuite
711
import org.neo4j.harness.Neo4j
812
import org.neo4j.harness.Neo4jBuilders
9-
import java.nio.file.Files
1013
import java.nio.file.Path
11-
import java.nio.file.Paths
12-
import java.util.stream.Stream
1314

1415
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
1516
class CypherTests {
@@ -71,27 +72,10 @@ class CypherTests {
7172
fun `custom-fields`() = CypherTestSuite("custom-fields.adoc", neo4j).generateTests()
7273

7374
@TestFactory
74-
fun `test issues`(): Stream<DynamicNode>? = Files
75-
.list(Paths.get("src/test/resources/issues"))
76-
.map {
77-
DynamicContainer.dynamicContainer(
78-
it.fileName.toString(),
79-
it.toUri(),
80-
CypherTestSuite("issues/${it.fileName}", neo4j).generateTests()
81-
)
82-
}
83-
75+
fun `test issues`() = createTestsInPath("issues", { CypherTestSuite(it, neo4j).generateTests() })
8476

8577
@TestFactory
86-
fun `new cypher tck tests`(): Stream<DynamicNode>? = Files
87-
.list(Paths.get("src/test/resources/tck-test-files/cypher"))
88-
.map {
89-
DynamicContainer.dynamicContainer(
90-
it.fileName.toString(),
91-
it.toUri(),
92-
CypherTestSuite("tck-test-files/cypher/${it.fileName}", neo4j).generateTests()
93-
)
94-
}
78+
fun `new cypher tck tests`() = createTestsInPath("tck-test-files/cypher", { CypherTestSuite(it, neo4j).generateTests() })
9579

9680
companion object {
9781
private val INTEGRATION_TESTS = System.getProperty("neo4j-graphql-java.integration-tests", "false") == "true"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package demo.org.neo4j.graphql.utils
2+
3+
import org.junit.jupiter.api.DynamicContainer
4+
import org.junit.jupiter.api.DynamicNode
5+
import java.nio.file.Files
6+
import java.nio.file.Paths
7+
import java.util.stream.Stream
8+
9+
object TestUtils {
10+
11+
fun createTestsInPath(path: String, factory: (testFile: String) -> Stream<DynamicNode>): Stream<DynamicNode>? = Files
12+
.list(Paths.get("src/test/resources/$path"))
13+
.sorted()
14+
.map {
15+
val tests = if (Files.isDirectory(it)) {
16+
createTestsInPath("$path/${it.fileName}", factory)
17+
} else {
18+
factory("$path/${it.fileName}")
19+
}
20+
DynamicContainer.dynamicContainer(it.fileName.toString(), it.toUri(), tests)
21+
}
22+
}

0 commit comments

Comments
 (0)