Skip to content

sajsanghvi/neo4j-graphql-binding

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

neo4j-graphql-binding

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.

Overview

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

neo4jGraphQLBinding

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.

neo4jIDL

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.

neo4jExecute

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.

buildNeo4jTypeDefs and @model directives

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).

Installation and usage

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;
  }
}
Request Examples

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"
    }
  }
}

Auto-Generated Query Types and Mutations

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);
    }
  }
}
More Examples

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"
  }
}

TODO

  • 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 the neo4j-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).

License

The code is available under the MIT license.

About

A GraphQL binding for your Neo4j GraphQL API

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 100.0%