qcypher
v0.3.3
Published
A q/promise based pure JS library for working with Neo4j.
Downloads
2
Maintainers
Readme
QCypher
A q/promise-based pure JS library for working with Neo4j
QCypher uses the Neo4j REST endpoints to issue HTTP requests to the graph database. Asynchronous queries return Q promises in order to support query chaining.
The library itself aims to be the thinnest possible layer between NodeJS and Neo4j, while allowing that interface to remain dead simple. For more involved usages, QCypher has a set of transaction query functions. Use those functions when more than simple chaining is required.
Install via NPM
QCypher may be installed via NPM.
$ npm install qcypher
Using QCypher
Using QCypher consists of requiring the module, initializing the module with the path to the graph database and issuing one or more query calls. When querying a local database the qcypher.init() call is optional, but using it is good form.
Queries are written in the Cypher query language to interact with the graph.
Earlier versions of QCypher only allowed you to import and use the module. This meant that you could only use one Neo4j database at one time. You must now require qcypher which returns a constructor function, then you can create an instance for use as before.
var QCypher = require('qcypher') , qcypher = new QCypher();
var QCypher = require('qcypher')
, qcypher = new QCypher()
, q = require('q');
qcypher.init('http://localhost:7474');
qcypher.query("CREATE (s:Student {name:{student}.name, grade:{student}.grade}) RETURN s", {
student: {
name: "Scott Riggs",
grade: 12
}
});
To retrieve data from the graph:
qcypher.query("MATCH (s:Student {name:{student}.name}) RETURN s", {
student: {
name: "Scott Riggs"
}
})
.then(function(result) {
var student = result.data[0][0].data;
console.log('student', student);
})
NOTE: QCypher uses Cypher query parameters. For more information see: Neo4j Cypher Parameters
Error Handling
All QCypher functions add a status object to the results it returns.
status: {
statusCode: '201',
httpCode: '201',
httpMessage: 'Created',
httpDescription: 'Resource created'
}
The following common errors are tracked:
httpCode | httpMessage | httpDescription ----------- | ---------------------- | ------------------------------------ 200 | OK | Request succeeded without error 201 | Created | Resource created 400 | Bad Request | Request is invalid, missing parameters? 401 | Unauthorized | User isn't authorized to access this resource 402 | Request Failed | Parameters are valid but request still failed 404 | Not Found | The requested resource was not found on the server 429 | Too Many Request | Too many requests issue within a period 500 | Server Error | An error occurred on the server 501 | Method Not Implemented | The requested method / resource isn't implemented on the server 503 | Service Unavailable | The server is currently unable to handle the request due to a temporary overloading or maintenance of the server. The implication is that this is a temporary condition which will be alleviated after some delay
Look at the result’s status object for request status:
IMPORTANT: httpCode
less than 300 is returned when the Q promise is resolved, 300 and greater are treated as errors and returned via reject.
qcypher.query('SERGE (n:Node name: "Test") RETURN n', {})
.then(function resolve(result) {
console.log(‘Success’, result.status.httpCode);
}, function reject(result) {
console.log(‘Failure’, result.status.httpCode);
});
In the example above SERGE
is an invalid Cypher command (should have been MERGE
) and so the example returns a 400 error indicating that we sent a Bad Request
.
Handling query errors
The following query is syntactically invalid and will cause Neo4j to return an error containing an exception and stack trace. This is valuable in helping you determine the cause of your error.
qcypher.query('MERGE (n:QCypher name: "first") RETURN n', {})
.then(function resolve(result) {
expect(false).toBeTrue(); // this should not happen
done();
}, function reject(result) {
expect(error.exception).toBeDefined();
done();
});
In the example above note that the Q.then handler accepts two functions, a resolve and reject function. The functions are named in the example above for clarity. Another way to write this is:
function resolve(result) {
expect(false).toBeTrue(); // this should not happen
done();
}
function reject(result) {
expect(error.exception).toBeDefined();
done();
}
qcypher.query('MERGE (n:QCypher name: "first") RETURN n', {})
.then(resolve, reject);
When qCypher detects a Neo4j error it rejects its promise allowing you to capture the results in the reject handler as shown above.
You can console log the error using console.log('error', JSON.stringify(error));
Here we see clues to the cause of the problem. Note, that the information returned in the message
is the same returned in the Cypher web browser console.
{
"message": "Invalid input 'n': expected whitespace, comment, NodeLabel, MapLiteral, a parameter, ')' or a relationship pattern (line 1, column 18)\n\"MERGE (n:QCypher name: \"first\") RETURN n\"\n ^",
"exception": "SyntaxException",
"fullname": "org.neo4j.cypher.SyntaxException",
"stacktrace": [
"org.neo4j.cypher.internal.compiler.v2_1.parser.CypherParser$$anonfun$parse$1.apply(CypherParser.scala:58)",
"org.neo4j.cypher.internal.compiler.v2_1.parser.CypherParser$$anonfun$parse$1.apply(CypherParser.scala:48)",
"scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)",
"scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)",
"scala.collection.immutable.List.foreach(List.scala:318)",
"scala.collection.TraversableLike$class.map(TraversableLike.scala:244)",
"scala.collection.AbstractTraversable.map(Traversable.scala:105)",
"org.neo4j.cypher.internal.compiler.v2_1.parser.CypherParser.parse(CypherParser.scala:47)",
"org.neo4j.cypher.internal.compiler.v2_1.CypherCompiler.isPeriodicCommit(CypherCompiler.scala:133)",
"org.neo4j.cypher.internal.CypherCompiler.isPeriodicCommit(CypherCompiler.scala:91)",
"org.neo4j.cypher.internal.ServerExecutionEngine.isPeriodicCommit(ServerExecutionEngine.scala:34)",
"org.neo4j.cypher.javacompat.internal.ServerExecutionEngine.isPeriodicCommitQuery(ServerExecutionEngine.java:56)",
"org.neo4j.server.rest.web.CypherService.cypher(CypherService.java:99)",
"java.lang.reflect.Method.invoke(Method.java:606)",
"org.neo4j.server.rest.transactional.TransactionalRequestDispatcher.dispatch(TransactionalRequestDispatcher.java:139)",
"java.lang.Thread.run(Thread.java:744)"
]
}
Handling single return values
When you specify a query which returns a single value, object or row of data, you can use the getSimpleData
helper function. It accepts a result object and returns an object containing the value returned from the query.
For example:
This query returns a single value id
MATCH (u:User {id: "0e87e623-f358-4e00-93ff-cc77dfd99b48"}) RETURN u.id;
This query also returns a single value, it just happens to be a single object"
MATCH (u:User {id: "0e87e623-f358-4e00-93ff-cc77dfd99b48"})
RETURN {"id: u.id, name": u.name, age: u.age};
The getSimpleData
can be used with each of the example above. And it will work with this next example.
MATCH (u:User {id: "0e87e623-f358-4e00-93ff-cc77dfd99b48"})
RETURN u;
However, it shouldn't be used when there are multiple rows returned by Neo4j Cypher.
View the tests in the project’s spec folder for other examples.
Transactions
For more complex queries and use cases, QCypher supports transactions.
Neo4j transaction support is outlined here: http://docs.neo4j.org/chunked/stable/rest-api-transactional.html
Note: QCypher does NOT support batch operations as defined here: http://docs.neo4j.org/chunked/stable/rest-api-batch-ops.html
Only transactions are supported.
Working with Transactions
You can create a transaction using transCreate
and execute one or more cypher statements using transExecute
. If you recieve an error and wish to abort a transaction you can do so with transRollback
.
Open transactions time out after a fixed amount of time, determined by the Neo4j database. If you're preforming a lengthy operation you can extend the life of an open transaction using transResetTimeout
.
Finally, once you're done with a transaction you can commit it using transCommit
.
var transobj = qcypher.transCreate();
var query = "MERGE (n:TNode {id:{value}}) RETURN n;";
var promise = qcypher.transExecute(transobj, [
{
"statement": query,
"parameters": {
"value": 1
}
},
{
"statement": query,
"parameters": {
"value": 2
}
},
{
"statement": query,
"parameters": {
"value": 3
}
}
]);
promise.then(function resolve(result) {
if (result.errors.length > 0) {
qcypher.transRollback();
} else {
qcypher.transCommit(transobj);
}
},
function reject(result) {
// no need to call qcypher.transRollback() because transaction was rejected.
}
);
Robust handling
When working with transactions we're sending an array of statements to be processed as a single transaction. So it's important to examine the results object for errors. That is, one or more of the statements you're sending for execution may have failed.
The results.errors member variable is an array which may contain details on one or more failures. If empty then there are no errors.
Often in transaction based processing if one statement fails we typically want to rollback the entire transaction. Because the exact action you take based on a transaction is application dependent (that is, up to you!), QCypher doesn't automatically do this for you. You're expected to analyze the results of a transaction exception and decide whether you want to commit or rollback.
Another reason why QCypher doesn't automatically commit a transaction is because it doesn't know when you're done with it. You can, for example, keep a transaction open and perform multiple cypher.transExecute
over a period of time.
However, QCypher has a helper function which will rollback or commit based on whether a transaction is completely successful. qcypher.transSingleExecute
will execute a single transaction and resolve or reject based on whether there were any errors. It will also commit or rollback automatically. It's only useful when you want to execute a single batch of statements and expect to rollback if any statement in the transaction fails.
Query statement builder
When working with queries it's important to build templates that can have parameters applied to them. This allows the Neo4j engine to cache queries.
An example of this can be seen in the following statements. The student's name is applied to the query string as {student}.name
qcypher.query("MATCH (s:Student {name:{student}.name}) RETURN s", {
student: {
name: "Scott Riggs"
}
})
This works really well but we can't use this method to change other parts of the query. For example, consider this:
var queryTemplate = 'MATCH (u:User {userID: {params}.userID}), (e:Events {name: "Events"}) ' +
'CREATE ' +
'(u)-[:SIGNIN_EVENT {ts: {params}.ts}]->(e) ' +
'RETURN true;';
We can use that query template to apply the userID and event timestamp. However we can't generalize the query by replacing the SIGNIN_EVENT relationship. That is, we can't use: {params}.eventType
.
This is where the queryStatementBuilder
function becomes useful. Let's change the last example a bit:
var queryTemplate = 'MATCH (u:User {userID: {params}.userID}), (e:Events {name: "Events"}) ' +
'CREATE ' +
'(u)-[:<%=eventType%> {ts: {params}.ts}]->(e) ' +
'RETURN true;';
By naming a template parameters using <%=
preceding an identifier and following with a closing %>
we can describe parameters that apply to templates.
Here's the full example:
var queryTemplate = 'MATCH (u:User {userID: {params}.userID}), (e:Events {name: "Events"}) ' +
'CREATE ' +
'(u)-[:<%=eventType%> {ts: {params}.ts}]->(e) ' +
'RETURN true;';
var query = qcypher.queryStatementBuilder(queryTemplate, {
eventType: 'JOINED_EVENT'
});
The resulting query variable can then be used with query
and transXXX
calls.
Tests
QCypher has a suite of tests in the /spec
folder. In order to run the tests neo4j must be running and jasmine-node must be installed.
The test suite requires jasmine-node to be installed globally. If you don't have it installed run:
$ npm install -g jasmine-node
Run the tests using:
Warning
: these tests will flush your local database!
$ npm test
You can run an individual test using:
$ jasmine-node spec/transaction-spec.js —-verbose