The Neo4j Type System

At this point, we should take a look at the Cypher type system. As Neo4j is written in Java (the j in Neo4j stands for Java after all), there are some discrepancies between the types stored in the Neo4j database and native JavaScript types.

Some values like strings, floats, booleans, and nulls map directly to JavaScript types, but more complex types need special handling.

JavaScript Types to Neo4j Types
JavaScript Type Neo4j Cypher Type Notes

null, undefined

null

Array

List

Neo4j can only store a flat array containing strings, booleans or numbers.

Object

Map

Boolean

Boolean

Integer*

Integer

See Integers

Number

Float

String

String

Int8Array

ByteArray

Date

Date

See Temporal Types

Time

Time

See Temporal Types

LocalTime

LocalTime

See Temporal Types

DateTime

DateTime

See Temporal Types

LocalDateTime

LocalDateTime

See Temporal Types

Duration

Duration

See Temporal Types

neo4j.spatial.Point

Point

See Spatial Types

neo4j.spatial.CartesianPoint

Point (Cartesian)

See Spatial Types

neo4j.spatial.WGS84Point

Point (WGS-84)

See Spatial Types

Node

Node

See Nodes & Relationships

Relationship

Relationship

See Nodes & Relationships

Path

Path

See Nodes & Relationships

Let’s take a look at some of these types in more detail.

Integers

Due to a discrepancy between integers in JavaScript and the Neo4j type system, we need to take extra care when working with integers in the Neo4j JavaScript Driver.

The Neo4j type system uses 64-bit signed integer values (with a range of -(264- 1) and (263- 1)) while JavaScript can only safely represent integers between -(253- 1) (Number.MIN_SAFE_INTEGER) and 253- 1 (Number.MAX_SAFE_INTEGER).

To conform with the Neo4j type system, the driver will not automatically convert to JavaScript integers. Instead, the driver will convert integers to a float to ensure no data is lost.

The driver provides an Integer type and an int() function for sending and receiving integers with neo4j.

The Driver will convert any integer values it receives into an instance of an Integer class. The Integer class has a toNumber() method which can be called to safely convert the value back into a JavaScript number.

If the number is outside of the valid range, it will be returned as a string.

js
Working with Integers
// import { int, isInt } from 'neo4j-driver'

// Convert a JavaScript 'number' into a Neo4j Integer
const thisYear = int(2022)

// Check if a value is a Neo4j integer
console.log(isInt(thisYear)) // true

// Convert the Neo4j integer into a JavaScript number
console.log(thisYear.toNumber()) // 2022

We make use of these functions often throughout this course.

Nodes & Relationships

Nodes and Relationships are both returned as similar classes.

As an example, let’s take the following code snippet:

js
Return Nodes and Relationships
// Execute a query within a read transaction
const res = await session.executeRead(tx => tx.run(`
  MATCH path = (person:Person)-[actedIn:ACTED_IN]->(movie:Movie)
  RETURN path, person, actedIn, movie
  LIMIT 1
`))

// Get the first row
const row = res.records[0]

The query will return one row for each :Person and :Movie node with an :ACTED_IN relationship between them.

Nodes

We can retrieve the movie value using the .get() method on the row.

js
const movie = row.get('movie')

The value assigned to the person variable will be the instance of a Node. Node is a class provided by the Driver to hold the information held in Neo4j for a node.

An instance of a Node has three properties:

js
Working with Node Objects
person.elementId // (1)
person.labels // (2)
person.properties // (3)
  1. elementId - a string representing the the unique identifier for the node.
    eg. Integer {high: 1234, low: 0}

  2. labels - an Array of String values, one per label stored against the node.
    eg. ['Person', 'Actor']

  3. properties - A JavaScript object containing all the properties for the node.
    eg. {name: 'Tom Hanks', tmdbId: '31' }

Internal IDs

Internal IDs represent the position in the Neo4j store files where the record is held. These numbers may be re-used so it is recommended that nodes are found by an an indexed property instead.

Relationships

Relationship objects are similar to Node in that they also include an elementId and properties properties.

js
Working with Relationship Objects
const actedIn = row.get('actedIn')

actedIn.elementId // (1)
actedIn.type // (2)
actedIn.properties // (3)
actedIn.startNodeElementId // (4)
actedIn.endNodeElementId // (5)
  1. elementId - a string representing the the unique identifier for the relationship.
    eg. Integer {high: 9876, low: 0}

  2. type - the type of the relationship
    eg. ACTED_IN

  3. properties - A JavaScript object containing all the properties for the node.
    eg. {role: 'Woody' }

  4. startNodeElementId - a string representing the unique identifier for the node at the start of the relationship

  5. endNodeElementId - a string representing the unique identifier for the node at the end of the relationship

Paths

If you return a path of nodes and relationships, they will be returned as an instance of a Path.

js
Working with Path Objects
const path = row.get('path')

path.start // (1)
path.end // (2)
path.length // (3)
path.segments // (4)
  1. start - a Neo4j Integer representing the internal ID for the node at the start of the path

  2. end - a Neo4j Integer representing the internal ID for the node at the end of the path

  3. length - A count of the number of segments within the path

  4. segments - An array of PathSegment objects.

Path Segments

A path is split into segments representing each relationship in the path. For example, say we have a path of (p:Person)-[:ACTED_IN]→(m:Movie)-[:IN_GENRE]→(g:Genre), there would be two segments.

  1. (p:Person)-[:ACTED_IN]→(m:Movie)

  2. (m:Movie)-[:IN_GENRE]→(g:Genre)

js
Iterating over Segments
path.segments.forEach(segment => {
  console.log(segment.start)
  console.log(segment.end)
  console.log(segment.relationship)
})

The PathSegment object has three properties:

  • relationship - A Relationship object representing that part of the path.

  • start - The internal ID for the start node for this path segment *

  • end - ID for the end node for this path segment *

* Start and End nodes within the Path Segment object

The start and end nodes on the PathSegment may differ from the start and end nodes of the relationship itself if the relationship was traversed in the reverse direction.

Temporal Types

The Temporal types used in the Cypher type system are also handled differently. The driver exports helper functions to assist in checking the type.

Learn how to interact with Neo4j from Node.js using the Neo4j JavaScript DriverTemporal Types
Neo4j Cypher Type Description Example Helper Function

Date

Represents an instant capturing the date, but not the time, nor the timezone.

2020-01-02

isDate

DateTime

Represents an instant capturing the date, the time and the timezone identifier.

2020-01-02T01:02:03+04:00

isDateTime

LocalDateTime

Represents an instant capturing the date and the time, but not the timezone.

2020-01-02T01:02:03

isLocalDateTime

LocalTime

Represents an instant capturing the time of day, but not the date, nor the timezone.

12:34:56

isLocalDate

Time

Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date.

12:34:56+04:00

isTime

Each type has a toString() method that can be used in conjunction with Date.parse() in JavaScript to convert the Cypher date into a native Date object.

js
Working with Temporal types
// import { isDateTime, isDate } from 'neo4j-driver'

// Convert a native Date into a Neo4j DateTime
const now = new Date()
const createdAt = DateTime.fromStandardDate(now)

console.log(isDateTime(createdAt)) // true
console.log(isDate(createdAt)) // false

// Convert a Neo4j DateTime back into a native Date
const dateNumber = Date.parse(createdAt.toString())
const nativeDate = new Date(dateNumber)

Spatial Types

Cypher has built-in support for handling spatial values (points), and the underlying database supports storing these point values as properties on nodes and relationships.

Point

When using the Point spatial type, regardless of the coordinate reference system, an instance of the Point is returned. The Point object has three public members:

  • srid - The coordinate reference system (CRS) identifier (4326 for the wsg-84 or wsg-84-3d CRS).

  • x - The x coordinate of the point.

  • y - The y coordinate of the point.

  • z - The z coordinate of the point or undefined if point is 2-dimensional.

The isPoint() helper function will allow you to check whether a variable is an instance of a Point

js
Working with Points
import { Point, isPoint } from 'neo4j-driver'

const london = new Point(-0.118092, 51.509865)
const shard = new Point(-0.086500, 51.504501, 310) // 310m high

const cartesian2d = new Point(10, 5)
const cartesian3d = new Point(10, 5, 20)

console.log(london) // Point({srid:4326, x:-0.118092, y:51.509865})
console.log(shard) // Point({srid:4979, x:-0.0865, y:51.504501, z:310})

console.log(catesian2d) // Point({srid:7203, x:5, y:10})
console.log(catesian3d) // Point({srid:9157, x:5, y:10, z:20})

console.log(isPoint(london)) // true
console.log(isPoint('string')) // false

x, y and z

Regardless of the srid or whether the point is created using latitude, longitude (and height), the object returned by the Driver will always contain x, y and z properties.

When using wsg-84 or wsg-84-3d coordinates, x will represent longitude, y will represent latitude and z will represent the height.

Distance

When using the point.distance function in Cypher, the distance calculated between two points is returned as a float.

cypher
WITH point({x: 1, y:1}) AS one,
     point({x: 10, y: 10}) AS two

RETURN point.distance(one, two) // 12.727922061357855

Converting these values en masse

There may be times when you need to convert many Neo4j types back into native JavaScript types. For example, when retrieving a set of properties.

For this purpose, We have added a utility function to src/utils.js, which, when given an Object of properties, will iterate through the keys and, where appropriate, convert the value to a native JavaScript value. This function is called toNativeTypes().

Show the toNativeTypes() method

This function checks for integer and temporal values mentioned in the previous section, and calls the appropriate method to convert them into a more friendly format.

js
src/utils.js
/**
 * Convert Neo4j Properties back into JavaScript types
 *
 * @param {Record<string, any>} properties
 * @return {Record<string, any>}
 */
export function toNativeTypes(properties) {
  return Object.fromEntries(Object.keys(properties).map((key) => {
    let value = valueToNativeType(properties[key])

    return [ key, value ]
  }))
}

/**
 * Convert an individual value to its JavaScript equivalent
 *
 * @param {any} value
 * @returns {any}
 */
function valueToNativeType(value) {
  if ( Array.isArray(value) ) {
    value = value.map(innerValue => valueToNativeType(innerValue))
  }
  else if ( isInt(value) ) {
    value = value.toNumber()
  }
  else if (
    isDate(value) ||
    isDateTime(value) ||
    isTime(value) ||
    isLocalDateTime(value) ||
    isLocalTime(value) ||
    isDuration(value)
  ) {
    value = value.toString()
  }
  else if (typeof value === 'object' && value !== undefined  && value !== null) {
    value = toNativeTypes(value)
  }

  return value
}

The function is recursive and will handle nested objects and arrays.

Check Your Understanding

1. Accessing Node Properties

Which property would you access to retrieve the name property for each person?

Select the correct option in the code block below.

js
const res = await session.executeRead(tx =>
    tx.run(`
        MATCH (p:Person)-[:ACTED_IN]->(:Movie {title: $title})
        RETURN p
        LIMIT 10
    `,
    { title: 'Toy Story'})
)

const names = res.records.map(row => {
    return row.get('p')./*select:properties.name*/
})
  • ❏ name

  • ❏ property['name']

  • ✓ properties.name

  • ❏ properties[0]

Hint

properties is a JavaScript object.

Solution

properties is a JavaScript object, so properties can be accessed using . - for example node.properties.name.

2. Integer Helper Function

Which of the following functions does the Neo4j Javascript export to help check that a value is an instance of an Integer?

  • int(value)

  • instanceOf(integer(value))

  • isInt(value)

  • isInteger(value)

Hint

The function name uses a shorthand version of the word integer.

Solution

The answer is isInt(value)

Lesson Summary

In this lesson you have learned how to handle some of the more complex objects returned by a Cypher statement.

As we progress through this module, you will use the knowledge gained so far to read data from, and write data back to the database. In the next Challenge, you will modify the repository to read from the database.