Skip to content

Node.js Neo4J OGM inspired by Mongoose & GraphQL

License

Notifications You must be signed in to change notification settings

danstarns/neogoose

Repository files navigation

neogoose

Node.js Neo4j OGM inspired by Mongoose & GraphQL

Node.js CI npm version

neogoose

TLDR

Use GraphQL schema language to define Models. On Model CRUD input validated & output resolved through a generated GraphQL schema. Modularize your neo4js-graphql-js augmented schema with the additional power of an OGM for further database operations.

Models are not designed to support querying relationships use a session for this. This library is designed to place a CRUD api over nodes. You can also create an Executable schema to execute more complex queries.

Installation

First install Node.js, then start Neo4j & finally...

$ npm install neogoose

neo4j-driver and graphql are peerDependencies you may need to install them too.

$ npm install neo4j-driver graphql

Importing

const neogoose = require("neogoose");

Connecting

await neogoose.connect("neo4j://localhost");

Creating Multiple connections

const connection1 = await neogoose.createConnection("neo4j://1.1.1.1");
const connection2 = await neogoose.createConnection("neo4j://2.2.2.2");

Disconnecting

await neogoose.disconnect();

Defining a Model

Models are defined using GraphQL schema language.

const User = neogoose.model(
    "User",
    {
        typeDefs: `
            type User {
                id: ID!
                name: String!
                email: String!
            }
        `
    }
);

Creating a session

await neogoose.connect("neo4j://localhost");

// https://neo4j.com/developer/javascript/
const session = await neogoose.session();

Retrieving a model

const user = neogoose.model("User");

Executable schema

Compile your models into an neo4js-graphql-js augmented schema

const schema = neogoose.makeAugmentedSchema();

Transforms made before calling makeAugmentedSchema

  1. constraint directives removed
  2. Validation directives removed

⚠ All other schema directives here are ignored in neogoose land

Query

Used with;

  1. findOne
  2. findMany
  3. updateOne
  4. updateMany
  5. deleteOne
  6. deleteMany
  7. count

Equality

const dan = await User.findOne({
    name: "Dan",
});

$or

const users = await User.findMany({
    $or: [
        { name: "Dan" },
        { name: "Daniel" }
    ],
});

$and

const users = await User.findMany({
    $and: [
        { name: "Dan" },
        { repo: "neogoose" }
    ],
});

$regex

const users = await User.findMany({
    name: {
        $regex: '(?i)d.*' // equal to new Regex("^d", "i")
    },
});

⚠ Javascript regex not supported use regex stated here

$in

const users = await User.findMany({
    name: {
        $in: ["Dan", "Daniel"]
    },
});

Comparison Operators

  1. $eq
  2. $gt
  3. $gte
  4. $in
  5. $lt
  6. $lte
  7. $ne
  8. $nin

Logical Operators

  1. $and
  2. $or

Evaluation Operators

  1. $regex

Skip/Limit

Used with

  1. findMany
  2. updateMany
  3. deleteMany
const paginatedUsers = await User.findMany(
    query,
    { skip: 30, limit: 10 }
);

Creating nodes

  1. create
  2. createMany
const user = await User.create(
    {
        id: uuid(),
        name: "Dan",
        email: "email@email.com"
    },
    {
        return: true
    }
);

await User.createMany([ ... ])

Find nodes

  1. findMany
  2. findOne
const query = {
    name: "Dan",
};

const users = await User.findMany(query);

const dan = await User.findOne(query);

Update nodes

  1. updateOne
  2. updateMany
const query = {
    name: "Dan",
};

const update = {
    name: "naD"
};

await User.updateMany(query, update);

const user = await User.updateOne(
    query,
    update,
    { return: true } // use to return the updated node
);

Using $set

Regular update will replace all properties use $set to += properties on the node

const query = {
    name: "Dan",
};

const update = {
    repo: "neogoose"
};

const user = await User.updateOne(
    query,
    { $set: update },
    { return: true }
);

user.name // Dan
user.repo // neogoose

Deleting properties

$set to null

const user = await User.updateOne(
    query,
    { $set: { loggedIn: null }
);

Delete nodes

  1. deleteOne
  2. deleteMany
const query = {
    name: "Dan",
};

await User.deleteOne(
    query, 
    {
        detach: true // set to true for DETACH DELETE, delete nodes and relationships
    }
);

const users = await User.deleteMany(
    query, 
    { return: true } // use to return the deleted nodes
);

Count nodes

  1. count
const query = {
    name: "Dan",
};

const userCount = await User.count(
    query
);

Resolvers

Records returned from your Neo4j instance are 'pulled' through a GraphQL schema, you can use Resolvers to achieve 'virtuals' on a model.

const User = neogoose.model(
    "User",
    {
        typeDefs: `
            type User {
                id: ID!
                name: String!
                email: String!
                resolved: String!
            }
        `,
        resolvers: {
            User: {
                id: (root) => root.id, // Not needed
                resolved: () => "I was Resolved"
            }
        }
    }
);

Selection Set

Select more than Autogenerated Selection Set works well with Resolvers and complex nested types, Used with;

  1. findOne
  2. findMany
  3. updateOne
  4. updateMany
  5. deleteOne
  6. deleteMany
const User = neogoose.model(
    "User",
    {
        typeDefs: `
            type NestedType {
                abc: String
            }

            type User {
                id: ID!
                name: String!
                email: String!
                nested: NestedType!
            }
        `
    }
);

const selectionSet = `
    {
        id
        name
        email
        nested {
            abc
        }
    }
`

const dan = await User.findOne(
    {
        name: "Dan",
    },
    { selectionSet }
);

// exists(dan.nested.abc) === true

Autogenerated Selection Set

⚠ If you don't specify Selection Set an auto generated one will be made based on the provided type.

const User = neogoose.model(
    "User",
    {
        typeDefs: `
            type NestedType {
                abc: String
            }

            type User {
                id: ID!
                name: String!
                email: String!
                nested: NestedType!
            }
        `
    }
);

const AUTO_SELECTION_SET = `
    {
        id
        name
        email
        nested # ⚠ ERROR
    }
`

Validation

Built in support for @Validation directive.

const User = neogoose.model(
    "User",
    {
        typeDefs: `
            input UserProperties {
                id: ID! 
                name: String
                email: String
            }

            type User @Validation(properties: UserProperties) {
                id: ID!
                name: String!
                email: String!
            }
        `
    }
);

Auto Validation

⚠ If you don't specify @Validation an auto generated input will be made based on the provided type. Nested input types are not supported!

Before

{
    typeDefs: `
        type User  {
            id: ID!
            name: String!
            email: String!
        }
    `
}

After

The below is representing the Models auto generated schema if you don't provide @Validation directive.

{
    typeDefs: `
        input AUTO_GENERATED { # Default if you don't specify properties
            id: ID!
            name: String!
            email: String!  
        }

        type User @Validation(properties: AUTO_GENERATED) {
            id: ID!
            name: String!
            email: String!
        }
    `
}

Directives

Built in support for graphql-constraint-directive.

const User = neogoose.model(
    "User",
    {
        typeDefs: `
            input UserProperties {
                id: ID! @constraint(minLength: 5, format: "uid")
                name: String @constraint(minLength: 5)
                email: String @constraint(minLength: 5, format: "email")
            }

            type User @Validation(properties: UserProperties) {
                id: ID!
                name: String!
                email: String!
            }
        `
    }
);

@constraint directives are removed before augmented schema generation.