Skip to content

Commit eabc147

Browse files
committed
Add support for the passThrough argument of the cypher directive.
The new argument `passThrough` of the `@cypher` directive allows the user to return an arbitrary result for the directive resolves #190
1 parent 02dd7a7 commit eabc147

File tree

7 files changed

+114
-9
lines changed

7 files changed

+114
-9
lines changed

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

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

1313
const val CYPHER = "cypher"
1414
const val CYPHER_STATEMENT = "statement"
15+
const val CYPHER_PASS_THROUGH = "passThrough"
1516

1617
const val PROPERTY = "property"
1718
const val PROPERTY_NAME = "name"

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.neo4j.cypherdsl.core.Node
77
import org.neo4j.cypherdsl.core.Relationship
88
import org.neo4j.cypherdsl.core.SymbolicName
99
import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER
10+
import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER_PASS_THROUGH
1011
import org.neo4j.graphql.DirectiveConstants.Companion.CYPHER_STATEMENT
1112
import org.neo4j.graphql.DirectiveConstants.Companion.DYNAMIC
1213
import org.neo4j.graphql.DirectiveConstants.Companion.DYNAMIC_PREFIX
@@ -200,7 +201,12 @@ fun <T> GraphQLDirective.getArgument(argumentName: String, defaultValue: T? = nu
200201
?: throw IllegalStateException("No default value for @${this.name}::$argumentName")
201202
}
202203

203-
fun GraphQLFieldDefinition.cypherDirective(): String? = getDirectiveArgument<String>(CYPHER, CYPHER_STATEMENT, null)
204+
fun GraphQLFieldDefinition.cypherDirective() :CypherDirective?= getDirective(CYPHER)?.let { CypherDirective(
205+
it.getMandatoryArgument(CYPHER_STATEMENT),
206+
it.getMandatoryArgument(CYPHER_PASS_THROUGH, false)
207+
) }
208+
209+
data class CypherDirective(val statement: String, val passThrough: Boolean)
204210

205211
fun Any.toJavaValue() = when (this) {
206212
is Value<*> -> this.toJavaValue()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import org.neo4j.graphql.*
1515
class CypherDirectiveHandler(
1616
private val type: GraphQLFieldsContainer?,
1717
private val isQuery: Boolean,
18-
private val cypherDirective: String,
18+
private val cypherDirective: CypherDirective,
1919
fieldDefinition: GraphQLFieldDefinition)
2020
: BaseDataFetcher(fieldDefinition) {
2121

@@ -44,7 +44,7 @@ class CypherDirectiveHandler(
4444
.with(org.neo4j.cypherdsl.core.Cypher.property(value, Functions.head(org.neo4j.cypherdsl.core.Cypher.call("keys").withArgs(value).asFunction())).`as`(variable))
4545
}
4646
val node = org.neo4j.cypherdsl.core.Cypher.anyNode(variable)
47-
val readingWithWhere = if (type != null) {
47+
val readingWithWhere = if (type != null && !cypherDirective.passThrough) {
4848
val projectionEntries = projectFields(node, field, type, env)
4949
query.returning(node.project(projectionEntries).`as`(field.aliasOrName()))
5050
} else {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ open class ProjectionBase {
214214
val isObjectField = fieldDefinition.type.inner() is GraphQLFieldsContainer
215215
if (cypherDirective != null) {
216216
val query = cypherDirective(field.contextualize(variable), fieldDefinition, field, cypherDirective, propertyContainer.requiredSymbolicName)
217-
projections += if (isObjectField) {
217+
projections += if (isObjectField && !cypherDirective.passThrough) {
218218
projectListComprehension(variable, field, fieldDefinition, env, query, variableSuffix)
219219
} else {
220220
query
@@ -275,13 +275,13 @@ open class ProjectionBase {
275275
return mapOf(*projections.toTypedArray())
276276
}
277277

278-
fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: String, thisValue: Any? = null): Expression {
278+
fun cypherDirective(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: CypherDirective, thisValue: Any? = null): Expression {
279279
val suffix = if (fieldDefinition.type.isList()) "Many" else "Single"
280280
val args = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective, thisValue)
281281
return call("apoc.cypher.runFirstColumn$suffix").withArgs(*args).asFunction()
282282
}
283283

284-
fun cypherDirectiveQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: String, thisValue: Any? = null): Array<Expression> {
284+
fun cypherDirectiveQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, cypherDirective: CypherDirective, thisValue: Any? = null): Array<Expression> {
285285
val args = mutableMapOf<String, Any?>()
286286
if (thisValue != null) args["this"] = thisValue
287287
field.arguments.forEach { args[it.name] = it.value }
@@ -290,7 +290,7 @@ open class ProjectionBase {
290290
.forEach { args[it.name] = it.defaultValue }
291291

292292
val argParams = args.map { (name, _) -> "$$name AS $name" }.joinNonEmpty(", ")
293-
val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective
293+
val query = (if (argParams.isEmpty()) "" else "WITH $argParams ") + cypherDirective.statement
294294
val argExpressions = args.flatMap { (name, value) -> listOf(name, if (name == "this") value else queryParameter(value, variable, name)) }
295295
return arrayOf(literalOf<String>(query), mapOf(*argExpressions.toTypedArray()))
296296
}
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
directive @relation(name:String, direction: RelationDirection = OUT, from: String = "from", to: String = "to") on FIELD_DEFINITION | OBJECT
2-
directive @cypher(statement:String) on FIELD_DEFINITION
2+
3+
directive @cypher(
4+
5+
# a cypher statement fields or top level queries and mutations. The current node is passed to the statement as `this`
6+
statement:String,
7+
8+
# if true, passes the sole responsibility for the nested query result for the field to your Cypher query.
9+
# You will have to provide all data/structure required by client queries.
10+
# Otherwise, we assume if you return object-types that you will return the appropriate nodes from your statement.
11+
passThrough: Boolean = false
12+
) on FIELD_DEFINITION
13+
314
directive @property(name:String) on FIELD_DEFINITION
415
directive @dynamic(prefix:String = "properties.") on FIELD_DEFINITION

core/src/test/resources/cypher-directive-tests.adoc

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,30 @@ type Person {
1111
name: String @cypher(statement:"RETURN this.name")
1212
age(mult:Int=13) : [Int] @cypher(statement:"RETURN this.age * mult as age")
1313
friends: [Person] @cypher(statement:"MATCH (this)-[:KNOWS]-(o) RETURN o")
14+
data: UserData @cypher(statement: "MATCH (this)-[:CREATED_MAP]->(m:Map) WITH collect({id: m.id, name: m.name}) AS mapsCreated, this RETURN {firstName: this.firstName, lastName: this.lastName, organization: this.organization, mapsCreated: mapsCreated}", passThrough:true)
1415
}
1516
type Query {
1617
person : [Person]
1718
p2: [Person] @cypher(statement:"MATCH (p:Person) RETURN p")
1819
p3(name:String): Person @cypher(statement:"MATCH (p:Person) WHERE p.name = name RETURN p LIMIT 1")
20+
getUser(userId: ID): UserData @cypher(statement: "MATCH (u:User{id: {userId}})-[:CREATED_MAP]->(m:Map) WITH collect({id: m.id, name: m.name}) AS mapsCreated, u RETURN {firstName: u.firstName, lastName: u.lastName, organization: u.organization, mapsCreated: mapsCreated}", passThrough:true)
1921
}
2022
type Mutation {
2123
createPerson(name:String): Person @cypher(statement:"CREATE (p:Person) SET p.name = name RETURN p")
2224
}
25+
26+
type UserData {
27+
firstName: String
28+
lastName: String
29+
organization: String
30+
mapsCreated: [MapsCreated]
31+
}
32+
33+
type MapsCreated {
34+
id: String
35+
name: String
36+
}
37+
2338
schema {
2439
query: Query
2540
mutation: Mutation
@@ -291,3 +306,73 @@ RETURN p3 {
291306
MATCH (person:Person) RETURN person { name:apoc.cypher.runFirstColumnSingle('WITH $this AS this RETURN this.name', { this:person }) } AS person
292307
----
293308

309+
=== pass through directives' result in query
310+
311+
.GraphQL-Query
312+
[source,graphql]
313+
----
314+
query queriesRootQuery {
315+
user: getUser(userId: "123") {
316+
firstName lastName organization
317+
mapsCreated { id }
318+
}
319+
}
320+
----
321+
322+
.Query variables
323+
[source,json,request=true]
324+
----
325+
{}
326+
----
327+
328+
.Cypher params
329+
[source,json]
330+
----
331+
{
332+
"userUserId" : "123"
333+
}
334+
----
335+
336+
.Cypher
337+
[source,cypher]
338+
----
339+
UNWIND apoc.cypher.runFirstColumnSingle('WITH $userId AS userId MATCH (u:User{id: {userId}})-[:CREATED_MAP]->(m:Map) WITH collect({id: m.id, name: m.name}) AS mapsCreated, u RETURN {firstName: u.firstName, lastName: u.lastName, organization: u.organization, mapsCreated: mapsCreated}', {
340+
userId: $userUserId
341+
}) AS user
342+
RETURN user AS user
343+
----
344+
345+
346+
=== pass through directives result in field
347+
348+
.GraphQL-Query
349+
[source,graphql]
350+
----
351+
query queriesRootQuery {
352+
person { id, data }
353+
}
354+
----
355+
356+
.Query variables
357+
[source,json,request=true]
358+
----
359+
{}
360+
----
361+
362+
.Cypher params
363+
[source,json]
364+
----
365+
{}
366+
----
367+
368+
.Cypher
369+
[source,cypher]
370+
----
371+
MATCH (person:Person)
372+
RETURN person {
373+
.id,
374+
data: apoc.cypher.runFirstColumnSingle('WITH $this AS this MATCH (this)-[:CREATED_MAP]->(m:Map) WITH collect({id: m.id, name: m.name}) AS mapsCreated, this RETURN {firstName: this.firstName, lastName: this.lastName, organization: this.organization, mapsCreated: mapsCreated}', {
375+
this: person
376+
})
377+
} AS person
378+
----

readme.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,10 @@ This example doesn't handle introspection queries, but the one in the test direc
246246
* inline and named fragments
247247
* auto-generate query fields for all objects
248248
* @cypher directive for fields to compute field values, support arguments
249-
* auto-generate mutation fields for all objects to create, update, delete
250249
* @cypher directive for top level queries and mutations, supports arguments
250+
* @cypher directives can have a `passThrough:true` argument, that gives sole responsibility for the nested query result for this field to your Cypher query. You will have to provide all data/structure required by client queries.
251+
Otherwise, we assume if you return object-types that you will return the appropriate nodes from your statement.
252+
* auto-generate mutation fields for all objects to create, update, delete
251253
* date(time)
252254
* interfaces
253255
* complex filter parameters, with optional query optimization strategy

0 commit comments

Comments
 (0)