neo-forte
TypeScript icon, indicating that this package has built-in type declarations

2.4.0 • Public • Published

neo-forte

A simple querying interface for a neo4j database, requiring only knowledge of cypher.

neo-forte

codecov

Why

Running cypher in code should be just as simple as in the data browser!

What

A few functions that allow anyone who knows cypher to run a query. neo-forte uses the neo4j driver to run your queries and returns simple jsons.

Queries automatically run using the preferred practice of transaction functions.

You can open a session with a simple call to getSession().

There are two functions for running queries that return jsons:

  • run() returns an array of objects
  • oneRecord() returns a single object.

The types Session and Driver from the current version of the neo4j-driver-core are also exposted so that you can hard-type them in your code.

Advantages

  1. You usually don't need to declare a driver or specify credentials for a session. For instance:

     const neo4j = require('neo4j-driver')
    
     const uri = process.env.URI;
     const user = process.env.USER_NAME
     const password = process.env.PASSWORD
    
     const driver = neo4j.driver(uri, neo4j.auth.basic(user, password))
     const session = driver.session()

    becomes

     const { getSession } = require('neo-forte')
     const session = getSession()
  2. Running queries is as simple as in the browser. In place of:

    const result = await session.writeTransaction(tx =>
        tx.run(queryString, params)

    simply call

    const result = await run(session, queryString, params)
  3. Records returned are simple jsons:

      result.records[0].get('name')

    becomes:

    • result[0].name with run()
    • result.name with oneRecord().

At times, you may need to supplement neo-forte with neo4j-driver session methods (see Limitations). But to start you do not need to learn about the neo4j driver.

Usage

[1] Install the package:

npm i neo-forte

[2] Create a file .env at the project root level with these three variables (or add them to your existing .env file):

  • DB_URI
  • DB_USER
  • DB_PASSWORD

Those are the credentials that you use to log into the data browser: sample browser login session

Optionally, you can also use DB_PASSWORD if you want to have a session with a particular database.

You can just copy over the .env.sample file to .env and update the values there.

[3] Use the following functions, all defined in the API section below:

  • getSession: returns a database session.

  • getSessionVerify: an async version that returns a database session after verifying the driver connection.

  • run: runs a query with params in a session. Returns an array of objects containing your data.

  • oneRecord: a special variation of run() that expects a single result and returns an object rather than an array.

    You can then access the results directly in your code. For example:

    import { getSession, run }  from 'neo-forte'
    
    const queryString =
        `match (movie:Movie)-[:ACTED_IN]-(actor:Person {name: $actor}) return movie.title as title`
    
    const params = {
        actor: 'Tom Hanks'
    }
    
    const session = getSession()
    
    const result = await run(
        session,
        queryString,
        params,
    )
    // in one sample database, the result is:
    // result = [{ title: "Forrest Gump" }, { title: "Big" }]

Transaction Types

Neo4j recommends running transactions for production queries. That adds a bit of complexity that neo-forte hides from you.

There are two types of transactions: read and write.

By default, neo-forte automatically chooses which one to run from your query. If a query string contains none of the updating clauses in cypher, then session.readTransaction is called. Otherwise, session.writeTransaction is called.

The list of updating clauses sought are:

  • CREATE
  • DELETE
  • DETACH
  • MERGE
  • REMOVE
  • SET

Usually, that works quite smoothly. But, if you happen to use any of these reserved strings in a query, then the query will be interpreted as an updating query, and writeTransaction will be used.

That can result in queries being misclassified in the rare case that a variable or name includes as a substring any element in the list above. For instance, the following query would be wrongly classified as an updating query:

match (ns:NumberSet {id:$nsId}) return ns 

The reason is that the node type "NumberSet" contains "Set" as a substring. In the event that a query is wrongly classified as updating, writeTransaction will be used instead of readTransaction. However, that should not affect query results. In the worst case, the query may run less efficiently if you are using a cluster.

That said, if you happen to have a query that you would expect to be wrongly classified as updating, you can change the transactionType parameter in run from the default TransactionType.Auto to TransactionType.Read. That option forces the query to be run within a readTransaction.

Passing Custom Transaction Functions

If you want to create your own custom function to pass into a transaction, then you need to call the session.readTransaction() or session.writeTransaction() methods built into neo4j-driver. Here's an example:

import { getSession }  from 'neo-forte'
const session = await getSession()

try {
  const relationshipsCreated = await session.writeTransaction(tx =>
    Promise.all(
      names.map(name =>
        tx
          .run(
            'MATCH (emp:Person {name: $person_name}) ' +
              'MERGE (com:Company {name: $company_name}) ' +
              'MERGE (emp)-[:WORKS_FOR]->(com)',
            { person_name: name, company_name: companyName }
          )
          .then(
            result =>
              result.summary.counters.updates().relationshipsCreated
          )
          .then(relationshipsCreated =>
            neo4j.int(relationshipsCreated).toInt()
          )
      )
    ).then(values => values.reduce((a, b) => a + b))
  )
} finally {
  await session.close()
}

See the driver documentation for more information.

API

This package fundamentally does two things:

  1. Creates a session
  2. Runs queries

Creating a Session

As suggested above, the simplest approach is to store these variables in your .env file:

  • DB_URI,
  • DB_USER,
  • DB_PASSWORD
  • [DB_DATABASE]

But you can also generate sessions for as other databases as needed.

DatabaseInfo type

The following types are exposed for your use:

interface DatabaseInfo {
  URI: string;
  USER: string;
  PASSWORD: string;
  DATABASE?: string;
}

Session //bfrom the driver core
Driver // from the driver core

So, for instance, you can hard-type an instance of a session as follows:

import { getSession }  from 'neo-forte'
const session: Session = await getSession()

getSession()

Returns a session synchronously:

function getSession(databaseInfo?: DatabaseInfo): Session

Takes an optional DatabaseInfo as its only parameter. If no value is passed for databaseInfo, here is what getSession does:

  1. If there are process.env variables, then they are used by default and a session is returned;
  2. If not, an error is thrown.

Here's a sample usage relying upon the .env file to provide the needed database info:

import { getSession } from 'neo-forte'

(()=> {
  import { Session } from "neo4j-driver-core"

  const session: Session = getSession()
  console.log(`session=${JSON.stringify(session, null, 2)}`)
})()

Here's a usage where databaseInfo is set manually:

import { Session } from "neo4j-driver-core"
import {DatabaseInfo, getSession} from 'neo-forte'

const databaseInfo:DatabaseInfo = {
  URI: 'neo4j+s://73ab4d76.databases.neo4j.io,
  USER: 'neo4j',
  PASSWORD: '7BxrLxO8Arbce3ffelddl2KJJK2Hyt08vPJ3lPQe60F',
}

(()=> {
  const session: Session = getSession(databaseInfo)
  console.log(`session=${JSON.stringify(session, null, 2)}`)
})()

getSessionVerify()

An async version of getSession that also verifies your connection. Everything else is the same.

async function getSessionVerify(databaseInfo?: DatabaseInfo)

Here's a sample usage:

import { getSessionVerify } from 'neo-forte'

(async ()=> {
  const session = await getSessionVerify()
  console.log(`session=${JSON.stringify(session, null, 2)}`)
})()

NOTE: getSessionVerify() will check the validity of your connection. If the test fails, you will receive an error.

Running Queries

There are two functions to run a query: run and oneRecord.

run

This function uses two exposed enums as parameter types:

enum Format {
    DataOnly,
    Complete,
}

enum TransactionType {
    Auto,
    Read,
    Write,
}

The function declaration looks like this:

async function run(
    session: Session,
    queryString: string,
    params: any,
    format: Format = Format.DataOnly,
    transactionType: TransactionType = TransactionType.Auto
)

run returns an array of objects. Simply have your query return specific needed fields or values, and they will appear as keys within the objects of the array.

NOTE For best results, do not return simple nodes. It is not a problem, they will be stored as jsons. But to get to their fields, you'll then need to access their properties.

If your query fails to execute, you will receive a clear error message, indicating the specific query and parameters that failed, and what the error was. For instance:

Error: the query set provided does not contain the given query:

query:
-----------------
MATCH (user:User {name:$name})-[:USES]->(resource:Resource {id:$resourceId})
RETURN user.id AS userId, user.name AS userName, resource.id AS resourceId
-----------------   
params: {"name":"Bruce","resourceId":"printer-XYX11aC42s"}

If you want to see the summary statistics, for instance to confirm that a node was deleted, you can set the optional parameter format to Format.Complete rather than the default Format.DataOnly. Doing so will return an object with two keys:

  • records: the records returned from the query
  • summary: a json of all of the summary statistics returned from the query.

You will probably not need to do so often.

The other optional parameter is transactionType. As is discussed in Transaction Types, by default the correct type is determined automatically. However, if you'd like to specify that the transaction type should be read or write, you may do so this way.

oneRecord

A second function, just for convenience, is:

export async function oneRecord(
    session: Session,
    queryString: string,
    params: any)

oneRecord returns an object with the requested fields. If more than one record is returned, oneRecord will return an error message.

If no records are returned, oneRecord returns null.

You cannot specify that you want to see a summary with oneRecord. For that, you must call run.

Limitations

There are use cases where you'll be best served to call the neo4j driver directly. The driver is complex for a reason--it's very versatile.

See in particular Passing Custom Transaction Functions.

Note that the session returned by getSession() is a fully functional neo4j driver session. So you can call any session method that you like as described in the driver documentation.

Relevant Package

This package actually uses neo-forgery. The two complement each other well. The goal of neo-forgery is to allow you to run unit tests by mocking the neo4j-driver easily.

Both pursue a shared mission: programming with neo4j2 should be really simple!


2 Or any third party service.

Contributing

If you share that vision 👍, please reach out with issues, or feel free to jump in and contribute!

Special Thanks

As with neo-forgery, a big thanks to Antonio Barcélos on the neo4j-driver team for his feedback on making this tool optimally useful.

Package Sidebar

Install

npm i neo-forte

Weekly Downloads

17

Version

2.4.0

License

MIT

Unpacked Size

27.8 kB

Total Files

25

Last publish

Collaborators

  • yisroel