neo4j-graphql-binding
provides a quick way to embed a Neo4j Graph Database GraphQL API (using the neo4j-graphql plugin) into your local GraphQL server.
If you're new to using Neo4j Graph Databases or the neo4j-graphql plugin, here is a good article to get started: Using the neo4j-graphql plugin in Neo4j Desktop
In your server setup, you use neo4jGraphQLBinding
to create a GraphQL binding to your Neo4j server and set the binding into your request context. The binding can then be accessed in your local resolvers to send requests to your remote Neo4j GraphQL server for any query
or mutation
in your typeDefs
with a @cypher directive. Queries use the read only graphql.query procedure and mutations use the read/write graphql.execute procedure.
In order to update your Neo4j GraphQL schema, you can use the neo4jIDL helper function. Internally, this sends a request to Neo4j to call the graphql.idl procedure using the typeDefs you provide.
You can use neo4jExecute
as a helper for using the binding. If you delegate the processing of a local resolver entirely to your Neo4j GraphQL server, then you only use the binding once in that resolver and have to repeat its name. neo4jExecute
automates this away for you by obtaining request information from the local resolver's info argument.
In order to use the query and mutation types generated by neo4j-graphql, you can use buildNeo4jTypeDefs
to add the same generated types into your typeDefs
. This currently generates query types for each type with a @model directive. The generated queries have support for first
and offset
(for pagination), and orderBy
in asc and desc order for each field on a type. The 'filter' argument is not yet available and, for now, only a creation mutation is generated (e.g. createPerson).
npm install --save neo4j-graphql-binding
In your local GraphQL server setup, neo4jGraphQLBinding
is used with your schema's typeDefs
and your Neo4j driver to create a GraphQL binding to your Neo4j Graphql server. The binding is then set into the server's request context at the path .neo4j
:
import { GraphQLServer } from 'graphql-yoga';
import { makeExecutableSchema } from 'graphql-tools';
import { v1 as neo4j } from 'neo4j-driver';
import { neo4jGraphQLBinding, neo4jIDL } from 'neo4j-graphql-binding';
import { typeDefs, resolvers } from './schema.js';
const driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("user", "password"));
neo4jIDL(driver, typeDefs);
const localSchema = makeExecutableSchema({
typeDefs: typeDefs,
resolvers: resolvers
});
const neo4jGraphqlAPI = neo4jGraphQLBinding({
typeDefs: typeDefs,
driver: driver,
log: true // default: false
});
const server = new GraphQLServer({
schema: localSchema,
context: {
neo4j: neo4jGraphqlAPI
}
});
const options = {
port: 4000,
endpoint: '/graphql',
playground: '/playground',
};
server.start(options, ({ port }) => {
console.log(`Server started, listening on port ${port} for incoming requests.`)
});
In your schema, the binding is accessed to send a request to your Neo4j Graphql server to process any query
or mutation
in your typeDefs
that has a @cypher
directive.
Note that the @cypher directive on the createPerson mutation formats its return data into a JSON that matches the custom payload type createPersonPayload. This is possible with some Cypher features released in Neo4j 3.1 (see: https://neo4j.com/blog/cypher-graphql-neo4j-3-1-preview/).
schema.js
export const typeDefs = `
type Person {
name: String
friends: [Person] @relation(
name: "friend",
direction: OUT
)
}
type Query {
readPersonAndFriends(name: String!): [Person]
@cypher(statement: "MATCH (p:Person {name: $name}) RETURN p")
}
input createPersonInput {
name: String!
}
type createPersonPayload {
name: String
}
type Mutation {
createPerson(person: createPersonInput!): createPersonPayload
@cypher(statement: "CREATE (p:Person) SET p += $person RETURN p{ .name } AS createPersonPayload")
}
schema {
query: Query
mutation: Mutation
}
`;
export const resolvers = {
Query: {
readPersonAndFriends: (obj, params, ctx, info) => {
return ctx.neo4j.query.readPersonAndFriends(params, ctx, info);
}
},
Mutation: {
createPerson: (obj, params, ctx, info) => {
return ctx.neo4j.mutation.createPerson(params, ctx, info);
}
}
};
If you use the binding to call a remote resolver of the same name as the local resolver it's called in, you can use neo4jExecute
to avoid repeating the resolver name:
import { neo4jExecute } from 'neo4j-graphql-binding';
export const resolvers = {
Query: {
readPersonAndFriends: (obj, params, ctx, info) => {
return neo4jExecute(params, ctx, info);
}
},
Mutation: {
createPerson: (obj, params, ctx, info) => {
return neo4jExecute(params, ctx, info);
}
}
}
Handling return data using async / await:
Query: {
readPersonAndFriends: async (obj, params, ctx, info) => {
const data = await neo4jExecute(params, ctx, info);
// post-process data, send subscriptions, etc.
return data;
}
}
readPersonAndFriends.graphql
query readPersonAndFriends($name: String!) {
readPersonAndFriends(name: $name) {
name
friends {
name
}
}
}
{
"data":{
"readPersonAndFriends": [
{
"name": "Michael",
"friends": [
{
"name": "Marie"
}
]
}
]
}
}
createPerson.graphql
mutation createPerson($person: createPersonInput!) {
createPerson(person: $person) {
name
}
}
{
"data":{
"createPerson": {
"name": "Michael"
}
}
}
First, add the @model type directive to any type for which you want query and mutation types to be generated.
type Person @model {
name: String
friends: [Person] @relation(
name: "friend",
direction: OUT
)
}
Next, use buildNeo4jTypeDefs
in your server setup to generate those queries and mutations into your typeDefs and use the result in both your binding and your schema. Optionally, you can set to false either of the query
or mutation
boolean arguments in order to prevent generation.
import { GraphQLServer } from 'graphql-yoga';
import { makeExecutableSchema } from 'graphql-tools';
import { v1 as neo4j } from 'neo4j-driver';
import { neo4jGraphQLBinding, buildNeo4jTypeDefs } from 'neo4j-graphql-binding';
import { typeDefs, resolvers } from './schema.js';
const driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("user", "password"));
const neo4jTypeDefs = buildNeo4jTypeDefs({
typeDefs: typeDefs,
query: true, // default
mutation: true // default
});
const neo4jGraphqlAPI = neo4jGraphQLBinding({
typeDefs: neo4jTypeDefs,
driver: driver
});
const localSchema = makeExecutableSchema({
typeDefs: neo4jTypeDefs,
resolvers: resolvers
});
...
If you already have a Person query or a createPerson mutation, buildNeo4jTypeDefs
will not overwrite them. In this case, the following query type would be added to your typeDefs along with the same arguments neo4j-graphql
generates (to learn more about these arguments, see: https://github.com/neo4j-graphql/neo4j-graphql/tree/3.3#auto-generated-query-types).
Person(name, names, orderBy, _id, _ids, first, offset): [Person]
as well as the mutation (which returns update statistics):
createPerson(name): String
Now the binding will be able to generate requests for the auto-generated query and mutation types.
import { neo4jExecute } from 'neo4j-graphql-binding';
export const resolvers = {
Query: {
Person: (obj, params, ctx, info) => {
return neo4jExecute(params, ctx, info);
}
},
Mutation: {
createPerson: (obj, params, ctx, info) => {
return neo4jExecute(params, ctx, info);
}
}
}
To query for Finn's first two friends, in ascending order:
readPersonAndFriends.graphql
query readPersonAndFriends($name: String!) {
Person(name: $name) {
name
friends(first: 2, orderBy: name_asc) {
name
}
}
}
{
"data":{
"Person": [
{
"name": "Finn",
"friends": [
{
"name": "BMO"
},
{
"name": "Jake"
}
]
}
]
}
}
createPerson.graphql
mutation createPerson($name: String) {
createPerson(name: $name)
}
{
"data": {
"createPerson": "Nodes created: 1\nProperties set: 1\nLabels added: 1\n"
}
}
- Add support for additional mutations generated by the
neo4j-graphql
plugin. - Write some tests.
- Document API using JSDoc.
- Provide more examples for complex queries and mutations in github repo.
- Look into ways to generate the cypher for complex mutations that use @cypher directives.
- Progressively update what
buildNeo4jTypeDefs
generates as theneo4j-graphql
plugin improves. - Generate the same schema description comments that neo4j-graphql adds to its schema (the comments show up in GraphQL Playground's schema display).
The code is available under the MIT license.