Skip to content
Pure JVM translation for GraphQL queries and mutations to Neo4j's Cypher
Branch: master
Clone or download
jexp added @cypher directive for queries
single/many call depending on cardinality of field
support field arguments as parameter
supports further drill-down in graphql query
Latest commit 07a66af Mar 8, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docs
src
.gitignore
pom.xml
readme.adoc

readme.adoc

JVM Library to translate GraphQL queries and mutations to Neo4j’s Cypher

This is an early stage alpha implementation written in Kotlin.

How does it work

This library

  1. parses a GraphQL schema and

  2. uses the information of the annotated schema to translate GraphQL queries and parameters into Cypher queries and parameters.

Those Cypher queries can then executed, e.g via the Neo4j-Java-Driver (or other JVM drivers) against the graph database and the results can be returned directly to the caller.

The request, result and error handling is not part of this library, but we provide demo programs on how to use it in different languages.

Note
All the supported features are listed below, detailed docs will be added.

FAQ

How does this relate to the other neo4j graphql libraries?

Similar to neo4j-graphql-js this library focusses on query translation, just for the JVM instead of Node.js. It does not provide a server or other facilities but is meant to be used as a dependency included for a single purpose.

If this library is feature complete we plan to replace the code in the current Neo4j server plugin neo4j-graphql with a single call to this library. The server plugin would still handle request-response and error-handling, and perhaps some schema management but be slimmed down to a tiny piece.

This library uses GraphQL Java under the hood for parsing of schema and queries, and managing the GraphQL state and context. But not for nested field resolvers or data fetching.

If you wanted to you could combine graphql-java resolvers with this library to have a part of your schema handled by Neo4j.

Usage

You can use the library as dependency: org.neo4j:neo4j-graphql-java:1.0.0-M01 in any JVM program.

The basic usage should be:

val schema =
        """
        type Person {
            name: String
            age: Int
        }
        type Query {
            person : [Person]
            personByName(name:String) : Person
        }"""

val query = """ { p:personByName(name:"Joe") { age } } """

val schema = SchemaBuilder.buildSchema(idl)
val (cypher, params) = Translator(schema).translate(query, params)

cypher == "MATCH (p:Person) WHERE p.name = $pName RETURN p {.age} as p"

Demo

Here is a minimalistic example in Groovy using the Neo4j-Java driver and Spark-Java as webserver. It is running against a Neo4j instance at bolt://localhost (username: neo4j, password: password) containing the :play movies graph.

// Simplistic GraphQL Server using SparkJava
@Grapes([
  @Grab('com.sparkjava:spark-core:2.7.2'),
  @Grab('org.neo4j.driver:neo4j-java-driver:1.7.2'),
  @Grab('org.neo4j:neo4j-graphql-java:1.0.0-M01'),
  @Grab('com.google.code.gson:gson:2.8.5')
])

import spark.*
import static spark.Spark.*
import com.google.gson.Gson
import org.neo4j.graphql.*
import org.neo4j.driver.v1.*

schema = """
type Person {
  name: String
  born: Int
  actedIn: [Movie] @relation(name:"ACTED_IN")
}
type Movie {
  title: String
  released: Int
  tagline: String
}
type Query {
    person : [Person]
}
"""

gson = new Gson()
render = (ResponseTransformer)gson.&toJson
def query(value) { gson.fromJson(value,Map.class)["query"] }

graphql = new Translator(SchemaBuilder.buildSchema(schema))
def translate(query) { graphql.translate(query) }

driver = GraphDatabase.driver("bolt://localhost",AuthTokens.basic("neo4j","password"))
def run(cypher) { driver.session().withCloseable { it.run(cypher.query, Values.value(cypher.params)).list{ it.asMap() }}}

post("/graphql","application/json", { req, res ->  run(translate(query(req.body())).first()) }, render);

Run the example with:

groovy docs/Server.groovy

and use http://localhost:4567/graphql as your GraphQL URL.

It uses a schema of:

type Person {
  name: String
  born: Int
  actedIn: [Movie] @relation(name:"ACTED_IN")
}
type Movie {
  title: String
  released: Int
  tagline: String
}
type Query {
    person : [Person]
}

And can run queries like:

{
  person(first:3) {
    name
    born
    actedIn(first:2) {
      title
    }
  }
}
graphiql

You can also test it with curl

curl -XPOST http://localhost:4567/graphql -d'{"query":"{person {name}}"}'

Advanced Queries

{
  person(filter: {name_starts_with: "L"}, orderBy: "born_asc", first: 5, offset: 2) {
    name
    born
    actedIn(first: 1) {
      title
    }
  }
}
{
  person(filter: {name_starts_with: "J", born_gte: 1970}, first:2) {
    name
    born
    actedIn(first:1) {
      title
      released
    }
  }
}

Features

Current

  • parse SDL schema

  • resolve query fields via result types

  • handle arguments as equality comparisons for top level and nested fields

  • handle relationships via @relation directive on schema fields

  • handle first, offset arguments

  • argument types: string, int, float, array

  • parameter support

  • parametrization

  • aliases

  • inline and named fragments

  • auto-generate queries

Next

  • auto-generate mutations

  • sorting (nested)

  • interfaces

  • input types

  • @cypher for fields

  • unions

  • scalars

  • date(time), spatial

Documentation

Parse SDL schema

Currently schemas with object types, enums and Query types are parsed and handled. It supports the built-in scalars for GraphQL.

Resolve query Fields via Result Types

For query fields that result in object types (even if wrapped in list/non-null), the appropriate object type is found in the schema and used to translate the query.

e.g.

type Query {
  person: [Person]
}
# query "person" is resolved to and via "Person"

type Person {
  name : String
}

Handle Arguments as Equality Comparisons for Top Level and Nested Fields

If you add a simple argument to your top-level query or nested related fields, those will be translated to direct equality comparisons.

person(name:"Joe", age:42) {
   name
}

to

MATCH (person:Person) WHERE person.name = 'Joe' AND person.age = 42 RETURN person { .name } AS person

Only that the literal values are turned into parameters.

Handle Relationships via @relation Directive on Schema Fields

If you want to represent a relationship from the graph in GraphQL you have to add an @relation directive that contains the relationship-type and the direction. Default relationship-type is 'OUT'. So you can use different domain names in your GraphQL fields that are independent of your graph model.

type Person {
  name : String
  actedIn: [Movie] @relation(name:"ACTED_IN", direction:"OUT")
}
person(name:"Keanu Reeves") {
  name
  actedIn {
    title
  }
}
Note
We use Neo4j’s pattern comprehensions to represent nested graph patterns in Cypher.

Handle first, offset Arguments

To support pagination first is translated to LIMIT in Cypher and offset into SKIP For nested queries these are converted into slices for arrays.

person(offset: 5, first:10) {
  name
}
MATCH (person:Person) RETURN person { .name }  AS person SKIP 5 LIMIT 10

Argument Types: string, int, float, array

The default Neo4j types are handled both as argument types as well as field types.

Note
Datetime and spatial not yet.

Parameter Support

We handle passed in GraphQL parameters, these are resolved correctly when used within the GraphQL query.

Parametrization

As we don’t want to have literal values in our Cypher queries, all of them are translated into parameters.

person(name:"Joe", age:42, first:10) {
   name
}

to

MATCH (person:Person) WHERE person.name = $personName AND person.age = $personAge RETURN person { .name } AS person LIMIT $first

Those parameters are returned as part of the Cypher type that’s returned from the translate() method.

Aliases

We support query aliases, they are used as Cypher aliases too, so you get them back as keys in your result records.

For example:

query {
  jane: person(name:"Jane") { name, age }
  joe: person(name:"Joe") { name, age }
}

Inline and Named Fragments

This is more of a technical feature, both types of fragments are resolved internally.

Sorting (top-level)

We support sorting via an orderBy argument, which takes an Enum or String value of fieldName_asc or fieldName_desc.

query {
  person(orderBy:[name_asc, age_desc]) {
     name
     age
  }
}
MATCH (person:Person)
RETURN person { .name, .age } AS person

ORDER BY person.name ASC, person.age DESC
Note
Those enums are not yet automatically generated. And we don’t support ordering yet on nested, related fields.

@relationship on Types

To represent rich relationship types with properties, a @relation directive is supported on an object type.

In our example it would be the Role type.

type Role @role(name:"ACTED_IN", start:"actor", end:"movie") {
   actor: Person
   movie: Movie
   roles: [String]
}
type Person {
  name: String
  born: Int
  roles: [Role]
}
type Movie {
  title: String
  released: Int
  characters: [Role]
}
person(name:"Keanu Reeves") {
   roles {
      roles
      movie {
        title
      }
   }
}

Filters

Filters are a powerful way of selecting a subset of data. Inspired by the graph.cool/Prisma filter approach, our filters work the same way.

Note
we’ll create more detailed docs, for now the prisma docs on that topic are pretty good.

We use nested input types for arbitrary filtering on query types and fields

{ Company(filter: { AND: { name_contains: "Ne", country_in ["SE"]}}) { name } }

You can also apply nested filter on relations, which use suffixes like ("",not,some, none, single, every)

{ Company(filter: {
    employees_none { name_contains: "Jan"},
    employees_some: { gender_in : [female]},
    company_not: null })
    {
      name
    }
}
Note
Those nested input types are not yet generated, we use leniency in the parser.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.