Seraph.js
A terse & familiar binding to the Neo4j REST API that is
idiomatic to node.js.
Install
npm install seraph
Quick Example
var db = require("seraph")("http://localhost:7474");
db.save({ name: "Test-Man", age: 40 }, function(err, node) {
if (err) throw err;
console.log("Test-Man inserted.");
db.delete(node, function(err) {
if (err) throw err;
console.log("Test-Man away!");
});
});
Documentation
### Initialization
* [seraph](#seraph) - initialize the seraph client
Generic Operations
- query - perform a cypher query and parse the results
- rawQuery - perform a cypher query and return unparsed results
API Communication Operations
- operation - create a representation of a REST API call
- call - take an operation and call it
- batch - perform a series of atomic operations with one api call.
Node Operations
Relationship Operations
Index Operations
Compatibility
Seraph has been tested with Neo4j 2.0.0-M03. If you would like to test
compatibility with another version of neo4j, change the version values stored in
test/util/database.js
.
Testing
You can test Seraph simply by running npm test
. It will spin up its own neo4j
instance for testing. Note that the first time you run your tests (or change
neo4j version), a new version of neo4j will need to be downloaded. That can,
of course, take a little time.
Initialization
### seraph([server|options])
Creates and returns the Seraph instance. If no parameters are given,
assumes the Neo4J REST API is running locally at the default location
http://localhost:7474/db/data
.
Arguments
options
(default={ server: "http://localhost:7474", endpoint: "/db/data" }
- server
is protocol and authority part of Neo4J REST API URI, and endpoint
should be the path segment of the URI.server
(string) - Short form to specify server parameter only. "http://localhorse:4747"
is equivalent to { server: "http://localhorse:4747" }
.
Example
var dbLocal = require("seraph")();
var dbRemote = require("seraph")({ server: "http://example.com:53280",
endpoint: "/neo" });
dbRemote.read({ id: 13 }, function(err, node) {
if (err) throw err;
delete node.id;
dbLocal.save(node, function(err, nodeL) {
if (err) throw err;
console.log("Copied remote node#13 to " +
"local node#" + nodeL.id.toString() + ".");
});
});
Generic Operations
query(query, [params,] callback), rawQuery(query, [params,] callback)
rawQuery
performs a cypher query and returns the results directly from the
REST API.
query
performs a cypher query and map the columns and results together.
Note: if you're performing large queries it may be advantageous to use
queryRaw
, since query
attempts to infer whole nodes and relationships that
are returned (in order to transform them into a nicer format).
Arguments
query
- Cypher query as a format string.params
(optional, default={}
). Replace {key}
parts in query string. See
cypher documentation for details. note that if you want to send a list of
ids as a parameter, you should send them as an array, rather than a string
representing them ([2,3]
rather than "2,3"
).callback
- (err, result). Result is an array of objects.
Example
Given database:
{ name: 'Jon', age: 23, id: 1 }
{ name: 'Neil', age: 60, id: 2 }
{ name: 'Katie', age: 29, id: 3 }
Return all people Jon knows:
var cypher = "START x = node({id}) "
+ "MATCH x -[r]-> n "
+ "RETURN n "
+ "ORDER BY n.name";
db.query(cypher, {id: 1}, function(err, result) {
if (err) throw err;
assert.deepEqual(result, [
{ name: 'Katie', age: 29, id: 3 },
{ name: 'Neil', age: 60, id: 2 }
]);
};
db.rawQuery(cypher, {id: 3}, function(err, result) {
if (err) throw err;
})
### operation(path, [method='get/post'], [data])
Create an operation object that will be passed to call.
Arguments
path
- the path fragment of the request URL with no leading slash.method
(optional, default='GET'
|'POST'
) - the HTTP method to use. When
data
is an object, method
defaults to 'POST'. Otherwise, method
defaults to GET
.data
(optional) - an object to send to the server with the request.
Example
var operation = db.operation('node/4285/properties', 'PUT', { name: 'Jon' });
db.call(operation, function(err) {
if (!err) console.log('Set `name` to `Jon` on node 4285!')
});
### call(operation, callback)
Perform an HTTP request to the server.
If the body is some JSON, it is parsed and passed to the callback.If the status
code is not in the 200's, an error is passed to the callback.
Arguments
operation
- an operation created by operation that specifies
what to request from the servercallback
- function(err, result, response). result
is the JSON parsed body
from the server (otherwise empty). response
is the response object from the
request.
Example
var operation = db.operation('node/4285/properties');
db.call(operation, function(err, properties) {
if (err) throw err;
});
### Batching/transactions - `batch([operations, callback])`
Batching provides a method of performing a series of operations atomically. You
could also call it a transaction. It has the added benefit of being performed
all in a single call to the neo4j api, which theoretically should result in
improved performance when performing more than one operation at the same time.
When you create a batch, you're given a new seraph
object to use. All calls to
this object will be added to the batch. Note that once a batch is committed, you
should no longer use this object.
How do I use it?
There's two ways. You can do the whole thing asynchronously, and commit the
transaction whenever you want, or you can do it synchronously, and have the
transaction committed for you as soon as your function is finished running.
Here's a couple of examples of performing the same operations with batch
synchronously and asynchronously:
Asynchronously
var txn = db.batch();
txn.save({ title: 'Kaikki Askeleet' });
txn.save({ title: 'Sinä Nukut Siinä' });
txn.save({ title: 'Pohjanmaa' });
txn.commit(function(err, results) {
});
Synchronously
Note - it's only the creation of operations that is synchronous. The actual
API call is asynchronous still, of course.
db.batch(function(txn) {
txn.save({ title: 'Kaikki Askeleet' });
txn.save({ title: 'Sinä Nukut Siinä' });
txn.save({ title: 'Pohjanmaa' });
}, function(err, results) {
});
What happens to my callbacks?
You can still pass callbacks to operations on a batch transaction. They will
perform as you expect, but they will not be called until after the batch has
been committed. Here's an example of using callbacks as normal:
var txn = db.batch();
txn.save({ title: 'Marmoritaivas' }, function(err, node) {
});
txn.commit();
Can I reference newly created nodes?
Yes! Calling, for example, node.save
will synchronously return a special object
which you can use to refer to that newly created node within the batch.
For example, this is perfectly valid in the context of a batch transaction:
var txn = db.batch();
var singer = txn.save({name: 'Johanna Kurkela'});
var album = txn.save({title: 'Kauriinsilmät', year: 2008});
var performance = txn.relate(singer, 'performs_on', album, {role: 'Primary Artist'});
txn.rel.index('performances', performance, 'year', '2008');
txn.commit(function(err, results) {});
I didn't use any callbacks. How can I find my results when the batch is done?
Each function you call on the batch object will return a special object that you
can use to refer to that call's results once that batch is finished (in
addition to the intra-batch referencing feature mentioned above). The best
way to demonstrate this is by example:
var txn = db.batch();
var album = txn.save({title: 'Hetki Hiljaa'});
var songs = txn.save([
{ title: 'Olen Sinussa', length: 248 },
{ title: 'Juurrun Tähän Ikävään', length: 271 }
]);
txn.relate(album, 'has_song', songs[0], { trackNumber: 1 });
txn.relate(album, 'has_song', songs[1], { trackNumber: 3 });
txn.commit(function(err, results) {
var album = results[album];
var tracks = results[songs];
});
What happens if one of the operations fails?
Then no changes are made. Neo4j's batch transactions are atomic, so if one
operation fails, then no changes to the database are made. Neo4j's own
documentation has the following to say:
This service is transactional. If any of the operations performed fails
(returns a non-2xx HTTP status code), the transaction will be rolled back and
all changes will be undone.
Can I nest batches?
No, as of now we don't support nesting batches as it tends to confuse the
intra-batch referencing functionality. To enforce this, you'll find that the
seraph-like object returned by db.batch()
has no .batch
function itself.
How can I tell if this db
object is a batch operation?
Like so:
var txn = db.batch();
if (txn.isBatch)
Node Operations
### save(object, [key, value,] callback)
*Aliases: __node.save__*
Create or update a node. If object
has an id property, the node with that id
is updated. Otherwise, a new node is created. Returns the newly created/updated
node to the callback.
Arguments
node
- an object to create or updatekey
, value
(optional) - a property key and a value to update it with. This
allows you to only update a single property of the node, without touching any
others. If key
is specified, value
must also be.callback
- function(err, node). node
is the newly saved or updated node. If
a create was performed, node
will now have an id property. The returned
object is not the same reference as the passed object (the passed object will
never be altered).
Example
db.save({ name: 'Jon', age: 22, likes: 'Beer' }, function(err, node) {
console.log(node);
delete node.likes;
node.age++;
db.save(node, function(err, node) {
console.log(node);
})
})
### node.saveUnique(node, index, key, value, [returnExistingOnConflict = false,] callback)
Save a node, using an index to enforce uniqueness.
See also node.index.saveUniqueOrFail &
node.index.getOrSaveUnique.
Arguments
node
- an object to create or updateindex
- the index in which key
and value
are relevantkey
- the key under which to index this node and enforce uniquenessvalue
- the value under which to index this node and enforce uniquenessreturnExistingOnConflict
(optional, default=false
) - what to do when there is
a conflict (when the index you specified already refers to a node). If set to
true
, the node that the index currently refers to is returned. Otherwise,
an error is return indicating that there was a conflict (you can check this by
testing err.statusCode == 409
.callback
- function(err, node) - node
is the newly created node or the node
that was in the specified index, depending on returnExistingOnConflict
.
Example
db.saveUnique({name: 'jon'}, 'people', 'name', 'jon', function(err, node) {
db.saveUnique({age: 24}, 'people', 'name', 'jon', function(err, node) {
});
db.saveUnique({location:'Bergen'}, 'people', 'name', 'jon', true, function(err, node) {
});
});
### read(id|object, callback)
*Aliases: __node.read__*
Read a node.
Note: If the node doesn't exist, Neo4j will return an exception. You can
check if this is indicating that your node doesn't exist because
err.statusCode
will equal 404
. This is inconsistent with behaviour of
node.index.read, but it is justified because the Neo4j REST api
behaviour is inconsistent in this way as well.
Arguments
id | object
- either the id of the node to read, or an object containing an id
property of the node to read.callback
- function(err, node). node
is an object containing the properties
of the node with the given id.
Example
db.save({ make: 'Citroen', model: 'DS4' }, function(err, node) {
db.read(node.id, function(err, node) {
console.log(node)
})
})
### delete(id|object, [force], [callback])
*Aliases: __node.delete__*
Delete a node.
Arguments
id | object
- either the id of the node to delete, or an object containing an id
property of the node to delete.force
- if truthy, will delete all the node's relations prior to deleting the node.callback
- function(err). if err
is falsy, the node has been deleted.
Example
db.save({ name: 'Jon' }, function(err, node) {
db.delete(node, function(err) {
if (!err) console.log('Jon has been deleted!');
})
})
### find(predicate, [any, [start,]] callback)
*Aliases: __node.find__*
Perform a query based on a predicate. The predicate is translated to a
cypher query.
Arguments
Example
Given database content:
{ name: 'Jon' , age: 23, australian: true }
{ name: 'Neil' , age: 60, australian: true }
{ name: 'Belinda', age: 26, australian: false }
{ name: 'Katie' , age: 29, australian: true }
Retrieve all australians:
var predicate = { australian: true };
var people = db.find(predicate, function (err, objs) {
if (err) throw err;
assert.equals(3, people.length);
};
### relationships(id|object, [direction, [type,]] callback)
**Aliases: __node.relationships__*
Read the relationships involving the specified node.
Arguments
id | object
- either the id of a node, or an object containing an id property of
a node.direction
('all'|'in'|'out') (optional unless type
is passed,
default='all'
) - the direction of relationships to read.type
(optional, default=''
(match all relationships)) - the relationship
type to findcallback
- function(err, relationships) - relationships
is an array of the
matching relationships
Example
db.relationships(452, 'out', 'knows', function(err, relationships) {
})
Relationship Operations
### rel.create(firstId|firstObj, type, secondId|secondobj, [properties], callback)
*Aliases: __relate__, __node.relate__*
Create a relationship between two nodes.
Arguments
firstId | firstObject
- id of the start node or an object with an id property
for the start nodetype
- the name of the relationshipsecondId | secondObject
- id of the end node or an object with an id property
for the end nodeproperties
(optional, default={}
) - properties of the relationshipcallback
- function(err, relationship) - relationship
is the newly created
relationship
Example
db.relate(1, 'knows', 2, { for: '2 months' }, function(err, relationship) {
assert.deepEqual(relationship, {
start: 1,
end: 2,
type: 'knows',
properties: { for: '2 months' },
id: 1
});
});
### rel.createUnique(firstId|firstObj, type, secondId|secondObj, [properties,] index, key, value, [returnExistingOnConflict = false,] callback)
Create a relationship between two nodes, using an index to enforce uniqueness.
See also rel.index.saveUniqueOrFail &
rel.index.getOrSaveUnique.
Arguments
firstId | firstObject
- id of the start node or an object with an id property
for the start nodetype
- the name of the relationshipsecondId | secondObject
- id of the end node or an object with an id property
for the end nodeproperties
(optional, default={}
) - properties of the relationshipindex
- the index in which key
and value
are relevantkey
- the key under which to index this relationship and enforce uniquenessvalue
- the value under which to index this relationship and enforce uniquenessreturnExistingOnConflict
(optional, default=false
) - what to do when there is
a conflict (when the index you specified already refers to a relationship). If
set to true
, the relationship that the index currently refers to is returned.
Otherwise, an error is return indicating that there was a conflict (you can
check this by testing err.statusCode == 409
.callback
- function(err, relationship) - relationship
is the newly created
relationship or the relationship that was in the specified index, depending
on returnExistingOnConflict
Example
db.rel.createUnique(1, 'knows', 2, 'friendships', 'type', 'super', function(err, rel) {
db.rel.createUnique(1, 'knows', 2, 'friendships', 'type', 'super', function(err, node) {
});
db.rel.createUnique(1, 'knows', 2, 'friendships', 'type', 'super', true, function(err, node) {
});
});
### rel.update(relationship, [key, value,] callback)
Update the properties of a relationship. Note that you cannot use this
method to update the base properties of the relationship (start, end, type) -
in order to do that you'll need to delete the old relationship and create a new
one.
Arguments
relationship
- the relationship object with some changed propertieskey
, value
(optional) - if a key and value is specified, only the property with
that key will be updated. the rest of the object will not be touched.callback
- function(err). if err is falsy, the update succeeded.
Example
var props = { for: '2 months', location: 'Bergen' };
db.rel.create(1, 'knows', 2, props, function(err, relationship) {
delete relationship.properties.location;
relationship.properties.for = '3 months';
db.rel.update(relationship, function(err) {
});
});
### rel.read(object|id, callback)
Read a relationship.
Arguments
object | id
- the id of the relationship to read or an object with an id
property of the relationship to read.callback
- function(err, relationship). relationship
is an object
representing the read relationship.
Example
db.rel.create(1, 'knows', 2, { for: '2 months' }, function(err, newRelationship) {
db.rel.read(newRelationship.id, function(err, readRelationship) {
assert.deepEqual(newRelationship, readRelationship);
assert.deepEqual(readRelationship, {
start: 1,
end: 2,
type: 'knows',
id: 1,
properties: { for: '2 months' }
});
});
});
### rel.delete(object|id, [callback])
Delete a relationship.
Arguments
object | id
- the id of the relationship to delete or an object with an id
property of the relationship to delete.callback
- function(err). If err
is falsy, the relationship has been
deleted.
Example
db.rel.create(1, 'knows', 2, { for: '2 months' }, function(err, rel) {
db.rel.delete(rel.id, function(err) {
if (!err) console.log("Relationship was deleted");
});
});
Index Operations
### node.index.create(name, [config,] callback)
### rel.index.create(name, [config,] callback)
Create a new index. If you're using the default index configuration, this
method is not necessary - you can just start using the index with
index.add as if it already existed.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
name
- the name of the index that is being createdconfig
(optional, default={}
) - the configuration of the index. See the neo4j docs
for more information.callback
- function(err). If err
is falsy, the index has been created.
Example
var indexConfig = { type: 'fulltext', provider: 'lucene' };
db.node.index.create('a_fulltext_index', indexConfig, function(err) {
if (!err) console.log('a fulltext index has been created!');
});
### node.index.add(indexName, id|object, key, value, callback);
### rel.index.add(indexName, id|object, key, value, callback);
*`node.index.add` is aliased as __node.index__ & __index__*
Add a node/relationship to an index.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
indexName
- the name of the index to add the node/relationship to.id | object
- the id of the node/relationship to add to the index or an object
with an id property of the node/relationship to add to the index.key
- the key to index the node/relationship withvalue
- the value to index the node/relationship withcallback
- function(err). If err
is falsy, the node/relationship has
been indexed.
Example
db.save({ name: 'Jon', }, function(err, node) {
db.index('people', node, 'name', node.name, function(err) {
if (!err) console.log('Jon has been indexed!');
});
});
### node.index.read(indexName, key, value, callback);
### rel.index.read(indexName, key, value, callback);
Read the object(s) from an index that match a key-value pair. See also
index.readAsList.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
indexName
- the index to read fromkey
- the key to matchvalue
- the value to matchcallback
- function(err, results). results
is a node or relationship object
(or an array of them if there was more than one) that matched the given
key-value pair in the given index. If nothing matched, results === false
.
index.readAsList is similar, but always gives results
as
an array, with zero, one or more elements.
Example
db.rel.index.read('friendships', 'location', 'Norway', function(err, rels) {
});
### node.index.readAsList(indexName, key, value, callback);
### rel.index.readAsList(indexName, key, value, callback);
Read the object(s) from an index that match a key-value pair. See also
index.read.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
indexName
- the index to read fromkey
- the key to matchvalue
- the value to matchcallback
- function(err, results). results
is an array of node or
relationship objects that matched the given key-value pair in the given index.
index.read is similar, but gives results
as false
, an object
or an array of objects depending on the number of hits.
Example
db.rel.index.readAsList('friendships', 'location', 'Norway', function(err, rels) {
});
### node.index.remove(indexName, id|object, [key, [value,]] callback);
### rel.index.remove(indexName, id|object, [key, [value,]] callback);
Remove a node/relationship from an index.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
indexName
- the index to remove the node/relationship from.id | object
- the id of the node/relationship to remove from the index or an
object with an id property of the node/relationship to remove from the index.key
(optional) - the key from which to remove the node/relationship. If none
is specified, every reference to the node/relationship is deleted from the
index.value
(optional) - the value from which to remove the node/relationship. If
none is specified, every reference to the node/relationship is deleted for the
given key.callback
- function(err). If err
is falsy, the specified references have
been removed.
Example
db.node.index.remove('people', 6821, function(err) {
if (!err) console.log("Every reference of node 6821 has been removed from the people index");
});
db.rel.index.remove('friendships', 351, 'in', 'Australia', function(err) {
if (!err) console.log("Relationship 351 is no longer indexed as a friendship in Australia");
})
### node.index.delete(name, callback);
### rel.index.delete(name, callback);
Delete an index.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments
name
- the name of the index to deletecallback
- function(err). if err
is falsy, the index has been deleted.
Example
db.rel.index.delete('friendships', function(err) {
if (!err) console.log('The `friendships` index has been deleted');
})
### node.index.getOrSaveUnique(node, index, key, value, callback);
### rel.index.getOrSaveUnique(startNode, relName, endNode, [properties,] index, key, value, callback);
Save a node or relationship, using an index to enforce uniqueness. If there is
already a node or relationship saved under the specified key
and value
in
the specified index
, that node or relationship will be returned.
Note that you cannot use this function to update nodes.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments (node)
node
- the node to saveindex
- the name of the index in which key
and value
are relevantkey
- the key to check or store undervalue
- the value to check or store undercallback
- function(err, node) - returns your saved node, or the node that
was referenced by the specified key
and value
if one already existed.
Arguments (relationship)
startNode
- the start point of the relationship (object containing id or id)relName
- the name of the relationship to createendNode
- the end point of the relationship (object containing id or id)properties
(optional) - an object containing properties to store on the
created relationship.index
- the name of the index in which key
and value
are relevantkey
- the key to check or store undervalue
- the value to check or store undercallback
- function(err, rel) - returns your created relationship, or the
relationship that was referenced by the specified key
and value
if one
already existed.
Example
var tag = { name: 'finnish' };
db.node.index.getOrSaveUnique(tag, 'tags', 'name', tag.name, function(err, tag) {
db.node.index.getOrSaveUnique({name: 'finnish'}, 'tags', 'name', 'finnish', function(err, newTag) {
});
});
### node.index.saveUniqueOrFail(node, index, key, value, callback);
### rel.index.saveUniqueOrFail(startNode, relName, endNode, [properties,] index, key, value, callback);
Save a node or relationship, using an index to enforce uniqueness. If there is
already a node or relationship saved under the specified key
and value
in
the specified index
, an error is returned indicating that there as a conflict.
You can check if the result was a conflict by checking if
err.statusCode == 409
.
NOTE for index functions: there are two different types on index in neo4j -
node indexes and relationship indexes. When you're working with node
indexes, you use the functions on node.index
. Similarly, when you're working
on relationship indexes you use the functions on rel.index
. Most of the
functions on both of these are identical (excluding the uniqueness functions),
but one acts upon node indexes, and the other upon relationship indexes.
Arguments (node)
node
- the node to saveindex
- the name of the index in which key
and value
are relevantkey
- the key to check or store undervalue
- the value to check or store undercallback
- function(err, node) - returns your created node, or an err with
statusCode == 409
if a node already existed at that index
Arguments (relationship)
startNode
- the start point of the relationship (object containing id or id)relName
- the name of the relationship to createendNode
- the end point of the relationship (object containing id or id)properties
(optional) - an object containing properties to store on the
created relationship.index
- the name of the index in which key
and value
are relevantkey
- the key to check or store undervalue
- the value to check or store undercallback
- function(err, rel) - returns your created relationship, or an
err with statusCode == 409
if a relationship already existed at that index
Example
var tag = { name: 'finnish' };
db.node.index.saveUniqueOrFail(tag, 'tags', 'name', tag.name, function(err, tag) {
db.node.index.saveUniqueOrFail({name: 'finnish'}, 'tags', 'name', 'finnish', function(err, newTag) {
});
});
Development of Seraph is lovingly sponsored by
BRIK Tekonologier AS in Bergen, Norway.