Skip to content

Commit e34613c

Browse files
committed
Use graphql infrastructure to parse requests
Using the graphql infrastructure for parsing the request ensures that: * the request is compliant with the schema * internally we can rely on `DataFetchingEnvironment` being correctly initialized
1 parent 0f81665 commit e34613c

17 files changed

+117
-122
lines changed

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,35 @@ abstract class AugmentationHandler(
5252
}
5353
return FieldDefinition.newFieldDefinition()
5454
.name("$prefix${resultType.name}")
55-
.inputValueDefinitions(getInputValueDefinitions(scalarFields, forceOptionalProvider))
55+
.inputValueDefinitions(getInputValueDefinitions(scalarFields, false, forceOptionalProvider))
5656
.type(type)
5757
}
5858

5959
protected fun getInputValueDefinitions(
6060
relevantFields: List<FieldDefinition>,
61+
addFieldOperations: Boolean,
6162
forceOptionalProvider: (field: FieldDefinition) -> Boolean): List<InputValueDefinition> {
62-
return relevantFields.map { field ->
63+
return relevantFields.flatMap { field ->
6364
var type = getInputType(field.type)
6465
type = if (forceOptionalProvider(field)) {
6566
(type as? NonNullType)?.type ?: type
6667
} else {
6768
type
6869
}
69-
input(field.name, type)
70+
if (addFieldOperations && !field.isNativeId()) {
71+
val typeDefinition = field.type.resolve()
72+
?: throw IllegalArgumentException("type ${field.type.name()} cannot be resolved")
73+
FieldOperator.forType(typeDefinition, field.type.inner().isNeo4jType())
74+
.map { op ->
75+
val wrappedType: Type<*> = when {
76+
op.list -> ListType(NonNullType(TypeName(type.name())))
77+
else -> type
78+
}
79+
input(op.fieldName(field.name), wrappedType)
80+
}
81+
} else {
82+
listOf(input(field.name, type))
83+
}
7084
}
7185
}
7286

@@ -280,6 +294,7 @@ abstract class AugmentationHandler(
280294
fun ImplementingTypeDefinition<*>.getFieldDefinition(name: String) = this.fieldDefinitions
281295
.filterNot { it.isIgnored() }
282296
.find { it.name == name }
297+
283298
fun ImplementingTypeDefinition<*>.getIdField() = this.fieldDefinitions
284299
.filterNot { it.isIgnored() }
285300
.find { it.type.inner().isID() }
@@ -289,7 +304,7 @@ abstract class AugmentationHandler(
289304
fun Type<*>.isNeo4jType(): Boolean = name()
290305
?.takeIf {
291306
!ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it)
292-
&& it.startsWith("_Neo4j") // TODO remove this check by refactoring neo4j input types
307+
&& it.startsWith("_Neo4j") // TODO remove this check by refactoring neo4j input types
293308
}
294309
?.let { neo4jTypeDefinitionRegistry.getUnwrappedType(it) } != null
295310

@@ -299,6 +314,7 @@ abstract class AugmentationHandler(
299314
fun FieldDefinition.isNativeId(): Boolean = name == ProjectionBase.NATIVE_ID
300315
fun FieldDefinition.dynamicPrefix(): String? =
301316
getDirectiveArgument(DirectiveConstants.DYNAMIC, DirectiveConstants.DYNAMIC_PREFIX, null)
317+
302318
fun FieldDefinition.isRelationship(): Boolean =
303319
!type.inner().isNeo4jType() && type.resolve() is ImplementingTypeDefinition<*>
304320

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ import graphql.schema.GraphQLType
44

55
data class Cypher @JvmOverloads constructor(val query: String, val params: Map<String, Any?> = emptyMap(), var type: GraphQLType? = null, val variable: String) {
66
fun with(p: Map<String, Any?>) = this.copy(params = this.params + p)
7-
fun escapedQuery() = query.replace("\"", "\\\"").replace("'", "\\'")
87
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ object DynamicProperties {
2929

3030
@Throws(CoercingParseLiteralException::class)
3131
private fun parse(input: Any, variables: Map<String, Any>): Any? {
32+
return when (input) {
33+
!is Value<*> -> throw CoercingParseLiteralException("Expected AST type 'StringValue' but was '${input::class.java.simpleName}'.")
34+
is NullValue -> null
35+
is ObjectValue -> input.objectFields.map { it.name to parseNested(it.value, variables) }.toMap()
36+
else -> Assert.assertShouldNeverHappen("Only maps structures are expected")
37+
}
38+
}
39+
40+
@Throws(CoercingParseLiteralException::class)
41+
private fun parseNested(input: Any, variables: Map<String, Any>): Any? {
3242
return when (input) {
3343
!is Value<*> -> throw CoercingParseLiteralException("Expected AST type 'StringValue' but was '${input::class.java.simpleName}'.")
3444
is NullValue -> null
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.neo4j.graphql
2+
3+
import graphql.GraphQLError
4+
5+
class InvalidQueryException(@Suppress("MemberVisibilityCanBePrivate") val error: GraphQLError) : RuntimeException(error.message)
Lines changed: 23 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,37 @@
11
package org.neo4j.graphql
22

3-
import graphql.execution.MergedField
4-
import graphql.language.Document
5-
import graphql.language.Field
6-
import graphql.language.FragmentDefinition
7-
import graphql.language.OperationDefinition
8-
import graphql.language.OperationDefinition.Operation.MUTATION
9-
import graphql.language.OperationDefinition.Operation.QUERY
10-
import graphql.parser.InvalidSyntaxException
11-
import graphql.parser.Parser
12-
import graphql.schema.DataFetchingEnvironmentImpl.newDataFetchingEnvironment
13-
import graphql.schema.GraphQLFieldDefinition
14-
import graphql.schema.GraphQLObjectType
3+
import graphql.ExceptionWhileDataFetching
4+
import graphql.ExecutionInput
5+
import graphql.GraphQL
6+
import graphql.InvalidSyntaxError
157
import graphql.schema.GraphQLSchema
16-
import java.math.BigDecimal
17-
import java.math.BigInteger
8+
import graphql.validation.ValidationError
189

1910
class Translator(val schema: GraphQLSchema) {
2011

12+
class CypherHolder(var cypher: Cypher?)
13+
14+
private val gql: GraphQL = GraphQL.newGraphQL(schema).build()
15+
2116
@JvmOverloads
2217
@Throws(OptimizedQueryException::class)
2318
fun translate(query: String, params: Map<String, Any?> = emptyMap(), ctx: QueryContext = QueryContext()): List<Cypher> {
24-
val ast = parse(query) // todo preparsedDocumentProvider
25-
val fragments = ast.definitions.filterIsInstance<FragmentDefinition>().associateBy { it.name }
26-
return ast.definitions.filterIsInstance<OperationDefinition>()
27-
.filter { it.operation == QUERY || it.operation == MUTATION } // todo variableDefinitions, directives, name
28-
.flatMap { operationDefinition ->
29-
operationDefinition.selectionSet.selections
30-
.filterIsInstance<Field>() // FragmentSpread, InlineFragment
31-
.map { field ->
32-
val cypher = toQuery(operationDefinition.operation, field, fragments, params, ctx)
33-
val resolvedParams = cypher.params.mapValues { toBoltValue(it.value) }
34-
cypher.with(resolvedParams)
35-
}
36-
}
37-
}
38-
39-
private fun toBoltValue(value: Any?) = when (value) {
40-
is BigInteger -> value.longValueExact()
41-
is BigDecimal -> value.toDouble()
42-
else -> value
43-
}
44-
45-
private fun toQuery(op: OperationDefinition.Operation,
46-
field: Field,
47-
fragments: Map<String, FragmentDefinition?>,
48-
variables: Map<String, Any?>,
49-
ctx: QueryContext = QueryContext()
50-
): Cypher {
51-
val name = field.name
52-
val operationObjectType: GraphQLObjectType
53-
val fieldDefinition: GraphQLFieldDefinition
54-
when (op) {
55-
QUERY -> {
56-
operationObjectType = schema.queryType
57-
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
58-
?: throw IllegalArgumentException("Unknown Query $name available queries: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
59-
}
60-
MUTATION -> {
61-
operationObjectType = schema.mutationType
62-
fieldDefinition = operationObjectType.getRelevantFieldDefinition(name)
63-
?: throw IllegalArgumentException("Unknown Mutation $name available mutations: " + (operationObjectType.getRelevantFieldDefinitions()).joinToString { it.name })
19+
val cypherHolder = CypherHolder(null)
20+
val executionInput = ExecutionInput.newExecutionInput()
21+
.query(query)
22+
.variables(params)
23+
.context(ctx)
24+
.localContext(cypherHolder)
25+
.build()
26+
val result = gql.execute(executionInput)
27+
result.errors?.forEach {
28+
when (it) {
29+
is ExceptionWhileDataFetching -> throw it.exception
30+
is ValidationError -> throw InvalidQueryException(it)
31+
is InvalidSyntaxError -> throw InvalidQueryException(it)
6432
}
65-
else -> throw IllegalArgumentException("$op is not supported")
6633
}
67-
val dataFetcher = schema.codeRegistry.getDataFetcher(operationObjectType, fieldDefinition)
68-
?: throw IllegalArgumentException("no data fetcher found for ${op.name.toLowerCase()} $name")
6934

70-
71-
return dataFetcher.get(newDataFetchingEnvironment()
72-
.mergedField(MergedField.newMergedField(field).build())
73-
.parentType(operationObjectType)
74-
.graphQLSchema(schema)
75-
.fragmentsByName(fragments)
76-
.context(ctx)
77-
.localContext(ctx)
78-
.fieldDefinition(fieldDefinition)
79-
.variables(variables)
80-
.build()) as? Cypher
81-
?: throw java.lang.IllegalStateException("not supported")
82-
}
83-
84-
private fun parse(query: String): Document {
85-
try {
86-
val parser = Parser()
87-
return parser.parseDocument(query)
88-
} catch (e: InvalidSyntaxException) {
89-
// todo proper structured error
90-
throw e
91-
}
35+
return listOf(requireNotNull(cypherHolder.cypher))
9236
}
9337
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ class AugmentFieldHandler(
5656
fieldBuilder.inputValueDefinition(input(ProjectionBase.ORDER_BY, orderType))
5757
}
5858
}
59+
if (!schemaConfig.useWhereFilter && schemaConfig.query.enabled && !schemaConfig.query.exclude.contains(fieldType.name)) {
60+
// legacy support
61+
val relevantFields = fieldType
62+
.getScalarFields()
63+
.filter { scalarField -> field.inputValueDefinitions.find { it.name == scalarField.name } == null }
64+
.filter { it.dynamicPrefix() == null } // TODO currently we do not support filtering on dynamic properties
65+
getInputValueDefinitions(relevantFields, true, { true }).forEach {
66+
fieldBuilder.inputValueDefinition(it)
67+
}
68+
}
5969
}
6070

6171
val filterFieldName = if (schemaConfig.useWhereFilter) ProjectionBase.WHERE else ProjectionBase.FILTER

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import org.neo4j.cypherdsl.core.renderer.Configuration
1111
import org.neo4j.cypherdsl.core.renderer.Renderer
1212
import org.neo4j.graphql.Cypher
1313
import org.neo4j.graphql.SchemaConfig
14+
import org.neo4j.graphql.Translator
1415
import org.neo4j.graphql.aliasOrName
1516
import org.neo4j.graphql.handler.projection.ProjectionBase
1617

1718
/**
18-
* The is a base class for the implementation of graphql data fetcher used in this project
19+
* This is a base class for the implementation of graphql data fetcher used in this project
1920
*/
2021
abstract class BaseDataFetcher(schemaConfig: SchemaConfig) : ProjectionBase(schemaConfig), DataFetcher<Cypher> {
2122

@@ -38,8 +39,10 @@ abstract class BaseDataFetcher(schemaConfig: SchemaConfig) : ProjectionBase(sche
3839
val params = statement.parameters.mapValues { (_, value) ->
3940
(value as? VariableReference)?.let { env.variables[it.name] } ?: value
4041
}
41-
4242
return Cypher(query, params, env.fieldDefinition.type, variable = field.aliasOrName())
43+
.also {
44+
(env.getLocalContext() as? Translator.CypherHolder)?.apply { this.cypher = it }
45+
}
4346
}
4447

4548
/**

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ class MergeOrUpdateHandler private constructor(private val merge: Boolean, schem
3333
}
3434

3535
val relevantFields = type.getScalarFields()
36-
val mergeField = buildFieldDefinition("merge", type, relevantFields, nullableResult = false)
36+
val idField = type.getIdField()
37+
?: throw IllegalStateException("Cannot resolve id field for type ${type.name}")
38+
39+
val mergeField = buildFieldDefinition("merge", type, relevantFields, nullableResult = false, forceOptionalProvider = { it != idField })
3740
.build()
3841
addMutationField(mergeField)
3942

40-
val updateField = buildFieldDefinition("update", type, relevantFields, nullableResult = true)
43+
val updateField = buildFieldDefinition("update", type, relevantFields, nullableResult = true, forceOptionalProvider = { it != idField })
4144
.build()
4245
addMutationField(updateField)
4346
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class QueryHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFet
3232
val arguments = if (schemaConfig.useWhereFilter) {
3333
listOf(input(WHERE, TypeName(filterTypeName)))
3434
} else {
35-
getInputValueDefinitions(relevantFields, { true }) +
35+
getInputValueDefinitions(relevantFields, true, { true }) +
3636
input(FILTER, TypeName(filterTypeName))
3737
}
3838

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ open class ProjectionBase(
268268
}
269269
if (nodeType is GraphQLInterfaceType
270270
&& !hasTypeName
271-
&& (env.getLocalContext() as? QueryContext)?.queryTypeOfInterfaces == true
271+
&& (env.getContext() as? QueryContext)?.queryTypeOfInterfaces == true
272272
) {
273273
// for interfaces the typename is required to determine the correct implementation
274274
val (pro, sub) = projectField(propertyContainer, variable, Field(TYPE_NAME), nodeType, env, variableSuffix)

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.neo4j.graphql
22

3-
import graphql.parser.InvalidSyntaxException
43
import org.junit.jupiter.api.Assertions
54
import org.junit.jupiter.api.DynamicNode
65
import org.junit.jupiter.api.DynamicTest
@@ -19,7 +18,7 @@ class TranslatorExceptionTests : AsciiDocTestSuite("translator-tests1.adoc") {
1918
val translator = Translator(SchemaBuilder.buildSchema(schema))
2019
return listOf(
2120
DynamicTest.dynamicTest("unknownType") {
22-
Assertions.assertThrows(IllegalArgumentException::class.java) {
21+
Assertions.assertThrows(InvalidQueryException::class.java) {
2322
translator.translate("""
2423
{
2524
company {
@@ -30,7 +29,7 @@ class TranslatorExceptionTests : AsciiDocTestSuite("translator-tests1.adoc") {
3029
}
3130
},
3231
DynamicTest.dynamicTest("mutation") {
33-
Assertions.assertThrows(InvalidSyntaxException::class.java) {
32+
Assertions.assertThrows(InvalidQueryException::class.java) {
3433
translator.translate("""
3534
{
3635
createPerson()

0 commit comments

Comments
 (0)