One of my closed-source projects has an API called Webb (after the James Webb Space Telescope) that interfaces a graphing database called Neo4j. It’s the brain 🧠of the project.
Unlock more value from your data by freely relating it. As different as your models may be, Neo4J makes it easy to associate them by drawing relationships between them using cypher.
When it comes to interfacing in NodeJS, use the neo4j-driver.
The following notes focus on the reactive approach using RxJS and Reactive Sessions.
Queries are performed in Transactions.
The neo4j-driver package’s sessions support 3 types of transactions.
The Read and Write transaction types are preferred, for if there is a transient error within the driver, the driver will automatically retry the transaction, unlike auto-commit.
Auto-commit transactions are immediately executed against the DBMS and acknowledged immediately. These are done through the run() method on the session object, ie:
// run query in auto-commit transactionrxSession.run('MATCH (m)-[r]->(n) RETURN m,n,r');
Important! The driver will not reattempt the query upon any errors with an auto-commit transaction. Therefore, it shouldn’t be used in
production
, but perhaps in one-off queries like seeding initial data.
Use the executeRead() method to provide a single parameter, a transaction callback method, that gives you a transaction object to perform the query via the read transaction’s run()
method, ie:
// Get people's names who like the dessert: ice-creamrxSession.executeRead((tx) =>tx.run(`MATCH (p:Person)-[:LIKES]->(d:Dessert)WHERE d.name = $namereturn p.name as nameLIMIT 10`,{ name: 'ice-cream' }).records() // returns RxJS Observable.pipe(map((record) => record.get('name')),toArray(), // wait for query completioncatchError((e) => throwError(() => e))));
Use the executeWrite() method to provide a transaction callback method that gives you a transaction object to perform a write query that has built-in cluster support (goes to leader for leader to sync with followers), ie:
// Add Michael as a PersonrxSession.executeWrite((tx) =>tx.run(`MERGE (p:Person {name: $name})`, { name: 'Michael' }) // $name is replaced with the corresponding key's value.records().pipe(last(), // wait for query to completemap((record) => record.get('name')),catchError((e) => throwError(() => e))));
It’s important to close a transaction’s session after the transaction’s query has completed:
return concat(// combine the two observables transaction's records() and session's close()rxSession.executeRead((txc) =>txc.run('MATCH (signal:Signal {url: $url}) RETURN signal LIMIT 1', {url,}).records().pipe(last(), // make sure the query has completed before mapping the resultmap((record) => record.get('signal')),catchError((e) => throwError(() => e)))),rxSession.close());
For adding constaints, create them via cypher:
CREATE CONSTRAINT personIdConstraint FOR (person:Person) REQUIRE person.id IS UNIQUE
Learn more here.
Learn more here.
IF NOT EXISTS
makes a query idempotent, therefore no error will be thrown if you attempt to create the same index twice.
There are functions available within Cypher queries to make it easier to write queries. Predicate functions, Scalar functions ie creating a UUID, and more.
Becareful! This will delete all nodes and relationships in the database that you run this query in, you’ve been warned!!
MATCH (n)DETACH DELETE n
Helpful when developing, expirmenting with new things, trying out new Models, etc.
Use the int()
function from neo4j-driver
to convert the values in a query into Neo4j integers since the Java Integer type and the Javascript Integer type support different range of number sizes. To get around this, anything too big for Javascript is casted into a string
.
import { int } from 'neo4j-driver';const res = await session.readTransaction((tx) =>tx.run(`MATCH (m:Movie)SKIP $skipLIMIT $limit`,{ skip: int(skip), limit: int(limit) }));
import { toNativeTypes } from 'neo4j-driver';const movies = res.records.map((row) => toNativeTypes(row.get('movie')));