Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/adam-cowley/neode

Neo4j OGM for Node.js
https://github.com/adam-cowley/neode

cypher cypher-query hacktoberfest neo4j neo4j-ogm nodejs

Last synced: about 7 hours ago
JSON representation

Neo4j OGM for Node.js

Awesome Lists containing this project

README

        

# 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](#getting-started)
- [Reading from the Graph](#reading)
- [Writing to the Graph](#writing)
- [Query Builder](#query-builder)
- [Schema](#schema)

## Getting Started

### Installation
```javascript
npm install --save neode
```

### Usage
```javascript
// 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.

```javascript
// 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
```

```javascript
// 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
```

#### Loading `with` Models

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

```javascript
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.

```javascript
// models/Person.js
module.exports = {
id: {
type: 'uuid',
primary: true
},
name: 'string'
}
```

```javascript
// 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.

```javascript
instance.model(name, schema);
```

#### Schema Object
```javascript
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](https://github.com/hapijs/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](https://github.com/hapijs/joi/blob/v13.4.0/API.md).

##### 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 _.
| email | 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.

```javascript
instance.model(label).relationship(type, relationship, direction, target, schema, eager, cascade, node_alias);
```

```javascript
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`.

```javascript
{
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)
```

```javascript
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)
```

```javascript
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)
```

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

### Get `all` Nodes

```
instance.all(label, properties)
instance.model(label).all(properties)
```

```javascript
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)
```

```javascript
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)
```

```javascript
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)
```
```javascript
instance.first('Person', 'name', 'Adam')
.then(adam => {...})
```

#### Using multiple properties
```
instance.first(label, properties)
instance.first(label).first(properties)
```
```javascript
instance.first('Person', {name: 'Adam', age: 29})
.then(adam => {...})
```

## Writing

### Creating a Node
```javascript
instance.create(label, properties);
instance.model(label).create(properties);
```

```javascript
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.

```javascript
instance.merge(label, properties);
instance.model(label).merge(properties);
```

```javascript
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.

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

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

```javascript
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.

```javascript
model.relateTo(other, type, properties)
```
```javascript
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.

```javascript
model.detachFrom(other)
```
```javascript
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.

```javascript
// 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
```javascript
instance.delete(label, where)
```

```javascript
instance.delete('Person', {living: false});
```

### Deleting all nodes of a given type
```javascript
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.

```javascript
const builder = instance.query();
```

Once you have a Builder instance, you can start to defining the query using the fluent API.
```javascript
builder.match('p', 'Person')
.where('p.name', 'Adam')
.return('p');
```

For query examples, check out the [Query Builder Test suite](https://github.com/adam-cowley/neode/blob/master/test/Query/Builder.spec.js).

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

```javascript
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.

```javascript
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
```javascript
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.

```javascript
instance.schema.drop()
.then(() => console.log('Schema dropped!'))
```