0.5.2 • Published 9 months ago

@bytebunker/neode v0.5.2

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

Neode

Neode is a Neo4j OGM for Node.js designed to take care of the CRUD boilerplate involved with setting up a neo4j project with Node. Just install, set up your models and go.

!IMPORTANT
This fork rewrites neode to Typescript, clean up and introduce some small new features currently.

While it is usable and most previous tests are passing, it is still a work in progress. The documentation should be mostly up to date now to reflect the changes and recommended usage. Expect a couple of breaking changes, most importantly that a lot of functions which were previously similar to getters, are now actually getters (so the function brackets have to be removed). But this shouldn't be a big issue, Typescript issues should highlight almost all migration issues.

Getting Started

Installation

pnpm add neode

Usage

// index.ts
import { Neode } from 'neode';

const instance = new Neode('bolt://localhost:7687', 'username', 'password');

Enterprise Mode

To initiate Neode in enterprise mode and enable enterprise features, provide a true variable as the fourth parameter.

// index.ts
import Neode from 'neode';

const instance = new Neode({
    connectionString: "bolt://localhost:7687",
    username: "username",
    password: "password",
    enterprise: false,
    database: "database",
    logging: "all"
});

Usage with .env variables

# .env
NEO4J_PROTOCOL=neo4j
NEO4J_HOST=localhost
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=neo4j
NEO4J_PORT=7687
NEO4J_DATABASE=neo4j
NEO4J_ENCRYPTION=ENCRYPTION_OFF
// index.ts
import { Neode } from 'neode';

const instance = Neode.fromEnv();

Additional Driver Config

Additional driver configuration can be passed as the fifth parameter in the constructor, or defined in .env:

# ENCRYPTION_ON or ENCRYPTION_OFF
NEO4J_ENCRYPTED=ENCRYPTION_ON
# TRUST_ALL_CERTIFICATES, TRUST_ON_FIRST_USE, TRUST_SIGNED_CERTIFICATES, TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
NEO4J_TRUST=TRUST_SIGNED_CERTIFICATES
NEO4J_TRUSTED_CERTIFICATES=/path/to/cert.pem
NEO4J_KNOWN_HOSTS=127.0.0.1
NEO4J_MAX_CONNECTION_POOLSIZE=100
NEO4J_MAX_TRANSACTION_RETRY_TIME=5000
# least_connected or round_robin
NEO4J_LOAD_BALANCING_STRATEGY=least_connected
NEO4J_MAX_CONNECTION_LIFETIME=36000
NEO4J_CONNECTION_TIMEOUT=36000
NEO4J_DISABLE_LOSSLESS_INTEGERS=false

Loading with Models

You can use the with() method to load multiple models at once.

import { Neode } from 'neode';
import { movieModel } from "./models/movie.js"; // a ts-file which is imported using .js extension
import { personModel } from "./models/person.js";

const neode = Neode
    .fromEnv()
    .with({
        Movie: movieModel,
        Person: personModel
    });

!IMPORTANT
Loading models by scanning a directory is not currently supported.

Defining a Node Definition

Neode revolves around the notion of node definitions, or Models. To interact with the graph, you will need to define a node, identified by a name and with a schema of properties.

instance.model(name, schema);

Schema Object

import type { SchemaObject } from '@bytebunker/neode/types';

instance.model('Person', {
    person_id: {
        primary: true,
        type: 'uuid',
        required: true, // Creates an Exists Constraint in Enterprise mode
    },
    payroll: {
        type: 'number',
        unique: 'true', // Creates a Unique Constraint
    },
    name: {
        type: 'name',
        index: true, // Creates an Index
    },
    age: 'number' // Simple schema definition of property : type
} satisfies SchemaObject);
Property Types

The following property types are supported:

  • string
  • number
  • int
  • integer
  • float
  • uuid
  • node
  • nodes
  • relationship
  • relationships
  • Temporal
    • date
    • time
    • datetime
    • localtime
    • localdatetime
    • duration
  • Spatial
    • point
    • distance
Validation

Validation is provided by the Joi library. Certain data types (float, integer, boolean) will also be type cast during the data cleansing process. For more information on the full range of validation options, read the Joi API documentation.

All Types
optiontypedescriptionexample
allowArrayWhitelist of values that are allowedallow: ['A', 'B', 'C']
validArrayA strict whitelist of valid options. All others will be rejected.valid: ['A', 'B', 'C']
invalidArrayA list of forbidden valuesinvalid: ['A', 'B', 'C']
requiredBooleanShould this field be required?required: true
optionalBooleanAllow the value to be undefinedoptional: true
forbiddenBooleanMarks a key as forbidden which will not allow any value except undefined. Used to explicitly forbid keys.forbidden: true
strictBooleanprevent type casting for the current keystrict: true
stripBooleanMarks a key to be removed from a resulting object or array after validation.strip: true
defaultMixed/FunctionDefault value for the propertydefault: () => new Date()
emptyBooleanConsiders anything that matches the schema to be emptyempty: true
errorError/String/FunctionOverrides the default errorerror: errors => new CustomValidationError('Oh No!', errors)
Boolean
optiontypedescriptionexample
truthyString
falsyString
insensitiveBoolean
Date, Time, DateTime, LocalDateTime, LocalTime
optiontypedescriptionexample
beforeStringDate, date string or "now" to compare to the current date
afterStringDate, date string or "now" to compare to the current date
Numbers (number, int, integer, float)
optiontypedescriptionexample
minNumber
maxNumber
integerBooleanRequires the number to be an integer
precisionNumberSpecifies the maximum number of decimal placesprecision: 2
multipleNumberMultiple of a numbermultiple: 2
positiveBoolean
negativeBoolean
portBooleanRequires the number to be a TCP port, so between 0 and 65535.
Strings
optiontypedescriptionexample
insensitiveBoolean
minNumberMin length
maxNumberMax length
truncateBooleanWill truncate value to the max length
creditCardBooleanRequires the number to be a credit card number (Using Luhn Algorithm).
lengthNumberExact string length
regexObjectRegular expression rule{ pattern: /([A-Z]+)/, invert: true, name: 'myRule'}
replaceObjectReplace in value{ pattern: /(^[A-Z]+)/, replace: '-' }
alphanumBooleanRequires the string value to only contain a-z, A-Z, and 0-9.
tokenBooleanRequires the string value to only contain a-z, A-Z, 0-9, and underscore _.
emailBoolean/Object
ipBoolean/Object
uriBoolean/Object
guidBoolean
hexBoolean/Object
base64Boolean/Object
hostnameBoolean
normalizeBoolean/String
lowercaseBoolean
uppercaseBoolean
trimBoolean
isoDateBoolean

Defining Relationships

Relationships can be created in the schema or defined retrospectively.

instance.model(label).relationship(type, relationship, direction, target, schema, eager, cascade, node_alias);
instance.model('Person').relationship('knows', 'relationship', 'KNOWS', 'out', 'Person', {
    since: {
        type: 'number',
        required: true,
    },
    defaulted: {
        type: 'string',
        default: 'default'
    }
});

Eager Loading

You can eager load relationships in a findAll() call by setting the eager property inside the relationship schema to true.

const schema = {
    acts_in: {
        type: "relationship",
        target: "Movie",
        relationship: "ACTS_IN",
        direction: "out",
        properties: {
            name: "string"
        },
        eager: true // <-- eager load this relationship
    }
}

Eager loaded relationships can be retrieved by using the get() method. A Collection instance will be returned.

const person = person.find({name: "Tom Hanks"})
const movies = person.get('acts_in');
const first = movies.first();

Extending a Schema definition

You can inherit the schema of a class and extend by calling the extend method.

instance.extend(originalLabel, newLabel, schema)
instance.extend('Person', 'Actor', {
    acts_in: {
        type: "relationship",
        target: "Movie",
        relationship: "ACTS_IN",
        direction: "out",
        properties: {
            name: "string"
        }
    }
})

Reading

Running a Cypher Query

instance.cypher(query, params)
const result = await instance.cypher('MATCH (p:Person {name: $name}) RETURN p', { name: "Adam" });
console.log(result.records.length);

Running a Batch

Batch queries run within their own transaction. Transactions can be sent as either a string or an object containing query and param properties.

instance.batch(queries)
const result = await instance.batch([
    { query: 'CREATE (p:Person {name: $name}) RETURN p', params: { name: "Adam" } },
    { query: 'CREATE (p:Person {name: $name}) RETURN p', params: { name: "Joe" } },
    {
        query: 'MATCH (first:Person {name: $first_name}), (second:Person {name: $second_name}) CREATE (first)-[:KNOWS]->(second)',
        params: { name: "Joe" }
    }
]);

console.log(result.records.length);

Get all Nodes

instance.all(label, properties)
instance.model(label).all(properties)
const collection = await instance.all('Person', { name: 'Adam' }, { name: 'ASC', id: 'DESC' }, 1, 0);

console.log(collection.length); // 1
console.log(collection.get(0).get('name')); // 'Adam'

Get Node by Internal Node ID

instance.findById(label, id)
instance.model(label).findById(id)
const person = await instance.findById('Person', 1);
console.log(person.id()); // 1

Get Node by Primary Key

Neode will work out the model's primary key and query based on the supplied value.

instance.find(label, id)
instance.model(label).find(id)
const result = await instance.find('Person', '1234');
// ...

First by Properties

Using a key and value

instance.first(label, key, value)
instance.first(label).first(key, value)
const adam = await instance.first('Person', 'name', 'Adam');

Using multiple properties

instance.first(label, properties)
instance.first(label).first(properties)
const adam = await instance.first('Person', { name: 'Adam', age: 29 });
// ...

Writing

Creating a Node

instance.create(label, properties);
instance.model(label).create(properties);
const adam = await instance.create('Person', {
    name: 'Adam'
});

console.log(adam.get('name')); // 'Adam'

Merging a Node

Nodes are merged based on the indexes and constraints.

instance.merge(label, properties);
instance.model(label).merge(properties);
await instance.merge('Person', {
    person_id: 1234,
    name: 'Adam',
});

Merge On Specific Properties

If you know the properties that you would like to merge on, you can use the mergeOn method.

instance.mergeOn(label, match, set);
instance.model(label).mergeOn(match, set);
await instance.mergeOn('Person', { person_id: 1234 }, { name: 'Adam' });

Updating a Node

You can update a Node instance directly by calling the update() method.

const adam = await instance.create('Person', { name: 'Adam' });
await adam.update({ age: 29 });

Creating a Relationships

You can relate two nodes together by calling the relateTo() method.

model.relateTo(other, type, properties)
const [adam, joe] = await Promise.all([
    instance.create('Person', { name: 'Adam' }),
    instance.create('Person', { name: 'Joe' })
]);

const relation = await adam.relateTo(joe, 'knows', { since: 2010 });
console.log(relation.startNode().get('name'), ' has known ', relation.endNode().get('name'), 'since', relation.get('since')); // Adam has known Joe since 2010

Note: when creating a relationship defined as in (DIRECTION_IN), from from() and to() properties will be inversed regardless of which model the relationship is created by.

Detaching two nodes

You can detach two nodes by calling the detachFrom() method.

model.detachFrom(other)
const [adam, joe] = await Promise.all([
    instance.create('Person', { name: 'Adam' }),
    instance.create('Person', { name: 'Joe' })
]);
adam.detachFrom(joe); // Adam does not know Joe

Deleting a node

You can delete a Node instance directly by calling the delete() method.

const adam = await instance.create('Person', { name: 'Adam' });
await adam.delete();

Cascade Deletion

While deleting a Node with the delete() method, you can delete any dependant nodes or relationships. For example, when deleting a Movie you may also want to remove any reviews but keep the actors.

You cna do this by setting the cascade property of a relationship to "delete" or "detach". "delete" will remove the node and relationship by performing a DETACH DELETE, while "detach" will simply remove the relationship, leaving the node in the graph.

// movieModel.ts
import { RelationshipDirectionEnum, RelationshipCascadePolicyEnum } from '@bytebunker/neode';
import { SchemaObject } from "@bytebunker/neode/types";

export const movieModel = {
    // ...
    ratings: {
        type: 'relationship',
        'relationship': 'RATED',
        direction: RelationshipDirectionEnum.IN,
        target: 'User',
        'cascade': RelationshipCascadePolicyEnum.DELETE
    },
    actors: {
        type: 'relationship',
        'relationship': 'ACTS_IN',
        direction: RelationshipDirectionEnum.IN,
        target: 'Actor',
        'cascade': RelationshipCascadePolicyEnum.DETACH
    }
} satisfies SchemaObject;

Note: Attempting to delete a Node without first removing any relationships will result in an error.

Deleting a set of nodes

TODO

await instance.delete(label, where)
await instance.delete('Person', { living: false });

Deleting all nodes of a given type

await instance.deleteAll('Person');
console.log('Everyone has been deleted');

Query Builder

Neode comes bundled with a query builder. You can create a Query Builder instance by calling the query() method on the Neode instance.

const builder = instance.query();

Once you have a Builder instance, you can start to defining the query using the fluent API.

builder.match('p', 'Person')
    .where('p.name', 'Adam')
    .return('p');

For query examples, check out the Query Builder Test suite.

Building Cypher

You can get the generated cypher query by calling the build() method. This method will return an object containing the cypher query string and an object of params.

const { query, params } = builder.build();

const result = await instance.query(query, params)
console.log(result.records.length);

Executing a Query

You can execute a query by calling the execute() method on the query builder.

const result = await builder.match('this', 'Node')
    .whereId('this', 1)
    .return('this')
    .execute();

console.log(result.records.length);

Schema

Neode will install the schema created by the constraints defined in your Node definitions.

Installing the Schema

await instance.schema.install();
console.log('Schema installed!');

Note: exists constraints will only be created when running in enterprise mode. Attempting to create an exists constraint on Community edition will cause a Neo.DatabaseError.Schema.ConstraintCreationFailed to be thrown.

Dropping the schema

Dropping the schema will remove all indexes and constraints created by Neode. All other indexes and constraints will be left intact.

await instance.schema.drop()
console.log('Schema dropped!');
0.5.2

9 months ago

0.5.1

9 months ago

0.5.0

9 months ago