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 |
---|---|---|
|
|
|
|
|
Neo4j can only store a flat array containing strings, booleans or numbers. |
|
|
|
|
|
|
|
|
See Integers |
|
|
|
|
|
|
|
|
|
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Spatial Types |
|
|
See Spatial Types |
|
|
See Spatial Types |
|
|
|
|
|
|
|
|
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.
// 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:
// 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.
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:
person.elementId // (1)
person.labels // (2)
person.properties // (3)
-
elementId
- astring
representing the the unique identifier for the node.
eg.Integer {high: 1234, low: 0}
-
labels
- an Array of String values, one per label stored against the node.
eg.['Person', 'Actor']
-
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.
const actedIn = row.get('actedIn')
actedIn.elementId // (1)
actedIn.type // (2)
actedIn.properties // (3)
actedIn.startNodeElementId // (4)
actedIn.endNodeElementId // (5)
-
elementId
- astring
representing the the unique identifier for the relationship.
eg.Integer {high: 9876, low: 0}
-
type
- the type of the relationship
eg.ACTED_IN
-
properties
- A JavaScript object containing all the properties for the node.
eg.{role: 'Woody' }
-
startNodeElementId
- a string representing the unique identifier for the node at the start of the relationship -
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
.
const path = row.get('path')
path.start // (1)
path.end // (2)
path.length // (3)
path.segments // (4)
-
start
- a Neo4jInteger
representing the internal ID for the node at the start of the path -
end
- a Neo4jInteger
representing the internal ID for the node at the end of the path -
length
- A count of the number of segments within the path -
segments
- An array ofPathSegment
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.
-
(p:Person)-[:ACTED_IN]→(m:Movie)
-
(m:Movie)-[:IN_GENRE]→(g:Genre)
path.segments.forEach(segment => {
console.log(segment.start)
console.log(segment.end)
console.log(segment.relationship)
})
The PathSegment
object has three properties:
-
relationship
- ARelationship
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
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.
Neo4j Cypher Type | Description | Example | Helper Function |
---|---|---|---|
|
Represents an instant capturing the date, but not the time, nor the timezone. |
|
|
|
Represents an instant capturing the date, the time and the timezone identifier. |
|
|
|
Represents an instant capturing the date and the time, but not the timezone. |
|
|
|
Represents an instant capturing the time of day, but not the date, nor the timezone. |
|
|
|
Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date. |
|
|
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.
// 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 thewsg-84
orwsg-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
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.
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.
/**
* 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.
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.