graphql-cypher
                                
                                 graphql-cypher copied to clipboard
                                
                                    graphql-cypher copied to clipboard
                            
                            
                            
                        A simple but powerful translation layer between GraphQL and Cypher
graphql-cypher
A simple but powerful translation layer between GraphQL and Cypher.
Note This library is currently coupled to Neo4j as a backing database, but I'd be happy to accept contributions to decrease that coupling if there is another graph database which uses Cypher that someone would like to support.
Note In its current form, certain features of this library require APOC to run. See Limitations
Table of Contents
- graphql-cypher
- Table of Contents
- Key Features
- 🔨 Simple setup
- 🌎 Helpful Cypher globals
- 🔗 Multi-data-source friendly
- 🔑 Authorization friendly
 
- Goals
- Documentation
- Setup
- Queries
- Basic querying (@cypher,@cypherNode)- @cypher: Cypher entry point
- @cypherNode: Traverse a relationship to another node
- @cypherRelationship: Represent a relationship with a type
- @cypherLinkedNodes: Represent a linked list
- @cypherVirtual: Add extra layers to the structure of returned data
 
- @cypherComputed: Compute aggregated or altered field values
- @cypherCustom: Custom Cypher queries (only supported with APOC)- Mutations
- Rules for @cypherCustomdirectives
 
- @cypherglobals
 
- Basic querying (
- Custom Resolver Logic
- Example: Authorization
- Example: External Service
 
- Generated Values
 
- Limitations
- Inspiration
- Local Development
- npm startor- yarn start
- npm run buildor- yarn build
- npm testor- yarn test
- npm run test:integration
 
 
Key Features
🔨 Simple setup
Attach Cypher resolution directives to an field in your schema and they'll be resolved accordingly, no matter where they are in the query.
🌎 Helpful Cypher globals
Important data is added automatically to your Cypher queries for you to reference. In addition to $args (the field arguments), you get parent (the parent node) and $context (special values you can add to your GraphQL context to give to every query).
type User {
  posts(offset: Int = 10): [Post!]!
    @cypherCustom(
      statement: "MATCH (parent)-[:HAS_POST]->(p:Post) RETURN p SKIP $args.offset LIMIT $context.globalPageSize"
    )
}
🔗 Multi-data-source friendly
"Skip" fields which are resolved from data sources external from your Cypher-powered database easily. graphql-cypher will plan all the queries and inter-dependencies for you.
Externally-powered fields are even woven back into Cypher via the parent variable, just like any normal Cypher query, so you can reference external data just as easily as graph-native data.
type User {
  # this field is resolved from an external source...
  settings: UserSettings! @cypherSkip
}
# suppose UserSettings is fetched from an external DB or API and
# has a shape like {accountId: String!, userId: String!}
type UserSettings {
  account: Account!
    @cypher(match: "(a:Account{id: parent.accountId})", return: "a")
}
type Query {
  user(id: ID!): User! @cypher(match: "(u:User{id:$args.id})", return: "u")
}
🔑 Authorization friendly
graphql-cypher doesn't require any resolvers by default, but it gives you the option to optionally omit fields based on logic you define in a regular old GraphQL resolver. This means it can better support custom authorization or pre/post-query logic.
const resolvers = {
  Query: {
    secret: (parent, args, ctx) => {
      if (!ctx.isAdmin) {
        return null;
      }
      // the field on the parent will be passed as a function you can choose
      // to call, or omit
      return parent.secret();
    },
  },
};
There are limitations to this functionality, please see Limitations
Goals
- Support application-focused use cases, giving users as much control as possible over their GraphQL query and mutation contracts
- Make it dead simple for a user familiar with Cypher to start resolving complex GraphQL queries with next to no business logic
- Support multi-data-source apps, letting users choose which data store is the right fit for each part of their GraphQL schema and stitch them together with ease
Documentation
Setup
Install the library and its dependencies* (if you don't have them already):
npm i -s graphql-cypher graphql graphql-middleware neo4j-driver
* graphql-middleware is not required if you're using graphql-yoga or another GraphQL server that supports middleware natively
The first step to get started is to add the middleware to your schema.
GraphQL Yoga Server
import { middleware as cypherMiddleware } from 'graphql-cypher';
import { GraphQLServer } from 'graphql-yoga';
const server = new GraphQLServer({
  // ... typedefs, etc
  middlewares: [cypherMiddleware],
});
Other GraphQL Server
import { middleware as cypherMiddleware } from 'graphql-cypher';
import { applyMiddleware } from 'graphql-middleware';
import schema from './my-schema';
const schemaWithMiddleware = applyMiddleware(schema, cypherMiddleware);
Providing directive typeDefs
Some GraphQL implementations want you to specify typeDefs for all your directives. You can do that by importing directiveTypeDefs from graphql-cypher. It's a function. Call it with no arguments, and it will generate the default typeDefs, which you can then add to your schema string. Or, pass in custom directive names if you have changed those, and it will use the names you provide (see Renaming the directives below for the names).
import { gql } from 'apollo-server';
import { makeExecutableSchema } from 'graphql-tools';
import { directiveTypeDefs } from 'graphql-cypher';
const myOtherTypeDefs = gql`
  type Query {
    foo: Boolean
  }
`;
const fullTypeDefs = [
  gql`
    ${directiveTypeDefs()}
  `,
  myOtherTypeDefs,
];
const schema = makeExecutableSchema({
  typeDefs,
});
Add your Neo4j Driver to Context
Finally, assign your Neo4j JS driver instance to a neo4jDriver field on your GraphQL context object for each request.
// Apollo example
const neo4jDriver = v1.driver('bolt://localhost:7687');
const apollo = new ApolloServer({
  context: ({ req }) => ({
    neo4jDriver,
  }),
});
Only Neo4j is supported by the library at the moment. Adding support for other Cypher-powered databases is welcome!
Now you're ready to begin adding directives to your schema. Find a field you want to resolve with Cypher and create your directive.
type Query {
  user(id: ID!): User
    @cypher(match: "(user:User {id: $args.id})", return: "user")
}
Try a query! If all went well (and data is present in your database to match the query), you should get your node back! No resolvers necessary!
Renaming the directives: if the default directive names don't work for you (for instance, if you're still using
neo4j-graphql-js@cypherCustomdirective for part of your schema), you can rename them. Just assign the directives to different properties inschemaDirectives, and then create a configured middleware by importingcreateMiddlewarefromgraphql-cypherand providing a config object.Example:
const customMiddleware = createMiddleware({ directiveNames: { cypher: 'myCypher', cypherSkip: 'myCypherSkip', cypherCustom: 'myCypherCustom', cypherNode: 'myCypherNode', cypherRelationship: 'myCypherRelationship', generateId: 'myGenerateId', } });
Queries
Basic querying (@cypher, @cypherNode)
The basic directives in this library will help you establish which parts of your schema are resolved via Cypher, and how the data is queried.
@cypher: Cypher entry point
The @cypher directive is the starting point for any Cypher-based query. Attach it to a root field, or to a field which has a non-Cypher-resolved parent.
It has a variety of arguments, all of which should feel familiar; they correspond with clauses in Cypher.
- match(- String): The contents will be added to a- MATCHclause. If you have a- WHEREstatement, it should be included in the string as well. If you want to match multiple paths, you can separate them with commas as usual.
- optionalMatch(- String): Similar to- match, but for- OPTIONAL MATCH.
- create(- String): Contents will be added to a- CREATEclause.
- createMany(- [String!]): If you have multiple- CREATEclauses, pass them as a list to this argument instead.
- merge/- mergeMany: Similar to- create/- createMany, but for- MERGEclauses.
- set/- setMany: Similar to- create/- createMany, but for- SETclauses.
- delete/- deleteMany: Similar to- create/- createMany, but for- DELETEclauses.
- detachDelete/- detachDeleteMany: Similar to- create/- createMany, but for- DETACH DELETEclauses.
- remove/- removeMany: Similar to- create/- createMany, but for- REMOVEclauses.
- orderBy(- String): Contents will be added to an- ORDER BYclause.
- skip(- String): Contents will be added to a- SKIPclause.
- limit(- String): Contents will be added to a- LIMITclause.
- return(- String!): required The name of the binding which you want to return from the query. Do not add custom property selections; the libray will handle these.
Using all these arguments, you can do almost any basic Cypher query. @cypher will be used for both queries and mutations. For queries, use it to find the node or nodes which the field returns. For mutations, use it to make changes to a node by referencing $args.
Example
type Query {
  user(id: ID!): User @cypher(match: "(user:User{id:$args.id})", return: "user")
  users(first: Int = 10, offset: Int = 0)
    @cypher(
      match: "(user:User)"
      skip: "$args.offset"
      limit: "$args.first"
      return: "user"
    )
}
type Mutation {
  createUser(input: UserCreateInput!): User!
    @cypher(
      create: "(user:User)"
      set: "user += $args.input"
      return: "user"
    )
  updateUser(input: UserUpdateInput!): User
    @cypher(
      match: "(user:User{id: $args.input.id})"
      setMany: [
        "user.name = $args.input.name",
        "user.age = $args.input.age"
      ]
      return: "user"
    )
}
@cypherNode: Traverse a relationship to another node
We're working with graphs, so obviously one of the biggest things to do is traverse a relationship and add another node to our query. @cypherNode lets us define these connections.
@cypherNode should be added to a field which represents another node or list of nodes in the graph relative to the parent type. It supports the following arguments:
- relationship(- String!): required The type of the connecting relationship (like "- HAS_POST")
- direction(- RelationshipDirection!): required The direction of the relationship (- INor- OUT). This is an enum, so no quotes are required.
- label(- String): The library will attempt to infer the label to use for the target node based on its GraphQL type name. If your type name does not match your graph node's label, supply one manually to this argument.
- where(- String): Add a- WHEREclause to your node connection to filter the results. Use the preset- nodeand- relationshipbindings to create predicates based on the matched node or relationship:- where: "node.age > $args.ageLimit"
Example
type User {
  posts: [Post!]! @cypherNode(relationship: "HAS_POST", direction: OUT)
}
@cypherRelationship: Represent a relationship with a type
If you're utilizing properties on relationships in your graph, you can also represent those using the cypherRelationship directive. Use it instead of @cypherNode on a field which represents a relationship. It accepts the following arguments:
- type(- String!): required The type of the relationship (like "- HAS_POST")
- direction(- RelationshipDirection!): required The direction of the relationship (- INor- OUT). This is an enum, so no quotes are required.
- nodeLabel(- String): We need to know the label of the target node of the relationship to make a good query, so we will try to infer it from your schema. If we can't (or if the target node label is different from your GraphQL type name), you can manually supply one here.
- where(- String): Add a- WHEREclause to your node connection to filter the results. Use the preset- nodeand- relationshipbindings to create predicates based on the matched node or relationship:- where: "node.age > $args.ageLimit"
Once you've added a @cypherRelationship directive to a field, you should then add a @cypherNode directive to the node field on your relationship type!
Example
type User {
  friends: [UserFriendship!]!
    @cypherRelationship(type: "HAS_FRIEND", direction: "OUT")
}
type UserFriendship {
  type: String
  friend: User! @cypherNode(relationshipType: "HAS_FRIEND", direction: "OUT")
}
@cypherLinkedNodes: Represent a linked list
Linked lists are a powerful concept to utilize for a series of data points in a graph. The @cypherLinkedNodes directive can help traverse a linked list with support for basic pagination. It accepts the following arguments:
- relationship(- String!): required The type of the relationship between linked nodes
- where(- String): Allows adding a- WHEREclause to the pattern. This is potentially useful to introduce cursor-based pagination. The value- nodewill be automatically bound to the ending node of the current linked list segment, so you can use- (node)in your- WHEREclause as you see fit.
- direction(- RelationshipDirection): The direction of the relationship from the source node (- INor- OUT). This defaults to- OUT.
- label(- String): We need to know the label of the list nodes to make a good query, so we will try to infer it from your schema. If we can't (or if the target node label is different from your GraphQL type name), you can manually supply one here.
- skip(- String): Provide a field parameter path to- skipto specify how many nodes you want to skip in the list (ex:- $args.input.offset)
- limit(- String): Provide a field parameter path to- limitto specify the maximum number of nodes you want to return in the list (ex:- $args.input.first)
Example
type User {
  posts(first: Int = 10, offset: Int = 0): [Post!]!
    @cypherLinkedNodes(
      relationship: "HAS_NEXT_POST"
      skip: "$args.first"
      limit: "$args.offset"
    )
  # cursor (experimental idea)
  # it may be possible to utilize the "node" binding in the where argument
  # to enforce that all returned paths come after a particular node,
  # selected by ID or some other cursor value.
  postsWithCursor(cursor: String, count: Int = 10): [Post!]!
    @cypherLinkedNodes(
      relationship: "HAS_NEXT_POST"
      limit: "$args.count"
      where: "(:Post {id:$args.cursor})-[:HAS_NEXT_POST*]->(node)"
    )
}
Tip: If your linked list uses multiple types of relationships, you can supply a multi-type matcher to
relationshiplike "HAS_FIRST_POST|HAS_NEXT_POST".
@cypherVirtual: Add extra layers to the structure of returned data
Sometimes you may want to introduce a new, intermediate layer between two parts of your GraphQL schema while continuing to fetch all the data from a single connected query. The @cypherVirtual directive is a GraphQL Type directive which indicates that a particular named type is "virtual": it only exists in your GraphQL schema, and is "invisible" to your Cypher query (mostly).
If it's still not making sense, here's an example. Suppose you've got a relationship between two nodes in your graph database like so:
(:User)-[:HAS_FRIEND]->(:Friend)
but you wanted your GraphQL schema to look like this:
type User {
  friendshipsConnection(type: String = "any"): FriendshipsConnection!
}
type FriendshipsConnection {
  edges: [FriendshipEdge!]!
}
type FriendshipEdge {
  node: User!
}
This can be accomplished by adding a cypherVirtual directive to the virual FriendshipsConnection type.
type User {
  friendshipsConnection(type: String = "any"): FriendshipsConnection!
}
type FriendshipsConnection @cypherVirtual {
  edges: [FriendshipEdge!]!
    @cypherRelationship(
      type: "HAS_FRIEND"
      direction: "OUT"
      where: "$virtual.type = 'any' OR relationship.type = $virtual.type"
    )
}
type FriendshipEdge {
  node: User! @cypherNode(relationship: "HAS_FRIEND", direction: "OUT")
}
There are a few new things to notice when you use virtual nodes. The first is the use of $virtual within the Cypher statement on the edges field. When you mark a type as Virtual, the parameters which were passed to the field which returned that type will be "copied" onward to child fields as the $virtual parameter. This is important, as it allows us to add arguments to our friendshipsConnection field which will then be used by the Cypher query to resolve our edges field within FriendshipsConnection. The library uses $virtual for this so that you can still pass in parameters directly to the edges field if you want, and they can all be easily differentiated and used in your final query.
@cypherComputed: Compute aggregated or altered field values
The @cypherComputed directive can help derive data from nodes in your graph to expose through your schema. For instance, suppose you stored firstName and lastName properties on a :User node in your graph, but wanted your GraphQL schema to have a fullName field. You can use @cypherComputed(value: "parent.firstName + ' ' + parent.lastName") to do this.
Computed fields only support one directive argument, value, which is a free-form Cypher string you may supply. Use the parent variable as usual to reference the parent node as you write your query. Whatever you supply to value must only include basic Cypher operator-based computation or user functions. You may not invoke custom procedures or use any Cypher clauses, or the generated query will be invalid.
You may also use $args or $context as usual when writing your value string. $args will reference arguments supplied to the field which you annotated with @cypherComputed.
@cypherCustom: Custom Cypher queries (only supported with APOC)
The @cypherCustom directive gives you full control over your Cypher query from start to finish, at the cost of performance. This feature uses APOC's custom Cypher functions under the hood, which can make queries hard for Neo4j to plan effectively. But, for complex queries that require advanced logic, it can help bridge the gap.
@cypherCustom can be used as a 'starting point' directive like @cypher.
There are a few ways you can write your Cypher statements:
Simple mode: one statement per directive, which will be run every time.
type Query {
  user(id: ID!): User
    @cypherCustom(
      statement: """
      MATCH (user:User {id: $args.id}) RETURN user
      """
    )
}
Conditional mode: multiple statements with conditions; the one that matches will be run.
type Query {
  posts(filter: PostFilter): [Post!]!
    @cypherCustom(
      statements: [
        {
          when: "$args.filter"
          statement: """
          MATCH (post:Post)
          WHERE post.title =~ $args.filter.titleMatch
          RETURN post
          """
        }
        {
          statement: """
          MATCH (post:Post)
          RETURN post
          """
        }
      ]
    )
}
In conditional mode, always write your last statement without a condition.
Currently, conditional mode only supports existential conditions: supply an arg name to when, and it will choose that statement if that arg exists. This supports deep paths.
@cypherCustom arguments:
- statement(- String): A single Cypher statement to run
- statements([- ConditionalCypherStatement!]): A list of conditional statements to test and run, in order.- A conditional statement has the form: { when: String, statement: String! }
 
- A conditional statement has the form: 
- returnsRelationship(- Boolean): Indicate if the custom Cypher statement will return a relationship or relationships instead of nodes. This is required if your statement returns relationships, or- @cypherNodeand- @cypherRelationshipdirectives on nested fields will generate invalid Cypher.
Mutations
Like @cypher, @cypherCustom works just fine as a starting point for a mutation as well. Just add it to a root field in your Mutation type and write your Cypher query.
Rules for @cypherCustom directives
- Don't specify property selections on the returned node ("{.id, .name}", etc). These will be managed by the library.
@cypher globals
We've already seen the $args parameter, which is available to our all our Cypher statements and fragments. There are a few other parameters as well:
- $args: All the arguments provided to your GraphQL field. This includes defaulted values.
- parent: (notice: no- $) This is the parent of the current field; analagous to the- parentfield in a GraphQL resolver.- parentworks even if the parent of your GraphQL field wasn't resolved from Cypher! Example usage:- MATCH (parent)-[:LIKES]->(post:Post)(for Cypher parents) or- MATCH (user:User{id: parent.userId})(for non-Cypher parents)
- $context: Pass values via a special- cypherContextproperty on your GraphQL context, and they will be populated in this parameter. This is great for context-centric values like the ID or permissions of the current API user.
- $generated: Any values generated by- graphql-cypherwill be available as properties on this parameter (see: Generated Values)
- $virtual: Only used in the fields of a type marked- @cypherVirtual. This parameter is an object that holds the- $argswhich were passed to the field which returned the parent- @cypherVirtualtype so they can be used by its child field queries.
Custom Resolver Logic
You can add your own custom resolver logic into the Cypher execution flow to control user access to data or call out to external services before returning the result.
Simply add your own resolver to a Cypher-powered field, and then be sure to call await parent.myFieldName() (where "myFieldName" is the name of the field you're resolving) to retrieve the field's data from your graph database when you're ready for it.
Example: Authorization
const resolvers = {
  Query: {
    adminData: (parent, args, context) => {
      if (!context.isAdmin) {
        return null;
      }
      return parent.adminData();
    },
  },
};
Example: External Service
const resolvers = {
  Query: {
    monitoredData: async (parent, args, context) => {
      const data = await parent.monitoredData();
      context.notifier.record('The user fetched the data');
      return data;
    },
  },
};
There are limitations to custom resolvers. Currently, there is no way to modify incoming arguments to a Cypher-powered field. This is a limitation of the design of this library and GraphQL itself; by the time your resolver is called, the Cypher query is likely to have already been sent to your database. With the exception of root fields, there is no way for us to modify arguments before making that initial Cypher query, and still retain the benefit of batching up all field Cypher statements into one larger query (which is the whole benefit of the library!) I have some ideas about possible ways to approach this problem, but they are a bit heavy and I'm waiting to see a bit more of how the usage pans out. In the meantime, if there's logic to do, it basically has to be done in Cypher.
Generated Values
This library ships with utility directive support for generating some values as extra arguments to your Cypher statements. All generated values are available on the $generated parameter in Cypher.
@generateId: Use it as a directive on a field to generate an ID which will be supplied as an extra argument to that field. This is useful for create mutations.
type Mutation {
  createPerson(input: PersonCreateInput!): Person!
    @generateId(argName: "personId")
    @cypher(
      create: "(person:Person{id: $generated.personId})"
      set: "person += $args.input"
      return: "person"
    )
}
@generateId takes one optional argument, argName, which can be used to change the property it gets assigned to on $generated. That's why the value is available on $generated.personId above. The default is $generated.id.
Limitations
- @cypherCustomdirectives rely on the popular APOC library for Neo4j. Chances are if you're running Neo4j, you already have it installed.
- Using parent.myField()to selectively fetch data only prevents the data from being queried if the field is the root field in your operation or the direct descendant of a non-Cypher-powered field. Otherwise, the data will still be fetched, but by omitting the call toparent.myField()you will just not return it.- This could probably be changed, but not within the current middleware model. A new traversal to just resolve the Cypher queries before the main resolvers are called would probably need to be introduced.
 
- @cypherCustomcustom queries are not going to be as performant as- @cypherand the other directives, because they run Cypher fragments in user functions, which basically means they get an entirely separate query planning phase. I haven't really profiled it much, but I see a pretty significant performance increase by sticking with the standard directives whenever possible.
Inspiration
Obviously the official neo4j-graphql-js was a huge inspiration for this library. I learned a lot by reading over their output queries, without which I would probably have struggled for far longer trying to understand how to craft the underlying Cypher queries for this library.
neo4j-graphql-js still has a lot of great features which I don't intend to bring to graphql-cypher, namely things like automated generation with convention-based parameters. I hope that graphql-cypher will become a tool geared toward a specific audience of people like me who want full control over their app, and neo4j-graphql-js can continue to evolve in the direction of auto-generated schemas and turnkey prototyping solutions.
This project was bootstrapped with TSDX.
Local Development
Below is a list of commands you will probably find useful.
npm start or yarn start
Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab.
 
Your library will be rebuilt if you make edits.
npm run build or yarn build
Bundles the package to the dist folder.
The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
 
npm test or yarn test
Runs the test watcher (Jest) in an interactive mode. By default, runs tests related to files changed since the last commit.
npm run test:integration
This library has a full integration tests suite against a real Neo4j database. To run it, you need to have Docker started. It will manage creating Neo4j containers and removing them as needed.