Using Neo4j Temporal types in JavaScript

In my last post I wrote an introduction to the five new Neo4j Temporal data types now supported in Neo4j 3.4.   Although the functions themselves are laid out in the detailed documentation,  information on how to use these data types in an application are still thin on the ground.  In this post, I will walk through a few code samples including how to save temporal data types to the graph and how to retrieve them.

Project Setup

Temporal data types have been supported in all official drivers since version 1.6.  You can install or update the neo4j-driver dependency through npm and yarn.

npm install neo4j-driver

Once you have done that, creating a driver and session instances are identical.

index.js
const neo4j = require('neo4j-driver').v1
const driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('neo4j', 'neo'))

const session = driver.session()

Saving Temporal Data

There are two options for saving temporal data in Neo4j.  The first option is to create an instance of the data type inside inside the application and pass it through to the cypher query as a parameter.  Alternatively, you can pass through a string or object and instantiate the type inside the query itself.

Passing values as Parameters

The temporal data types are exported via v1 property of the neo4j package (and also via the v1.types key) along with a set of helper functions which allow you to determine whether a variable is an instance of any of these types.

const {
// Type Constructors
Duration,
LocalTime,
Time,
Date,
LocalDateTime,
DateTime,

// Helper Functions
isDuration,
isLocalTime,
isTime,
isDate,
isLocalDateTime,
isDateTime,
} = require('neo4j-driver').v1

Here are the links to the individual constructors, but for argument sake I will go with one of the more complex data types, DateTime.

The DateTime data type takes a harrowing nine arguments:

new DateTime(
year,
month,
day,
hour,
minute,
second,
nanosecond,
timeZoneOffsetSeconds,
timeZoneId
)

You can instantiate a DateTime and pass that inside an object of parameters in the second parameter in any session.run or tx.run call. Bolt will handle the transportation of the data and Neo4j will handle the persistence.

index.js
session.run(
'CREATE (e:Event { id: $id, title: $title, startsAt: $startsAt }) RETURN e',
{
id: 1,
title: 'Test Event',
startsAt: new neo4j.types.DateTime(2018, 01, 02, 12, 34, 56, 123400000, 'Z')
}
)

NB: make sure your nanoseconds are in the hundreds of millions.

Creating instances of data types is solid way of doing things. The benefit being that you will have no nasty surprises or data saved in inconsistent formats. Equally, you may not want to go through the trouble of importing the types into your project. Or you may just not like the look of the constructors. If this is the case, you can always use Cypher…

 

Storing Temporal data Using Cypher

As mentioned in my previous post, Cypher offers a really convenient API for creating dates and times. This way, you only have to pass through the parts that are required and not worry about smaller fractions of time.

You can either pass through the individual units as their own parameter.

Providing Individual Parameters

:param startsAtYear => 2018
:param startsAtMonth => 01
:param startsAtDay => 02

RETURN datetime({ year: $startsAtYear, month: $startsAtMonth, day: $startsAtDay })

However if for some reason you feel you need to miss a unit, your query will fail with a missing parameter query. If you prefer, you could instead send a single object containing the required units.

Sending a single Object

:param startsAt => { year: 2018, month: 01, day: 02 }

RETURN datetime(startsAt)

Just make sure you use the appropriate function to cast the property as a date.

As temporal functions also accept strings, you could also send a valid string as a parameter.

If you’re currently working with native JavaScript Dates, the easiest thing to send the milliseconds since epoch as a parameter and then construct the date in cypher by providing a epochMillis value.

Construct Parameters
const startsAt = new Date()
const params = { startsAt: startsAt.getTime() }

Cypher Statement
RETURN date({ epochMillis: $startsAt })

 

Working with Temporal Data Types

Instances of temporal data types provide the same accessors as Cypher. Dates include .year, month and .day accessors, and time types include hour, minute, second, nanosecond. Depending on the construction of the time, the object may also return a non-null timeZoneOffsetSeconds or timeZoneId property.

Due to the issues with Integers in Javascript. these accessors will return an instance of a neo4j Integer. To convert this into a number, you can call to the toNumber() function. If you would like to skip this option, you can set the disableLosslessIntegers option to true when instantiating the driver. You can read more about this option here.

session.run(cypher, params)
.then(res => {
// Access the Property
const startsAt = res.records[0].get('e').properties.startsAt

// Use Accessors
console.log('year', startsAt.year.toNumber()) // 2018
console.log('month', startsAt.month.toNumber()) // 01
console.log('day', startsAt.day.toNumber()) // 02
console.log('hour', startsAt.hour.toNumber()) // 12
console.log('minute', startsAt.minute.toNumber()) // 34
console.log('second', startsAt.second.toNumber()) // 56
console.log('nanosecond', startsAt.nanosecond.toNumber()) // 123400000
})

The .toString() function returns an ISO8601 date string. This can be used to create a native Neo

console.log('toString', startsAt.toString()) // 2018-01-02T12:34:56.1234Z
const nativeDate = new Date( startsAt.toString() ) ) // Date("2018-01-02T12:34:56.123Z")

Full Scripts

I have created a Gist with full versions of the code above. For more information on the drivers you can check the driver documentation on github or check out the detailed documentation.

Happy coding!