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.
Getting Started
Installation
npm install --save neode
Usage
// index.js
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.js
import Neode from 'neode';
const instance = new Neode('bolt://localhost:7687', 'username', 'password', true);
Usage with .env variables
npm i --save dotenv
// .env
NEO4J_PROTOCOL=neo4j
NEO4J_HOST=localhost
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=neo4j
NEO4J_PORT=7687
NEO4J_DATABASE=neo4j
NEO4J_ENCRYPTION=ENCRYPTION_OFF
// index.js
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:
NEO4J_ENCRYPTED=ENCRYPTION_ON # ENCRYPTION_ON or ENCRYPTION_OFF
NEO4J_TRUST=TRUST_SIGNED_CERTIFICATES # TRUST_ALL_CERTIFICATES, TRUST_ON_FIRST_USE, TRUST_SIGNED_CERTIFICATES, TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, TRUST_SYSTEM_CA_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
NEO4J_LOAD_BALANCING_STRATEGY=least_connected # least_connected or round_robin
NEO4J_MAX_CONNECTION_LIFETIME=36000
NEO4J_CONNECTION_TIMEOUT=36000
NEO4J_DISABLE_LOSSLESS_INTEGERS=false
with
Models
Loading You can use the with()
method to load multiple models at once.
const neode = require('neode')
.fromEnv()
.with({
Movie: require('./models/Movie'),
Person: require('./models/Person')
});
Load from Directory
You can load a directory of models by calling the withDirectory()
method.
// models/Person.js
module.exports = {
id: {
type: 'uuid',
primary: true
},
name: 'string'
}
// index.js
instance.withDirectory(__dirname+'/models');
Defining a Node Definition
Neode revolves around the notion of node definitions, or Model
s. 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
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
});
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
option | type | description | example |
---|---|---|---|
allow | Array | Whitelist of values that are allowed | allow: ['A', 'B', 'C'] |
valid | Array | A strict whitelist of valid options. All others will be rejected. | valid: ['A', 'B', 'C'] |
invalid | Array | A list of forbidden values | invalid: ['A', 'B', 'C'] |
required | Boolean | Should this field be required? | required: true |
optional | Boolean | Allow the value to be undefined |
optional: true |
forbidden | Boolean | Marks a key as forbidden which will not allow any value except undefined. Used to explicitly forbid keys. | forbidden: true |
strict | Boolean | prevent type casting for the current key | strict: true |
strip | Boolean | Marks a key to be removed from a resulting object or array after validation. | strip: true |
default | Mixed/Function | Default value for the property | default: () => new Date() |
empty | Boolean | Considers anything that matches the schema to be empty | empty: true |
error | Error/String/Function | Overrides the default error | error: errors => new CustomValidationError('Oh No!', errors) |
Boolean
option | type | description | example |
---|---|---|---|
truthy | String | ||
falsy | String | ||
insensitive | Boolean |
Date, Time, DateTime, LocalDateTime, LocalTime
option | type | description | example |
---|---|---|---|
before | String | Date , date string or "now" to compare to the current date |
|
after | String | Date , date string or "now" to compare to the current date |
Numbers (number, int, integer, float)
option | type | description | example |
---|---|---|---|
min | Number | ||
max | Number | ||
integer | Boolean | Requires the number to be an integer | |
precision | Number | Specifies the maximum number of decimal places | precision: 2 |
multiple | Number | Multiple of a number | multiple: 2 |
positive | Boolean | ||
negative | Boolean | ||
port | Boolean | Requires the number to be a TCP port, so between 0 and 65535. |
Strings
option | type | description | example |
---|---|---|---|
insensitive | Boolean | ||
min | Number | Min length | |
max | Number | Max length | |
truncate | Boolean | Will truncate value to the max length | |
creditCard | Boolean | Requires the number to be a credit card number (Using Luhn Algorithm). | |
length | Number | Exact string length | |
regex | Object | Regular expression rule | { pattern: /([A-Z]+)/, invert: true, name: 'myRule'} |
replace | Object | Replace in value | { pattern: /(^[A-Z]+)/, replace: '-' } |
alphanum | Boolean | Requires the string value to only contain a-z, A-Z, and 0-9. | |
token | Boolean | Requires the string value to only contain a-z, A-Z, 0-9, and underscore _. | |
Boolean/Object | |||
ip | Boolean/Object | ||
uri | Boolean/Object | ||
guid | Boolean | ||
hex | Boolean/Object | ||
base64 | Boolean/Object | ||
hostname | Boolean | ||
normalize | Boolean/String | ||
lowercase | Boolean | ||
uppercase | Boolean | ||
trim | Boolean | ||
isoDate | Boolean |
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
.
{
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(original, new, 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)
instance.cypher('MATCH (p:Person {name: $name}) RETURN p', {name: "Adam"})
.then(res => {
console.log(res.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)
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"}}
])
.then(res => {
console.log(res.records.length);
})
all
Nodes
Get instance.all(label, properties)
instance.model(label).all(properties)
instance.all('Person', {name: 'Adam'}, {name: 'ASC', id: 'DESC'}, 1, 0)
.then(collection => {
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)
instance.findById('Person', 1)
.then(person => {
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)
instance.find('Person', '1234')
.then(res => {...});
First by Properties
Using a key and value
instance.first(label, key, value)
instance.first(label).first(key, value)
instance.first('Person', 'name', 'Adam')
.then(adam => {...})
Using multiple properties
instance.first(label, properties)
instance.first(label).first(properties)
instance.first('Person', {name: 'Adam', age: 29})
.then(adam => {...})
Writing
Creating a Node
instance.create(label, properties);
instance.model(label).create(properties);
instance.create('Person', {
name: 'Adam'
})
.then(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);
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);
instance.mergeOn('Person', {person_id: 1234}, {name: 'Adam'});
Updating a Node
You can update a Node instance directly by calling the update()
method.
instance.create('Person', {name: 'Adam'})
.then(adam => adam.update({age: 29}));
Creating a Relationships
You can relate two nodes together by calling the relateTo()
method.
model.relateTo(other, type, properties)
Promise.all([
instance.create('Person', {name: 'Adam'}),
instance.create('Person', {name: 'Joe'})
])
.then(([adam, joe]) => {
adam.relateTo(joe, 'knows', {since: 2010})
.then(res => {
console.log(res.startNode().get('name'), ' has known ', res.endNode().get('name'), 'since', res.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)
Promise.all([
instance.create('Person', {name: 'Adam'}),
instance.create('Person', {name: 'Joe'})
])
.then(([adam, joe]) => {
adam.detachFrom(joe) // Adam does not know Joe
});
### Deleting a node
You can delete a Node instance directly by calling the `delete()` method.
```javascript
instance.create('Person', {name: 'Adam'})
.then(adam => 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.
// Movie.js
module.exports = {
// ...
ratings: {
type: 'relationship',
'relationship': 'RATED',
direction: 'IN',
target: 'User',
'cascade': 'delete'
},
actors: {
type: 'relationship',
'relationship': 'ACTS_IN',
direction: 'IN',
target: 'Actor',
'cascade': 'detach'
}
};
Note: Attempting to delete a Node without first removing any relationships will result in an error.
Deleting a set of nodes
TODO
instance.delete(label, where)
instance.delete('Person', {living: false});
Deleting all nodes of a given type
instance.deleteAll('Person');
.then(() => 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();
instance.query(query, params)
.then(res => {
console.log(res.records.length);
});
Executing a Query
You can execute a query by calling the execute()
method on the query builder.
builder.match('this', 'Node')
.whereId('this', 1)
.return('this')
.execute()
.then(res => {
console.log(res.records.length);
});
Schema
Neode will install the schema created by the constraints defined in your Node definitions.
Installing the Schema
instance.schema.install()
.then(() => 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.
instance.schema.drop()
.then(() => console.log('Schema dropped!'))