This is the reference documentation for @neo4j/graphql. It covers the programming model, APIs, concepts, annotations and technical details of the library.
This document is split into zones;
-
Setup - Importable functions and classes
-
Schema - Defining your GraphQL Schema
-
Querying - Interacting with the generated schema
-
Developer Notes - Some tips, pointers and gotchas pointed out
Importable functions and classes.
Main Entry to the library.
const neo4j = require("neo4j-driver");
const { Neo4jGraphQL } = require("@neo4j/graphql");
const { ApolloServer } = require("apollo-server");
const driver = neo4j.driver(
config.NEO_URL,
neo4j.auth.basic("admin", "password")
);
const neoSchema = new Neo4jGraphQL({
typeDefs,
resolvers?,
context?,
schemaDirectives?,
debug?,
});
const apolloServer = new ApolloServer({
schema: neoSchema.schema,
context: ({ req }) => ({ req, driver })
});
Notice
context
driver injection
This library will auto-generate resolvers for queries and mutations, you don’t need to implement resolvers yourself, however if you have some custom code you can specify custom resolvers.
const typeDefs = `
type User {
userId: ID!
firstName: String
lastName: String
fullName: String
}
`;
const resolvers = {
User: {
fullName(root, params, ctx, resolveInfo) {
return `${root.firstName} ${root.lastName}`;
},
},
};
const neoSchema = new Neo4jGraphQL({
typeDefs,
resolvers,
});
Common applications won’t just expose a single API. On the same instance as the GraphQL API there may be; scheduled jobs, authentication, migrations and not to forget any custom logic in the resolvers themselves. We expose a OGM(Object Graph Model) on top of the pre-existing GraphQL work and abstractions. Generate your normal GraphQL schema & use the exposed .model method to receive an instance of a model.
import { OGM } from "@neo4j/graphql";
import * as neo4j from "neo4j-driver";
const typeDefs = `
type Movie {
id: ID
name: String
}
`;
const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("admin", "password"));
const ogm = new OGM({ typeDefs, driver });
const Movie = ogm.model("Movie");
const [theMatrix] = await Movie.find({ where: { name: "The Matrix" } });
You can call the following on the model;
-
find
-
create
-
delete
-
update
Each method maps to the underlying generated Query or Mutation for that Model.
The @private
directive allows you to specify fields that should only be accessible through the OGM. This is very handy as you can hide fields such as user password to the outside world. Simply put the @private directive on the field you wish to be inaccessible through the exposed API;
type User {
username: String!
email: String!
password: String! @private
}
Using the password field is a great example here. In your application, you would want to hash passwords & hide them from snoopers. You could have a custom resolver, using the OGM, to update and set passwords. This is more apparent when you want to use the same type definitions to drive a public-facing schema and an OGM;
import { Neo4jGraphQL, OGM } from "@neo4j/graphql";
import * as neo4j from "neo4j-driver";
const driver = neo4j.driver(
"bolt://localhost:7687",
neo4j.auth.basic("admin", "password")
);
const typeDefs = `
type User {
username: String!
email: String!
password: String! @private
}
`;
// public without password
const neoSchema = new Neo4jGraphQL({ typeDefs, context: { driver } });
// private with access to password
const ogm = new OGM({ typeDefs, driver });
const apolloServer = new ApolloServer({ schema: neoSchema.schema });
We also exclude the following directives from OGM generation;
-
@auth
-
@exclude
This is a GraphQL specific term. When you preform a query you have the operation;
query {
myOperation
}
And you also have a Selection Set;
query {
myOperation {
# Selection Set start
id
name
} # Selection Set end
}
When using the OGM we do not want users providing a selections sets… Doing so would make the OGM feel more like querying the GraphQL Schema when the OGM is designed as an abstraction ontop of it. To combat this we do Autogenerated Selection Sets. Given a Node;
type Node {
id: ID
name: String
relation: [Node] @relationship(...)
customCypher: [Node] @cypher(...)
}
We pre-generate a pre-defined selection set. We don’t include any relationships or cypher fields, as they could be computationally expensive. Given the above Node the auto pre-defined selection set would be;
{
id
name
}
This means that by default, querying for Node(s), you would only get the .id
and .name
properties returned. If you want to select more you can either define a selection set at execution time or as a static on the Model;
import { OGM } from "@neo4j/graphql";
import * as neo4j from "neo4j-driver";
const driver = neo4j.driver(
"bolt://localhost:7687",
neo4j.auth.basic("admin", "password")
);
const typeDefs = `
type Node {
id: ID
name: String
relation: [Node] @relationship(...)
customCypher: [Node] @cypher(...)
}
`;
const ogm = new OGM({ typeDefs, driver });
const Node = ogm.model("Node");
const selectionSet = `
{
id
name
relation {
id
name
}
customCypher {
id
name
}
}
`;
const nodes = await Node.find({ selectionSet });
import { OGM } from "@neo4j/graphql";
import * as neo4j from "neo4j-driver";
const driver = neo4j.driver(
"bolt://localhost:7687",
neo4j.auth.basic("admin", "password")
);
const typeDefs = `
type Node {
id: ID
name: String
relation: [Node] @relationship(...)
customCypher: [Node] @cypher(...)
}
`;
const ogm = new OGM({ typeDefs, driver });
const Node = ogm.model("Node");
const selectionSet = `
{
id
name
relation {
id
name
}
customCypher {
id
name
}
}
`;
Node.setSelectionSet(selectionSet)
Used to translate the resolveInfo
object of a custom resolver into cypher and params. Only to be used on custom/overridden resolvers. Using this function can act as both a pre and post mechanism for your resolvers.
const { Neo4jGraphQL, translate } = require("@neo4j/neo4j-graphql");
const typeDefs = `
type User {
name: String
}
`;
const resolvers = {
Query: {
users: (root, args, context, resolveInfo) => {
// pre
const [cypher, params] = translate({
context,
resolveInfo,
});
// post
},
},
};
const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers });
The Neo4j database may be added to the GraphQL context object;
const server = new ApolloServer({
schema,
context: { driver, driverConfig: { database: "sanmateo" } }
});
Defining your GraphQL Schema.
To represent a relationship between two nodes use the @relationship
directive;
type Node {
id: ID
related: [Node] @relationship(type: "RELATED", direction: "OUT")
}
GraphQL schema directive that can be used to bind a GraphQL field to the results of a Cypher query.
For example, let’s add a field similarMovies
to our Movie which is bound to a Cypher query to find other movies with an overlap of actors;
type Actor {
actorId: ID!
name: String
movies: [Movie] @relationship(type: "ACTED_IN", direction: "OUT")
}
type Movie {
movieId: ID!
title: String
description: String
year: Int
actors(limit: Int = 10): [Actor]
@relationship(type: "ACTED_IN", direction: "IN")
similarMovies(limit: Int = 10): [Movie]
@cypher(
statement: """
MATCH (this)<-[:ACTED_IN]-(:Actor)-[:ACTED_IN]->(rec:Movie)
WITH rec, COUNT(*) AS score ORDER BY score DESC
RETURN rec LIMIT $limit
"""
)
}
As well as fields on types you can also define a custom @cypher
directive on a custom Query or Mutation;
type Actor {
actorId: ID!
name: String
}
type Query {
allActors: [Actor]
@cypher(
statement: """
MATCH (a:Actor)
RETURN a
"""
)
}
Global variables available inside the @cypher
statement.
-
this
- bound to the currently resolved node -
auth
- See below
auth
interface Auth {
isAuthenticated: boolean;
roles?: string[];
jwt: any;
}
You must return a single value representing corresponding type;
Primitives
type Query {
randomNumber: Int @cypher(statement: "RETURN rand()") ## ✅ Supported
}
Nodes
type Query {
users: [User]
@cypher(
statement: """
MATCH (u:User)
RETURN u
"""
) ## ✅ Supported
}
Objects
type User {
id
}
type Query {
users: [User] @cypher(statement: """
MATCH (u:User)
RETURN {
id: u.id
}
""") ## ✅ Supported
}
Multiple Rows ❌
type User {
id
}
type Query {
users: [User] @cypher(statement: """
MATCH (u:User)-[:HAS_POST]->(p:Post)
RETURN u, p
""") ## ❌ Not Supported
}
Solution exposes built-in GraphQL Directive @auth
;
type Post @auth(rules: [
{ operations: ["create"], isAuthenticated: true }
]) {
title: String!
}
When you have production-style Auth the directive can get large and complicated. Use Extend to tackle this.
type Post {
title: String!
}
extend type Post @auth(rules: [
{ operations: ["create"], isAuthenticated: true }
])
You can use the directive on 'Type Definitions', as seen in the example above, you can also apply the directive on any field so as long as it’s not a @relationship
;
type User {
id: ID!
name: String!
}
extend type User {
password: String! @auth(rules: [
{
operations: "*",
OR: [{ roles: ["admin"] }, { allow: { id: "$jwt.sub" } }]
}
])
}
This implementation will just expect there to be an authorization header in the request object, you can authenticate users however you like. One could; Have a custom sign-in mutation, integrate with Auth0, or roll your own SSO server. The point here is that it’s just a JWT, in the library, we will decode it to make sure it’s valid… but it’s down to you to issue tokens.
The auth implementation uses JWT tokens. You are expected to pass a JWT into the request. The accepted token type should be Bearer where the header should be authorization;
POST / HTTP/1.1
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI
content-type: application/json
-
JWT_SECRET
to specificity the JWT secret -
JWT_NO_VERIFY
to disable the verification of the JWT. -
JWT_ROLES_OBJECT_PATH
to specify a object path for JWT roles otherwise defaults tojwt.roles
You specify authorization rules inside the @auth
directive, this section looks at each argument & explains how to use to secure your GraphQL API.
You can have many rules for many operations. We fallthrough each rule, on the corresponding operation, until a match. On no match, an error is thrown. You can think of rules as a big OR.
@auth(rules: [
{ operations: ["create", "update"], ... }, ## or
{ operations: ["read", "update"], ...}, ## or
{ operations: ["delete", "update"], ... } ## or
])
Operations is an array, you can re-use the same rule for many operations.
@auth(rules: [
{ operations: ["create", "update", "delete", "connect", "disconnect"] },
{ operations: ["read"] }
])
You can use `operations: "*" to denote all operations
Many different operations can be called in one query take the below mutation;
mutation {
createPosts(
input: [
{
content: "I like GraphQL",
creator: { connect: { where: { id: "user-01" } } }
}
]
) {
posts {
content
}
}
}
In the above example; First we do a create
operation then we do a connect
operation.
The full list of operations are;
-
read -
MATCH
-
create -
CREATE
-
update -
SET
-
delete -
DELETE
-
connect -
MATCH
&MERGE
-
disconnect -
MATCH
&DELETE
This is the most basic of auth. Used to ensure that there is a valid decoded JWT in the request. The most basic of applications could look something like this;
type Todo {
id: ID
title: String
}
extend type Todo @auth(rules: [{ operations: "*", isAuthenticated: true }])
Use the roles property to specify the allowed roles for an operation. Use ENV JWT_ROLES_OBJECT_PATH
to specify a object path for JWT roles otherwise defaults to jwt.roles
type User {
id: ID
name: String
}
extend type User @auth(rules: [{ operations: ["update"], roles: ["admin"] }])
Above showing an admin role is required for all operations against Users. If you have multiple roles you can add more items to the array;
extend type User @auth(rules: [{ operations: "*", roles: ["admin", "super-admin"] }])
Users only need one of many roles to satisfy a rule.
Use allow to ensure, on matched nodes, a connection exists between a value on the JWT vs a property on each matched node. Taking a closer, look let’s put two users in a hypothetical empty database;
CREATE (:User {id:"user1", name: "one"}), (:User {id:"user2", name: "two"})
type User {
id: ID!
name: String!
}
Now we have two users in our database, and given the above GraphQL type definitions - How can we restrict user1
from seeing user2
? This is where allow comes in;
type User {
id: ID!
name: String!
}
extend type User @auth(
rules: [
{
operations: ["read"],
allow: { id: "$jwt.sub" }
}
]
)
After we match the node we validate that the property on the node is equal to the jwt.sub
property. This validation is done in Cypher with two functions; validatePredicate & validate.
Given user1
has the decoded JWT;
{
"sub": "user1",
"iat": 1516239022
}
With this JWT makes a GraphQL query to get user2
;
query {
users(where: { id: "user2" }) {
name
}
}
The generated cypher for this query would look like the below and throw you out the operation.
MATCH (u:User {id: "user2"})
CALL apoc.util.validate(NOT(u.id = "user1"), "Forbidden")
RETURN u
Allow is used on the following operations;
-
read
-
update
-
connect
-
disconnect
-
delete
There may be a reason where you need to traverse across relationships to satisfy your Auth implementation. One example of this could be "Grant update access to all Moderators of a Post";
type User {
id: ID
name: String
}
type Post {
content: String
moderators: [User] @relationship(type: "MODERATES_POST", direction: "IN")
}
extend type Post @auth(rules: [
{ operations: ["update"], allow: { moderators: { id: "$jwt.sub" } } }
])
When you specify allow on a relationship you can select fields on the referenced node. It’s worth pointing out that allow on a relationship will perform an ANY
on the matched nodes; to see if there is a match.
Given the above example - There may be a time when you need to give update access to either the creator of a post or a moderator, you can use OR
and AND
inside allow;
type User {
id: ID
name: String
}
type Post {
content: String
moderators: [User] @relationship(type: "MODERATES_POST", direction: "IN")
creator: User @relationship(type: "HAS_POST", direction: "IN")
}
extend type Post
@auth(
rules: [
{
operations: ["update"],
allow: { OR: [{ moderators: { id: "$jwt.sub" } }, { creator: { id: "$jwt.sub" } }] }
}
]
)
Allow works the same as it does on Type Definitions although its context is the Field. So instead of enforcing auth rules when the node is matched and or upserted, it would instead; be called when the Field is selected or upserted. Given the following, it is hiding the password to only the user themselves;
type User {
id: ID!
name: String!
password: String! @auth(rules: [{ operations: "*", allow: { id: "$jwt.sub" } }])
}
Use the where
argument, on Node definitions, to conceptually append predicates to the Cypher WHERE
clause.
Given the current user ID is "123" and the following the schema;
type User {
id: ID
name: String
}
extend type User @auth(rules: [{ operations: "*", where: { id: "$jwt.id" } }])
Then issues a GraphQL query for users;
query {
users {
id
name
}
}
Behind the scenes the user’s ID is conceptually prepended to the query;
query {
users(where: { id: "123" }){
id
name
}
}
Where is used on the following operations;
-
read
-
update
-
connect
-
disconnect
-
delete
Use bind to ensure, on creating or updating nodes, a connection exists between a value on the JWT vs a property on a matched node. This validation is done after the operation but inside a transaction. Taking a closer, look let’s put a user in our database;
CREATE (:User {id:"user1", name: "one"})
type User {
id: ID!
name: String!
}
Given the above GraphQL type definitions - How can we restrict user1
from changing there id ?
type User {
id: ID!
name: String!
}
extend type User @auth(
rules: [
{
operations: ["update"],
bind: { id: "$jwt.sub" }
}
]
)
After we update or create the node we validate that the property on the node is equal to the JWT.sub
property. This validation is done in Cypher with function apoc.util.validate
Given user1
has the decoded JWT;
{
"sub": "user1",
"iat": 1516239022
}
With this JWT makes a GraphQL mutation to update there id to someone else;
mutation {
updateUsers(where: { id: "user1" }, update: { id: "user2" }) {
users {
name
}
}
}
The generated cypher for this query would look like the below, Throwing us out of the operation because the ids do not match.
MATCH (u:User {id: "user1"})
SET u.id = "user2"
CALL apoc.util.validate(NOT(u.id = "user1"), "Forbidden")
RETURN u
Bind is used on the following operations;
-
create
-
update
-
connect
-
disconnect
-
delete
There may be a reason where you need to traverse across relationships to satisfy your Auth implementation. One example of this could be "Ensure that users only create Posts related to themselves";
type User {
id: ID
name: String
}
type Post {
content: String
creator: User @relationship(type: "HAS_POST", direction: "IN")
}
extend type Post @auth(rules: [
{ operations: ["create"], bind: { creator: { id: "$jwt.sub" } } }
])
When you specify bind
on a relationship you can select fields on the referenced node. It’s worth pointing out that allow on a relationship will perform an ALL
on the matched nodes; to see if there is a match. This means you can only use bind
to enforce a single relationship to a single node.
You cant put the auth directive on a custom resolver. We do make life easier by injecting the auth param into it. It will be available under the context.auth
property;
import { Neo4jGraphQL } from "@neo4j/graphql";
import { ApolloServer } from "apollo-server";
const typeDefs = `
type User {
id: ID!
email: String!
password: String!
}
type Query {
myId: ID!
}
`;
const driver = neo4j.driver(
"bolt://localhost:7687",
neo4j.auth.basic("admin", "password")
);
const resolvers = {
Query: {
myId(root, args, context) {
return context.auth.jwt.sub
}
}
};
const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers });
const server = new ApolloServer({
schema: neo4jGraphQL.schema,
context: ({ req }) => ({ req, driver }),
});
server.listen(4000).then(() => console.log("online"));
You can put the @auth
directive on a field with the @cypher
directive. Functionality like allow and bind will not work but you can still utilize isAuthenticated
and roles
.
type User @exclude(operations: "*") {
id: ID
name: String
}
type Query {
users: [User] @cypher(statement: "MATCH (a:User) RETURN a") @auth(rules: [{ isAuthenticated: true }])
}
Notice you don't need to specify operations for `@auth` directives on `@cypher` fields.
type History @exclude(operations: "*") {
website: String!
}
type User {
id: ID
name: String
history: [History]
@cypher(statement: "MATCH (this)-[:HAS_HISTORY]->(h:History) RETURN h")
@auth(rules: [{ roles: ["admin"] }])
}
If you are using 3rd party Auth solutions such as Auth0 you may find your roles property being nested inside an object;
{
"https://auth0.mysite.com/claims": {
"https://auth0.mysite.com/claims/roles": ["admin"]
}
}
Specify the path in the environment;
$ JWT_ROLES_OBJECT_PATH="https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles" node server
This directive can be used to tell Neo4jGraphQL
to skip the automatic generation of the Query or Mutations for a certain type.
The only (and required) argument for this directive. Its value must either be an array containing a subset of strings from ["read", "create", "update", "delete"]
, or the string "*"
if you wish to skip the generation of the Query and all Mutations for a particular type.
To disable Query generation:
type User @exclude(operations: ["read"]) {
name: String
}
To disable single Mutation generation:
type User @exclude(operations: ["create"]) {
name: String
}
To disable multiple Mutation generation:
type User @exclude(operations: ["create", "delete"]) {
name: String
}
To disable all automatic Query and Mutation generation:
type User @exclude(operations: "*") {
name: String
}
Exclude will not effect OGM methods.
ISO datetime string stored as a [datetime
](https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime) temporal type.
type User {
createdAt: DateTime
}
@neo4j/graphql
offers Point
and CartesianPoint
types which translate to spatial values stored using [point
](https://neo4j.com/docs/cypher-manual/current/syntax/spatial) in the database. The use of either of these types in a GraphQL schema will automatically introduce the types needed to run queries and mutations relevant to these spatial types.
Queries can be run to find nodes containing a point containing the exact values specified.
query Users($longitude: Float!, $latitude: Float!) {
users(where: { location: { longitude: $longitude, latitude: $latitude } }) {
name
location {
longitude
latitude
}
}
}
Similarly, spatial values can be used in mutations to create nodes with spatial values.
mutation CreateUsers($name: String!, $longitude: Float!, $latitude: Float!) {
createUsers(input: [{ name: $name, location: { longitude: $longitude, latitude: $latitude } }]) {
users {
name
location {
longitude
latitude
}
}
}
}
Queries can be run to find nodes relative to a distance from the specified point.
For example, the following query will find users whose location is greater than 5000m (5km) away from the specified point.
query UsersOver5kmAway($longitude: Float!, $latitude: Float!) {
users(where: { location_GT: { point: { longitude: $longitude, latitude: $latitude }, distance: 5000 } }) {
name
location {
longitude
latitude
}
}
}
If the directive is specified and not provided on create will use the [database to generate a uuid](https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-randomuuid).
type User {
id: ID! @autogenerate
username: String!
}
If you place the @autogenerate
directive on a DateTime it will, on specified operations
, append a [datetime
](https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime) property to the node.
type User {
id: ID! @autogenerate
createdAt: DateTime! @autogenerate(operations: ["create"])
updatedAt: DateTime! @autogenerate(operations: ["update"])
}
This field will only feature in input types, and will not be available for querying the object type through a Query or through a Mutation response.
This field will essentially be completely ignored, and will require another way to resolve the field, such as through the use of a custom resolver.
When generating the input type for the create mutation, the value specified in this directive will be used as the default value for this field.
"""Int | Float | String | Boolean | ID | DateTime"""
scalar Scalar
"""Instructs @neo4j/graphql to set the specified value as the default value in the CreateInput type for the object type in which this directive is used."""
directive @default(
"""The default value to use. Must be a scalar type and must match the type of the field with which this directive decorates."""
value: Scalar!,
) on FIELD_DEFINITION
When translating from GraphQL to Cypher, any instances of fields to which this directive is applied will be wrapped in a coalesce()
function in the WHERE
clause (see https://neo4j.com/developer/kb/understanding-non-existent-properties-and-null-values/#_use_coalesce_to_use_a_default_for_a_null_value). This helps to query against non-existent properties in a database, however it is encouraged to populate these properties with meaningful values if this is becoming the norm. This is a very primitive implementation of the function which only takes a static default value as opposed to using another property in a node or a Cypher expression.
"""Int | Float | String | Boolean | ID | DateTime"""
scalar Scalar
"""Instructs @neo4j/graphql to wrap the property in a coalesce() function during queries, using the single value specified."""
directive @coalesce(
"""The value to use in the coalesce() function. Must be a scalar type and must match the type of the field with which this directive decorates."""
value: Scalar!,
) on FIELD_DEFINITION
Interacting with the generated schema. For the purposes of this section we will use the following schema;
type Post {
id: ID! @autogenerated
content: String!
creator: User @relationship(type: "HAS_POST", direction: "IN")
}
type User {
id: ID! @autogenerate
name: String
posts: [Post] @relationship(type: "HAS_POST", direction: "OUT")
}
You are highly encouraged to 'spin up' a playground and experiment will the full generated schema. You can also checkout the [TCK test’s](https://github.com/neo4j/graphql/tree/master/packages/graphql/tests/tck/tck-test-files) for more a detailed view.
query {
users {
id
name
}
}
query {
users {
posts {
content
}
}
}
Checkout [TCK](https://github.com/neo4j/graphql/blob/master/packages/graphql/tests/tck/tck-test-files/cypher-advanced-filtering.md) for more advanced querying.
Use the where
argument;
query {
users(where: { id: "123" }) {
id
name
}
}
Use the where
argument, on the field;
query {
users {
id
name
posts(where: { id: "123" }) {
content
}
}
}
Sort using the options
argument;
query {
users(options: { sort: { createdAt: DESC } }) {
id
name
createdAt
}
}
Sort using the options
argument, on the field;
query {
users {
id
name
posts(options: { sort: { createdAt: DESC } }) {
content
}
}
}
Limit using the options
argument;
query {
users(options: { limit: 10 }) {
id
name
createdAt
}
}
Limit using the options
argument, on the field;
query {
users {
id
name
posts(options: { limit: 10 }) {
content
}
}
}
Limit using the options
argument;
query {
users(options: { skip: 10 }) {
id
name
createdAt
}
}
Limit using the options
argument, on the field;
query {
users {
id
name
posts(options: { skip: 10 }) {
content
}
}
}
mutation {
createUsers(input: [{ name: "dan" }]) {
users {
id
name
}
}
}
mutation {
createUsers(
input: [
{
name: "dan"
posts: { create: [{ content: "cool nested mutations" }] }
}
]
) {
users {
id
name
}
}
}
mutation {
createUsers(
input: [
{
name: "dan"
posts: {
connect: { where: { content: "cool nested mutations" } }
}
}
]
) {
users {
id
name
}
}
}
mutation {
updateUsers(where: { name: "dan" }, update: { name: "dan" }) {
users {
id
name
}
}
}
mutation {
updateUsers(
where: { name: "dan" }
create: { posts: [{ content: "cool nested mutations" }] }
) {
users {
id
name
}
}
}
mutation {
updateUsers(
where: { name: "dan" }
connect: { posts: { where: { content: "cool nested mutations" } } }
) {
users {
id
name
}
}
}
mutation {
updateUsers(
where: { name: "dan" }
disconnect: { posts: { where: { content: "cool nested mutations" } } }
) {
users {
id
name
}
}
}
mutation {
deleteUsers(where: { name: "dan" }) {
nodesDeleted
}
}
mutation {
deleteUsers(where: { name: "dan" }, delete: { friends: { where: { name: "darrell" } } }) {
nodesDeleted
relationshipsDeleted
}
}
Some tips, pointers and gotchas pointed out
There is no lie that nested mutations are very powerful. We have to generate complex cypher to provide the abstractions such as connect
and disconnect
. Due to the complexity and size of the cypher we generate its not advised to abuse it. Using the Generated GraphQL schema, If you were to attempt the creation of say one hundred nodes and relations at once Neo4j may throw memory errors. This is simply because of the size of the cypher we generate. If you need to do large edits to the graph you may be better using cypher directly, that being said the abstraction’s provided should be fine for most use cases.
If memory issues are a regular occurrence. You can edit the
dbms.memory.heap.max_size
in the DBMS settings
We currently wrap the Int and Float scalars and pass them through to the database accordingly. One caveat here is that Neo4j Integers are 64-bit and JS numbers are only 53-bit, so there’s potential precision loss here, not to mention that GraphQL Int’s are only 32-bit: http://spec.graphql.org/June2018/#sec-Int. We only support 32-bit integers because of the GraphQL limit.