Skip to content

Latest commit

 

History

History
1768 lines (1366 loc) · 38.7 KB

reference.adoc

File metadata and controls

1768 lines (1366 loc) · 38.7 KB

Reference

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;

  1. Setup - Importable functions and classes

  2. Schema - Defining your GraphQL Schema

  3. Querying - Interacting with the generated schema

  4. Developer Notes - Some tips, pointers and gotchas pointed out

Setup

Importable functions and classes.

Neo4jGraphQL

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

Implementing Custom Resolvers

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.

Custom Field Resolver

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,
});

Custom Query Resolver

Same applies for Mutations and Subscriptions

const typeDefs = `
    type User {
        userId: ID!
    }

    type Query {
        users: [User]
    }
`;

const resolvers = {
    Query: {
        users: () => // do some logic
    }
};

const neoSchema = new Neo4jGraphQL({
    typeDefs,
    resolvers,
});

OGM

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;

  1. find

  2. create

  3. delete

  4. update

Each method maps to the underlying generated Query or Mutation for that Model.

@private

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;

  1. @auth

  2. @exclude

Selection Set

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;

Selection set at execution time
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 });
Selection set as a static
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)

translate

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

Specifying The Neo4j Database

The Neo4j database may be added to the GraphQL context object;

const server = new ApolloServer({
  schema,
  context: { driver, driverConfig: { database: "sanmateo" } }
});

Passing A Neo4j Driver Bookmark

A Neo4j driver bookmark may be added to the GraphQL context object;

const server = new ApolloServer({
  schema,
  context: { driver, driverConfig: { bookmarks: ["sanmateo"] } }
});

Schema

Defining your GraphQL Schema.

Nodes

To represent a node in the GraphQL schema use the type definition;

type Node {
    id: ID
}

Relationships

To represent a relationship between two nodes use the @relationship directive;

type Node {
    id: ID
    related: [Node] @relationship(type: "RELATED", direction: "OUT")
}

@cypher

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

Statement Globals

Global variables available inside the @cypher statement.

  1. this - bound to the currently resolved node

  2. auth - See below

auth

interface Auth {
    isAuthenticated: boolean;
    roles?: string[];
    jwt: any;
}

Returning from the cypher statement

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
}

@auth

About

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

Authentication

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.

Setup

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
Environment Variables
  1. JWT_SECRET to specificity the JWT secret

  2. JWT_NO_VERIFY to disable the verification of the JWT.

  3. JWT_ROLES_OBJECT_PATH to specify a object path for JWT roles otherwise defaults to jwt.roles

Server Construction

Request object needs to be injected into the context before you can use auth. Here is an example using Apollo Sever;

const neoSchema = new Neo4jGraphQL({});

const server = new ApolloServer({
    schema: neoSchema.schema,
    context: ({ req }) => ({ req }),
});

Authorization

You specify authorization rules inside the @auth directive, this section looks at each argument & explains how to use to secure your GraphQL API.

rules

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

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;

  1. read - MATCH

  2. create - CREATE

  3. update - SET

  4. delete - DELETE

  5. connect - MATCH & MERGE

  6. disconnect - MATCH & DELETE

isAuthenticated

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 }])
roles

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.

allow

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;

  1. read

  2. update

  3. connect

  4. disconnect

  5. delete

allow Across Relationships

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" } }] }
            }
        ]
    )
Field Level allow

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

where

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;

  1. read

  2. update

  3. connect

  4. disconnect

  5. delete

where across relationships

You can hop relationships with where;

type User {
    id: ID
    name: String
}

type Post {
    id: ID
    creator: User @relationship(type: "HAS_POST", direction: "OUT")
}

extend type Post @auth(rules: [{ operations: "*", where: { creator: { id: "$jwt.id" } } }])

bind

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;

  1. create

  2. update

  3. connect

  4. disconnect

  5. delete

bind Across Relationships

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.

Field Level bind

You can use bind on a field. The root is still considered the node. Taking the example at the start of this bind section; you could do the following;

type User {
    id: ID! @auth(rules: [{ operations: ["update"], bind: { id: "$jwt.sub" } }])
    name: String!
}

Auth Custom Resolvers

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"));

Auth on @cypher

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

JWT Role Path

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

Auth Value Plucking

You may have noticed, in the examples above, the usage of $jwt.xyz in the directive. This is going and grabbing the jsonwebtoken and using the xyz property. You can use both;

  1. $jwt. - Pulls value from jsonwebtoken

  2. $context. - Pulls value from context

@exclude

This directive can be used to tell Neo4jGraphQL to skip the automatic generation of the Query or Mutations for a certain type.

operations

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.

Examples

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.

DateTime

ISO datetime string stored as a [datetime](https://neo4j.com/docs/cypher-manual/current/functions/temporal/#functions-datetime) temporal type.

type User {
    createdAt: DateTime
}

Spatial types

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

Querying using spatial values

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

Mutating using spatial values

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

Advanced filtering using spatial values

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

@autogenerate

ID’s

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

Timestamps

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

Directive index

@readonly
Description

The field will only feature in object types for querying, and will not be mutable.

Definition
"""Instructs @neo4j/graphql to only include a field in generated input type for creating, and in the object type within which the directive is applied."""
directive @readonly on FIELD_DEFINITION
@writeonly
Description

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.

Definition
"""Instructs @neo4j/graphql to only include a field in the generated input types for the object type within which the directive is applied, but exclude it from the object type itself."""
directive @writeonly on FIELD_DEFINITION
@ignore
Description

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.

Definition
"""Instructs @neo4j/graphql to completely ignore a field definition, assuming that it will be fully accounted for by custom resolvers."""
directive @ignore on FIELD_DEFINITION
@default
Description

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.

Definition
"""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
@coalesce
Description

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.

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

Querying

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.

Reading

query {
    users {
        id
        name
    }
}

Reading with OGM

const User = ogm.model("User");

const users = await User.find();

Reading Relationships

query {
    users {
        posts {
            content
        }
    }
}

Reading Relationships with OGM

const User = ogm.model("User");

const selectionSet = `
    {
        posts {
            content
        }
    }
`;

const users = await User.find({
    selectionSet,
});

Filtering

Use the where argument;

query {
    users(where: { id: "123" }) {
        id
        name
    }
}

Filtering Relationships

Use the where argument, on the field;

query {
    users {
        id
        name
        posts(where: { id: "123" }) {
            content
        }
    }
}

Sorting

Sort using the options argument;

query {
    users(options: { sort: { createdAt: DESC } }) {
        id
        name
        createdAt
    }
}

Sorting Relationships

Sort using the options argument, on the field;

query {
    users {
        id
        name
        posts(options: { sort: { createdAt: DESC } }) {
            content
        }
    }
}

Limiting

Limit using the options argument;

query {
    users(options: { limit: 10 }) {
        id
        name
        createdAt
    }
}

Limiting Relationships

Limit using the options argument, on the field;

query {
    users {
        id
        name
        posts(options: { limit: 10 }) {
            content
        }
    }
}

Skipping

Limit using the options argument;

query {
    users(options: { skip: 10 }) {
        id
        name
        createdAt
    }
}

Skipping Relationships

Limit using the options argument, on the field;

query {
    users {
        id
        name
        posts(options: { skip: 10 }) {
            content
        }
    }
}

Creating

mutation {
    createUsers(input: [{ name: "dan" }]) {
        users {
            id
            name
        }
    }
}

Creating with OGM

const User = ogm.model("User");

const { users } = await User.create({ input: [{ name: "dan" }] });

Creating a relationship (Create Mutation)

mutation {
    createUsers(
        input: [
            {
                name: "dan"
                posts: { create: [{ content: "cool nested mutations" }] }
            }
        ]
    ) {
        users {
            id
            name
        }
    }
}

Connecting a relationship (Create Mutation)

mutation {
    createUsers(
        input: [
            {
                name: "dan"
                posts: {
                    connect: { where: { content: "cool nested mutations" } }
                }
            }
        ]
    ) {
        users {
            id
            name
        }
    }
}

Updating

mutation {
    updateUsers(where: { name: "dan" }, update: { name: "dan" }) {
        users {
            id
            name
        }
    }
}

Updating with OGM

const User = ogm.model("User");

const { users } = await User.update({
    where: { name: "dan" },
    update: { name: "dan" },
});

Creating a relationship (Update Mutation)

mutation {
    updateUsers(
        where: { name: "dan" }
        create: { posts: [{ content: "cool nested mutations" }] }
    ) {
        users {
            id
            name
        }
    }
}

Connecting a relationship (Update Mutation)

mutation {
    updateUsers(
        where: { name: "dan" }
        connect: { posts: { where: { content: "cool nested mutations" } } }
    ) {
        users {
            id
            name
        }
    }
}

Disconnecting a relationship

mutation {
    updateUsers(
        where: { name: "dan" }
        disconnect: { posts: { where: { content: "cool nested mutations" } } }
    ) {
        users {
            id
            name
        }
    }
}

Deleting

mutation {
    deleteUsers(where: { name: "dan" }) {
        nodesDeleted
    }
}

Nested deletes

mutation {
    deleteUsers(where: { name: "dan" }, delete: { friends: { where: { name: "darrell" } } }) {
        nodesDeleted
        relationshipsDeleted
    }
}

Deleting with OGM

const User = ogm.model("User");

await User.delete({
    where: { name: "dan" },
});

Developer notes

Some tips, pointers and gotchas pointed out

Large mutations

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

Precision Loss

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.