diff --git a/README.md b/README.md index b176bd13c..e04bf85c3 100644 --- a/README.md +++ b/README.md @@ -1,505 +1,28 @@ # Neo4j Driver for JavaScript +**This is a preview branch for the new Vector type in the JavaScript driver.** -This is the official Neo4j driver for JavaScript. +The 6.0 release of the JavaScript driver will introduce the ability to read and write Vectors to the database. +These are single type arrays with inner types of int 8/16/32/64 and float 32/64. +To facilitate ease of use and good integration with AI packages in JavaScript, Vectors will be represented by JavaScript TypedArrays, wrapped inside a new Vector type much like Integers. -Starting with 5.0, the Neo4j Drivers will be moving to a monthly release cadence. A minor version will be released on the last Friday of each month so as to maintain versioning consistency with the core product (Neo4j DBMS) which has also moved to a monthly cadence. +A number of pieces of example code can be found in the Vector types examples test file [here](./packages/neo4j-driver/test/vector-examples.test.js) -As a policy, patch versions will not be released except on rare occasions. Bug fixes and updates will go into the latest minor version and users should upgrade to that. Driver upgrades within a major version will never contain breaking API changes. +Vector types are usuable from bolt version 6.0 and forward, which no current server currently supports, so this can not be tested against any live Neo4j Database at this time. -See also: https://neo4j.com/developer/kb/neo4j-supported-versions/ +## Example code -Resources to get you started: - -- [API Documentation](https://neo4j.com/docs/api/javascript-driver/current/) -- [Neo4j Manual](https://neo4j.com/docs/) -- [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) - -## What's New in 5.x - -- [Changelog](https://github.com/neo4j/neo4j-javascript-driver/wiki/5.0-changelog) - -## Including the Driver - -### In Node.js application - -Stable channel: - -```shell -npm install neo4j-driver -``` - -Pre-release channel: - -```shell -npm install neo4j-driver@next -``` - -Please note that `@next` only points to pre-releases that are not suitable for production use. -To get the latest stable release omit `@next` part altogether or use `@latest` instead. - -```javascript -var neo4j = require('neo4j-driver') -``` - -Driver instance should be closed when Node.js application exits: - -```javascript -driver.close() // returns a Promise -``` - -otherwise application shutdown might hang or it might exit with a non-zero exit code. - -### In web browser - -We build a special browser version of the driver, which supports connecting to Neo4j over WebSockets. -It can be included in an HTML page using one of the following tags: - -```html - - - - - - - - - - - - -``` - -This will make a global `neo4j` object available, where you can create a driver instance with `neo4j.driver`: - -```javascript -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password') -) -``` - -From `5.4.0`, this version is also exported as ECMA Script Module. -It can be imported from a module using the following statements: - -```javascript -// Direct reference -import neo4j from 'lib/browser/neo4j-web.esm.min.js' - -// unpkg CDN non-minified , version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' - -// unpkg CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' - -// jsDelivr CDN non-minified, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' - -// jsDelivr CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 -import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' - -``` - -It is not required to explicitly close the driver on a web page. Web browser should gracefully close all open -WebSockets when the page is unloaded. However, driver instance should be explicitly closed when it's lifetime -is not the same as the lifetime of the web page: - -```javascript -driver.close() // returns a Promise -``` - -## Usage examples - -### Constructing a Driver - -```javascript -// Create a driver instance, for the user `neo4j` with password `password`. -// It should be enough to have a single driver per database per application. -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password') -) - -// Close the driver when application exits. -// This closes all used network connections. -await driver.close() -``` - -### Acquiring a Session - -#### Regular Session - -```javascript -// Create a session to run Cypher statements in. -// Note: Always make sure to close sessions when you are done using them! -var session = driver.session() -``` - -##### with a Default Access Mode of `READ` - -```javascript -var session = driver.session({ defaultAccessMode: neo4j.session.READ }) -``` - -##### with Bookmarks - -```javascript -var session = driver.session({ - bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] -}) -``` - -##### against a Database - -```javascript -var session = driver.session({ - database: 'foo', - defaultAccessMode: neo4j.session.WRITE -}) -``` - -#### Reactive Session - -```javascript -// Create a reactive session to run Cypher statements in. -// Note: Always make sure to close sessions when you are done using them! -var rxSession = driver.rxSession() -``` - -##### with a Default Access Mode of `READ` - -```javascript -var rxSession = driver.rxSession({ defaultAccessMode: neo4j.session.READ }) -``` - -##### with Bookmarks - -```javascript -var rxSession = driver.rxSession({ - bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] -}) -``` - -##### against a Database - -```javascript -var rxSession = driver.rxSession({ - database: 'foo', - defaultAccessMode: neo4j.session.WRITE -}) -``` - -### Transaction functions - -```javascript -// Transaction functions provide a convenient API with minimal boilerplate and -// retries on network fluctuations and transient errors. Maximum retry time is -// configured on the driver level and is 30 seconds by default: -// Applies both to standard and reactive sessions. -neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'password'), { - maxTransactionRetryTime: 30000 -}) -``` - -#### Reading with Async Session - -```javascript -// It is possible to execute read transactions that will benefit from automatic -// retries on both single instance ('bolt' URI scheme) and Causal Cluster -// ('neo4j' URI scheme) and will get automatic load balancing in cluster deployments -var readTxResultPromise = session.executeRead(txc => { - // used transaction will be committed automatically, no need for explicit commit/rollback - - var result = txc.run('MATCH (person:Person) RETURN person.name AS name') - // at this point it is possible to either return the result or process it and return the - // result of processing it is also possible to run more statements in the same transaction - return result -}) - -// returned Promise can be later consumed like this: -readTxResultPromise - .then(result => { - console.log(result.records) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Reading with Reactive Session - -```javascript -rxSession - .executeRead(txc => - txc - .run('MATCH (person:Person) RETURN person.name AS name') - .records() - .pipe(map(record => record.get('name'))) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: err => console.log(error) - }) -``` - -#### Writing with Async Session - -```javascript -// It is possible to execute write transactions that will benefit from automatic retries -// on both single instance ('bolt' URI scheme) and Causal Cluster ('neo4j' URI scheme) -var writeTxResultPromise = session.executeWrite(async txc => { - // used transaction will be committed automatically, no need for explicit commit/rollback - - var result = await txc.run( - "MERGE (alice:Person {name : 'Alice'}) RETURN alice.name AS name" - ) - // at this point it is possible to either return the result or process it and return the - // result of processing it is also possible to run more statements in the same transaction - return result.records.map(record => record.get('name')) -}) - -// returned Promise can be later consumed like this: -writeTxResultPromise - .then(namesArray => { - console.log(namesArray) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Writing with Reactive Session - -```javascript -rxSession - .executeWrite(txc => - txc - .run("MERGE (alice:Person {name: 'James'}) RETURN alice.name AS name") - .records() - .pipe(map(record => record.get('name'))) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: error => console.log(error) - }) -``` - -### Consuming Records - -#### Consuming Records with Streaming API - -```javascript -// Run a Cypher statement, reading the result in a streaming manner as records arrive: -session - .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { - nameParam: 'Alice' - }) - .subscribe({ - onKeys: keys => { - console.log(keys) - }, - onNext: record => { - console.log(record.get('name')) - }, - onCompleted: () => { - session.close() // returns a Promise - }, - onError: error => { - console.log(error) - } - }) -``` - -Subscriber API allows following combinations of `onKeys`, `onNext`, `onCompleted` and `onError` callback invocations: - -- zero or one `onKeys`, -- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked in this case -- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked in this case. - -#### Consuming Records with Promise API - -```javascript -// the Promise way, where the complete result is collected before we act on it: -session - .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { - nameParam: 'James' - }) - .then(result => { - result.records.forEach(record => { - console.log(record.get('name')) +### Read and Write Vectors +```Javascript + const driver = neo4j.driver(uri, sharedNeo4j.authToken) + await driver.executeQuery('CREATE (p:Product) SET p.embeddings = $embeddings', { + embeddings: neo4j.vector(Float32Array.from([0, 1, 2, 3])), //Typed arrays can be created from a regular list of Numbers }) - }) - .catch(error => { - console.log(error) - }) - .then(() => session.close()) -``` - -#### Consuming Records with Reactive API - -```javascript -rxSession - .run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', { - nameParam: 'Bob' - }) - .records() - .pipe( - map(record => record.get('name')), - concatWith(rxSession.close()) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: err => console.log(err) - }) -``` - -### Explicit Transactions - -#### With Async Session - -```javascript -// run statement in a transaction -const txc = session.beginTransaction() -try { - const result1 = await txc.run( - 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', - { - nameParam: 'Bob' - } - ) - result1.records.forEach(r => console.log(r.get('name'))) - console.log('First query completed') - - const result2 = await txc.run( - 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', - { - nameParam: 'Adam' - } - ) - result2.records.forEach(r => console.log(r.get('name'))) - console.log('Second query completed') - - await txc.commit() - console.log('committed') -} catch (error) { - console.log(error) - await txc.rollback() - console.log('rolled back') -} finally { - await session.close() -} -``` - -#### With Reactive Session - -```javascript -rxSession - .beginTransaction() - .pipe( - mergeMap(txc => - concatWith( - txc - .run( - 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', - { - nameParam: 'Bob' - } - ) - .records() - .pipe(map(r => r.get('name'))), - of('First query completed'), - txc - .run( - 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', - { - nameParam: 'Adam' - } - ) - .records() - .pipe(map(r => r.get('name'))), - of('Second query completed'), - txc.commit(), - of('committed') - ).pipe(catchError(err => txc.rollback().pipe(throwError(() => err)))) - ) - ) - .subscribe({ - next: data => console.log(data), - complete: () => console.log('completed'), - error: error => console.log(error) - }) -``` - -### Numbers and the Integer type - -The Neo4j type system uses 64-bit signed integer values. The range of values is between `-(2``64``- 1)` and `(2``63``- 1)`. - -However, JavaScript can only safely represent integers between `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. - -In order to support the full Neo4j type system, the driver will not automatically convert to javascript integers. -Any time the driver receives an integer value from Neo4j, it will be represented with an internal integer type by the driver. - -_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ - -#### Writing integers - -Numbers written directly e.g. `session.run("CREATE (n:Node {age: $age})", {age: 22})` will be of type `Float` in Neo4j. - -To write the `age` as an integer the `neo4j.int` method should be used: - -```javascript -var neo4j = require('neo4j-driver') - -session.run('CREATE (n {age: $myIntParam})', { myIntParam: neo4j.int(22) }) -``` - -To write an integer value that are not within the range of `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`, use a string argument to `neo4j.int`: - -```javascript -session.run('CREATE (n {age: $myIntParam})', { - myIntParam: neo4j.int('9223372036854775807') -}) -``` - -#### Reading integers - -In Neo4j, the type Integer can be larger what can be represented safely as an integer with JavaScript Number. - -It is only safe to convert to a JavaScript Number if you know that the number will be in the range `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. - -In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. - -```javascript -var smallInteger = neo4j.int(123) -if (neo4j.integer.inSafeRange(smallInteger)) { - var aNumber = smallInteger.toNumber() -} -``` - -If you will be handling integers that is not within the JavaScript safe range of integers, you should convert the value to a string: - -```javascript -var largeInteger = neo4j.int('9223372036854775807') -if (!neo4j.integer.inSafeRange(largeInteger)) { - var integerAsString = largeInteger.toString() -} -``` - -#### Enabling native numbers + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.embeddings as embeddings') + + let vector = res.records[0].get('embeddings') -Starting from 1.6 version of the driver it is possible to configure it to only return native numbers instead of custom `Integer` objects. -The configuration option affects all integers returned by the driver. **Enabling this option can result in a loss of precision and incorrect numeric -values being returned if the database contains integer numbers outside of the range** `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. -To enable potentially lossy integer values use the driver's configuration object: + console.log(vector.toTypedArray()) //Float32Array[0, 1, 2, 3] + console.log(vector.type) //FLOAT32 -```javascript -var driver = neo4j.driver( - 'neo4j://localhost', - neo4j.auth.basic('neo4j', 'password'), - { disableLosslessIntegers: true } -) + await driver.close() ``` diff --git a/original_README.md b/original_README.md new file mode 100644 index 000000000..b176bd13c --- /dev/null +++ b/original_README.md @@ -0,0 +1,505 @@ +# Neo4j Driver for JavaScript + +This is the official Neo4j driver for JavaScript. + +Starting with 5.0, the Neo4j Drivers will be moving to a monthly release cadence. A minor version will be released on the last Friday of each month so as to maintain versioning consistency with the core product (Neo4j DBMS) which has also moved to a monthly cadence. + +As a policy, patch versions will not be released except on rare occasions. Bug fixes and updates will go into the latest minor version and users should upgrade to that. Driver upgrades within a major version will never contain breaking API changes. + +See also: https://neo4j.com/developer/kb/neo4j-supported-versions/ + +Resources to get you started: + +- [API Documentation](https://neo4j.com/docs/api/javascript-driver/current/) +- [Neo4j Manual](https://neo4j.com/docs/) +- [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) + +## What's New in 5.x + +- [Changelog](https://github.com/neo4j/neo4j-javascript-driver/wiki/5.0-changelog) + +## Including the Driver + +### In Node.js application + +Stable channel: + +```shell +npm install neo4j-driver +``` + +Pre-release channel: + +```shell +npm install neo4j-driver@next +``` + +Please note that `@next` only points to pre-releases that are not suitable for production use. +To get the latest stable release omit `@next` part altogether or use `@latest` instead. + +```javascript +var neo4j = require('neo4j-driver') +``` + +Driver instance should be closed when Node.js application exits: + +```javascript +driver.close() // returns a Promise +``` + +otherwise application shutdown might hang or it might exit with a non-zero exit code. + +### In web browser + +We build a special browser version of the driver, which supports connecting to Neo4j over WebSockets. +It can be included in an HTML page using one of the following tags: + +```html + + + + + + + + + + + + +``` + +This will make a global `neo4j` object available, where you can create a driver instance with `neo4j.driver`: + +```javascript +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password') +) +``` + +From `5.4.0`, this version is also exported as ECMA Script Module. +It can be imported from a module using the following statements: + +```javascript +// Direct reference +import neo4j from 'lib/browser/neo4j-web.esm.min.js' + +// unpkg CDN non-minified , version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' + +// unpkg CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://unpkg.com/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' + +// jsDelivr CDN non-minified, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.js' + +// jsDelivr CDN minified for production use, version X.Y.Z where X.Y.Z >= 5.4.0 +import neo4j from 'https://cdn.jsdelivr.net/npm/neo4j-driver@X.Y.Z/lib/browser/neo4j-web.esm.min.js' + +``` + +It is not required to explicitly close the driver on a web page. Web browser should gracefully close all open +WebSockets when the page is unloaded. However, driver instance should be explicitly closed when it's lifetime +is not the same as the lifetime of the web page: + +```javascript +driver.close() // returns a Promise +``` + +## Usage examples + +### Constructing a Driver + +```javascript +// Create a driver instance, for the user `neo4j` with password `password`. +// It should be enough to have a single driver per database per application. +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password') +) + +// Close the driver when application exits. +// This closes all used network connections. +await driver.close() +``` + +### Acquiring a Session + +#### Regular Session + +```javascript +// Create a session to run Cypher statements in. +// Note: Always make sure to close sessions when you are done using them! +var session = driver.session() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var session = driver.session({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var session = driver.session({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var session = driver.session({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` + +#### Reactive Session + +```javascript +// Create a reactive session to run Cypher statements in. +// Note: Always make sure to close sessions when you are done using them! +var rxSession = driver.rxSession() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var rxSession = driver.rxSession({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var rxSession = driver.rxSession({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var rxSession = driver.rxSession({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` + +### Transaction functions + +```javascript +// Transaction functions provide a convenient API with minimal boilerplate and +// retries on network fluctuations and transient errors. Maximum retry time is +// configured on the driver level and is 30 seconds by default: +// Applies both to standard and reactive sessions. +neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'password'), { + maxTransactionRetryTime: 30000 +}) +``` + +#### Reading with Async Session + +```javascript +// It is possible to execute read transactions that will benefit from automatic +// retries on both single instance ('bolt' URI scheme) and Causal Cluster +// ('neo4j' URI scheme) and will get automatic load balancing in cluster deployments +var readTxResultPromise = session.executeRead(txc => { + // used transaction will be committed automatically, no need for explicit commit/rollback + + var result = txc.run('MATCH (person:Person) RETURN person.name AS name') + // at this point it is possible to either return the result or process it and return the + // result of processing it is also possible to run more statements in the same transaction + return result +}) + +// returned Promise can be later consumed like this: +readTxResultPromise + .then(result => { + console.log(result.records) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Reading with Reactive Session + +```javascript +rxSession + .executeRead(txc => + txc + .run('MATCH (person:Person) RETURN person.name AS name') + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(error) + }) +``` + +#### Writing with Async Session + +```javascript +// It is possible to execute write transactions that will benefit from automatic retries +// on both single instance ('bolt' URI scheme) and Causal Cluster ('neo4j' URI scheme) +var writeTxResultPromise = session.executeWrite(async txc => { + // used transaction will be committed automatically, no need for explicit commit/rollback + + var result = await txc.run( + "MERGE (alice:Person {name : 'Alice'}) RETURN alice.name AS name" + ) + // at this point it is possible to either return the result or process it and return the + // result of processing it is also possible to run more statements in the same transaction + return result.records.map(record => record.get('name')) +}) + +// returned Promise can be later consumed like this: +writeTxResultPromise + .then(namesArray => { + console.log(namesArray) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Writing with Reactive Session + +```javascript +rxSession + .executeWrite(txc => + txc + .run("MERGE (alice:Person {name: 'James'}) RETURN alice.name AS name") + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` + +### Consuming Records + +#### Consuming Records with Streaming API + +```javascript +// Run a Cypher statement, reading the result in a streaming manner as records arrive: +session + .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { + nameParam: 'Alice' + }) + .subscribe({ + onKeys: keys => { + console.log(keys) + }, + onNext: record => { + console.log(record.get('name')) + }, + onCompleted: () => { + session.close() // returns a Promise + }, + onError: error => { + console.log(error) + } + }) +``` + +Subscriber API allows following combinations of `onKeys`, `onNext`, `onCompleted` and `onError` callback invocations: + +- zero or one `onKeys`, +- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked in this case +- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked in this case. + +#### Consuming Records with Promise API + +```javascript +// the Promise way, where the complete result is collected before we act on it: +session + .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { + nameParam: 'James' + }) + .then(result => { + result.records.forEach(record => { + console.log(record.get('name')) + }) + }) + .catch(error => { + console.log(error) + }) + .then(() => session.close()) +``` + +#### Consuming Records with Reactive API + +```javascript +rxSession + .run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', { + nameParam: 'Bob' + }) + .records() + .pipe( + map(record => record.get('name')), + concatWith(rxSession.close()) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(err) + }) +``` + +### Explicit Transactions + +#### With Async Session + +```javascript +// run statement in a transaction +const txc = session.beginTransaction() +try { + const result1 = await txc.run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' + } + ) + result1.records.forEach(r => console.log(r.get('name'))) + console.log('First query completed') + + const result2 = await txc.run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + result2.records.forEach(r => console.log(r.get('name'))) + console.log('Second query completed') + + await txc.commit() + console.log('committed') +} catch (error) { + console.log(error) + await txc.rollback() + console.log('rolled back') +} finally { + await session.close() +} +``` + +#### With Reactive Session + +```javascript +rxSession + .beginTransaction() + .pipe( + mergeMap(txc => + concatWith( + txc + .run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('First query completed'), + txc + .run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('Second query completed'), + txc.commit(), + of('committed') + ).pipe(catchError(err => txc.rollback().pipe(throwError(() => err)))) + ) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` + +### Numbers and the Integer type + +The Neo4j type system uses 64-bit signed integer values. The range of values is between `-(2``64``- 1)` and `(2``63``- 1)`. + +However, JavaScript can only safely represent integers between `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. + +In order to support the full Neo4j type system, the driver will not automatically convert to javascript integers. +Any time the driver receives an integer value from Neo4j, it will be represented with an internal integer type by the driver. + +_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ + +#### Writing integers + +Numbers written directly e.g. `session.run("CREATE (n:Node {age: $age})", {age: 22})` will be of type `Float` in Neo4j. + +To write the `age` as an integer the `neo4j.int` method should be used: + +```javascript +var neo4j = require('neo4j-driver') + +session.run('CREATE (n {age: $myIntParam})', { myIntParam: neo4j.int(22) }) +``` + +To write an integer value that are not within the range of `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`, use a string argument to `neo4j.int`: + +```javascript +session.run('CREATE (n {age: $myIntParam})', { + myIntParam: neo4j.int('9223372036854775807') +}) +``` + +#### Reading integers + +In Neo4j, the type Integer can be larger what can be represented safely as an integer with JavaScript Number. + +It is only safe to convert to a JavaScript Number if you know that the number will be in the range `Number.MIN_SAFE_INTEGER` `-(2``53``- 1)` and `Number.MAX_SAFE_INTEGER` `(2``53``- 1)`. + +In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. + +```javascript +var smallInteger = neo4j.int(123) +if (neo4j.integer.inSafeRange(smallInteger)) { + var aNumber = smallInteger.toNumber() +} +``` + +If you will be handling integers that is not within the JavaScript safe range of integers, you should convert the value to a string: + +```javascript +var largeInteger = neo4j.int('9223372036854775807') +if (!neo4j.integer.inSafeRange(largeInteger)) { + var integerAsString = largeInteger.toString() +} +``` + +#### Enabling native numbers + +Starting from 1.6 version of the driver it is possible to configure it to only return native numbers instead of custom `Integer` objects. +The configuration option affects all integers returned by the driver. **Enabling this option can result in a loss of precision and incorrect numeric +values being returned if the database contains integer numbers outside of the range** `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. +To enable potentially lossy integer values use the driver's configuration object: + +```javascript +var driver = neo4j.driver( + 'neo4j://localhost', + neo4j.auth.basic('neo4j', 'password'), + { disableLosslessIntegers: true } +) +``` diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js new file mode 100644 index 000000000..9db026f1a --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x8 from './bolt-protocol-v5x8' + +import transformersFactories from './bolt-protocol-v6x0.transformer' +import Transformer from './transformer' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V6_0 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x8 { + get version () { + return BOLT_PROTOCOL_V6_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js new file mode 100644 index 000000000..12b239e22 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x8 from './bolt-protocol-v5x8.transformer' +import { TypeTransformer } from './transformer' +import { structure } from '../packstream' +import { Vector, newError } from 'neo4j-driver-core' +const VECTOR = 0x56 +const FLOAT_32 = 0xc6 +const FLOAT_64 = 0xc1 +const INT_8 = 0xc8 +const INT_16 = 0xc9 +const INT_32 = 0xca +const INT_64 = 0xcb + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const dataview = new DataView(new ArrayBuffer(vector.typedArray.byteLength)) + let set + let typeMarker + switch (vector.type) { + case 'INT8': + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8.bind(dataview) + break + case 'INT16': + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16.bind(dataview) + break + case 'INT32': + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32.bind(dataview) + break + case 'INT64': + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setBigInt64.bind(dataview) + break + case 'FLOAT32': + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32.bind(dataview) + break + case 'FLOAT64': + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64.bind(dataview) + break + default: + throw newError(`Vector is of unsupported type ${vector.type}`) + } + for (let i = 0; i < vector.typedArray.length; i++) { + set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) + } + const struct = new structure.Structure(VECTOR, [typeMarker, new Int8Array(dataview.buffer)]) + return struct + }, + fromStructure: structure => { + const isLittleEndian = checkLittleEndian() + const typeMarker = structure.fields[0][0] + const arrayBuffer = structure.fields[1] + const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) + const getview = new DataView(arrayBuffer.buffer) + let get + let set + let resultArray + switch (typeMarker) { + case INT_8: + return new Vector(Int8Array.from(arrayBuffer)) + case INT_16: + resultArray = new Int16Array(setview.buffer) + get = getview.getInt16.bind(getview) + set = setview.setInt16.bind(setview) + break + case INT_32: + resultArray = new Int32Array(setview.buffer) + get = getview.getInt32.bind(getview) + set = setview.setInt32.bind(setview) + break + case INT_64: + resultArray = new BigInt64Array(setview.buffer) + get = getview.getBigInt64.bind(getview) + set = setview.setBigInt64.bind(setview) + break + case FLOAT_32: + resultArray = new Float32Array(setview.buffer) + get = getview.getFloat32.bind(getview) + set = setview.setFloat32.bind(setview) + break + case FLOAT_64: + resultArray = new Float64Array(setview.buffer) + get = getview.getFloat64.bind(getview) + set = setview.setFloat64.bind(setview) + break + default: + throw newError(`Recieved Vector of unknown type ${typeMarker}`) + } + for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { + set(i, get(i), isLittleEndian) + } + return new Vector(resultArray) + } + }) +} + +function checkLittleEndian () { + const dataview = new DataView(new ArrayBuffer(2)) + dataview.setInt16(0, 1000, true) + const typeArray = new Int16Array(dataview.buffer) + return typeArray[0] === 1000 +} + +export default { + ...v5x8, + createVectorTransformer +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 1b8f792a5..18891e197 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -33,6 +33,7 @@ import BoltProtocolV5x5 from './bolt-protocol-v5x5' import BoltProtocolV5x6 from './bolt-protocol-v5x6' import BoltProtocolV5x7 from './bolt-protocol-v5x7' import BoltProtocolV5x8 from './bolt-protocol-v5x8' +import BoltProtocolV6x0 from './bolt-protocol-v6x0' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -266,6 +267,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 6.0: + return new BoltProtocolV6x0(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/bolt-connection/src/bolt/handshake.js b/packages/bolt-connection/src/bolt/handshake.js index 7f1d2a1c4..4354654cf 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -19,7 +19,7 @@ import { alloc } from '../channel' import { newError } from 'neo4j-driver-core' const BOLT_MAGIC_PREAMBLE = 0x6060b017 -const AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference +const AVAILABLE_BOLT_PROTOCOLS = ['6.0', '5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference const DESIRED_CAPABILITES = 0 function version (major, minor) { diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap new file mode 100644 index 000000000..a56e2c5af --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v6x0.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; + +exports[`#unit BoltProtocolV6x0 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV6x0 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`; diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js new file mode 100644 index 000000000..afc7fbf61 --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v6x0.test.js @@ -0,0 +1,1606 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV6x0 from '../../src/bolt/bolt-protocol-v6x0' +import RequestMessage from '../../src/bolt/request-message' +import { v2, structure } from '../../src/packstream' +import utils from '../test-utils' +import { LoginObserver, RouteObserver } from '../../src/bolt/stream-observers' +import fc from 'fast-check' +import { + Date, + DateTime, + Duration, + LocalDateTime, + LocalTime, + Path, + PathSegment, + Point, + Relationship, + Time, + UnboundRelationship, + Node, + internal, + vector, + json +} from 'neo4j-driver-core' + +import { alloc } from '../../src/channel' +import { notificationFilterBehaviour, telemetryBehaviour } from './behaviour' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV6x0', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + telemetryBehaviour.protocolSupportsTelemetry(newProtocol) + + it('should enrich error metadata', () => { + const protocol = new BoltProtocolV6x0() + const enrichedData = protocol.enrichErrorMetadata({ neo4j_code: 'hello', diagnostic_record: {} }) + expect(enrichedData.code).toBe('hello') + expect(enrichedData.diagnostic_record.OPERATION).toBe('') + expect(enrichedData.diagnostic_record.OPERATION_CODE).toBe('0') + expect(enrichedData.diagnostic_record.CURRENT_SCHEMA).toBe('/') + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, [], { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should request routing information sending bookmarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const listOfBookmarks = ['a', 'b', 'c'] + const bookmarks = new Bookmarks(listOfBookmarks) + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName, + sessionContext: { bookmarks } + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, listOfBookmarks, { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should run a query', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should run a with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should begin a transaction', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, impersonatedUser }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + + expect(protocol.version).toBe(6.0) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV6x0(null, null, false) + + const transformedMetadata = protocol.transformMetadata(metadata) + + expect(transformedMetadata).toEqual({ + result_available_after: 1, + result_consumed_after: 2, + db_hits: 3, + some_other_key: 4 + }) + }) + + it('should initialize connection', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const boltAgent = { + product: 'neo4j-javascript/5.28', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(clientName, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each([ + 'javascript-driver/6.0.0', + '', + undefined, + null + ])('should always use the user agent set by the user', (userAgent) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const boltAgent = { + product: 'neo4j-javascript/5.28', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(userAgent, boltAgent) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each( + [true, false] + )('should logon to the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.logon({ authToken, flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it.each( + [true, false] + )('should logoff from the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.logoff({ flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logoff() + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it('should begin a transaction', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should commit', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.commitTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.commit()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should rollback', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.rollbackTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.rollback()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should support logoff', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + + expect(protocol.supportsReAuth).toBe(true) + }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV6x0(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('notificationFilter', () => { + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnRun(newProtocol) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV6x0(recorder, null, false) + ) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + const observer = protocol.run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + lowRecordWatermark: 100, + highRecordWatermark: 200 + }) + + expect(observer._lowRecordWatermark).toEqual(100) + expect(observer._highRecordWatermark).toEqual(200) + }) + }) + + describe('packstream', () => { + it('should configure v2 packer', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV6x0(null, null, false) + expect(protocol.unpacker()).toBeInstanceOf(v2.Unpacker) + }) + }) + + describe('.packable()', () => { + it.each([ + ['Node', new Node(1, ['a'], { a: 'b' }, 'c')], + ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], + ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], + ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] + ])('should resultant function not pack graph types (%s)', (_, graphType) => { + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + null, + false + ) + + const packable = protocol.packable(graphType) + + expect(packable).toThrowErrorMatchingSnapshot() + }) + + it.each([ + ['Duration', new Duration(1, 1, 1, 1)], + ['LocalTime', new LocalTime(1, 1, 1, 1)], + ['Time', new Time(1, 1, 1, 1, 1)], + ['Date', new Date(1, 1, 1)], + ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ], + ['Point2D', new Point(1, 1, 1)], + ['Point3D', new Point(1, 1, 1, 1)] + ])('should pack spatial types and temporal types (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + ['Int8', vector(Int8Array.from([1, 2, 3]))], + ['Int16', vector(Int16Array.from([1, 2, 3]))], + ['Int32', vector(Int32Array.from([1, 2, 3]))], + ['Int64', vector(BigInt64Array.from([BigInt(1), BigInt(2), BigInt(3)]))], + ['Float32', vector(Float32Array.from([1, 2, 3]))], + ['Float64', vector(Float64Array.from([1, 2, 3]))] + ])('should pack vectors (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + {} + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(json.stringify(unpacked)).toEqual(json.stringify(object)) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(1978, 12, 16, 12, 35, 59, 128000987, undefined, 'Europe/Istanbul') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Pacific/Honolulu') + ], + [ + 'DateWithWithZoneId / Berlin before common era', + new DateTime(-2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateWithWithZoneId / Max Date', + new DateTime(99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Kiritimati') + ], + [ + 'DateWithWithZoneId / Min Date', + new DateTime(-99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ], + [ + 'DateWithWithZoneId / Ambiguous date between 00 and 99', + new DateTime(50, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it('should pack and unpack DateTimeWithOffset', () => { + fc.assert( + fc.property( + fc.date({ + min: temporalUtil.newDate(utils.MIN_UTC_IN_MS + utils.ONE_DAY_IN_MS), + max: temporalUtil.newDate(utils.MAX_UTC_IN_MS - utils.ONE_DAY_IN_MS) + }), + fc.integer({ min: 0, max: 999_999 }), + utils.arbitraryTimeZoneId(), + (date, nanoseconds, timeZoneId) => { + const object = new DateTime( + date.getUTCFullYear(), + date.getUTCMonth() + 1, + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds() * 1_000_000 + nanoseconds, + undefined, + timeZoneId + ) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + ) + }) + + it('should pack and unpack DateTimeWithZoneIdAndNoOffset', () => { + fc.assert( + fc.property(fc.date(), date => { + const object = DateTime.fromStandardDate(date) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + expect(unpacked).toEqual(object) + }) + ) + }) + }) + + describe('.unpack()', () => { + it.each([ + [ + 'Node', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, 'elementId']), + new Node(1, ['a'], { c: 'd' }, 'elementId') + ], + [ + 'Relationship', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2']), + new Relationship(1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2') + ], + [ + 'UnboundRelationship', + new structure.Structure(0x72, [1, '2', { 3: 4 }, 'elementId']), + new UnboundRelationship(1, '2', { 3: 4 }, 'elementId') + ], + [ + 'Path', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }, 'node1']), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }, 'node2']), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }, 'node3']) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2']), + new structure.Structure(0x52, [5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3']) + ], + [1, 1, 2, 2] + ] + ), + new Path( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Node(2, ['3'], { 4: '5' }, 'node3'), + [ + new PathSegment( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Relationship(3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2'), + new Node(4, ['5'], { 6: 7 }, 'node2') + ), + new PathSegment( + new Node(4, ['5'], { 6: 7 }, 'node2'), + new Relationship(5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3'), + new Node(2, ['3'], { 4: '5' }, 'node3') + ) + ] + ) + ] + ])('should unpack graph types (%s)', (_, struct, graphObject) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(graphObject) + }) + + it.each([ + [ + 'Node with less fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }]) + ], + [ + 'Node with more fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, '1', 'b']) + ], + [ + 'Relationship with less fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }]) + ], + [ + 'Relationship with more fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, '1', '2', '3', '4']) + ], + [ + 'UnboundRelationship with less fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }]) + ], + [ + 'UnboundRelationship with more fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }, '1', '2']) + ], + [ + 'Path with less fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ] + ] + ) + ], + [ + 'Path with more fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ], + [1, 1, 2, 2], + 'a' + ] + ) + ], + [ + 'Point with less fields', + new structure.Structure(0x58, [1, 2]) + ], + [ + 'Point with more fields', + new structure.Structure(0x58, [1, 2, 3, 4]) + ], + [ + 'Point3D with less fields', + new structure.Structure(0x59, [1, 2, 3]) + ], + + [ + 'Point3D with more fields', + new structure.Structure(0x59, [1, 2, 3, 4, 6]) + ], + [ + 'Duration with less fields', + new structure.Structure(0x45, [1, 2, 3]) + ], + [ + 'Duration with more fields', + new structure.Structure(0x45, [1, 2, 3, 4, 5]) + ], + [ + 'LocalTime with less fields', + new structure.Structure(0x74, []) + ], + [ + 'LocalTime with more fields', + new structure.Structure(0x74, [1, 2]) + ], + [ + 'Time with less fields', + new structure.Structure(0x54, [1]) + ], + [ + 'Time with more fileds', + new structure.Structure(0x54, [1, 2, 3]) + ], + [ + 'Date with less fields', + new structure.Structure(0x44, []) + ], + [ + 'Date with more fields', + new structure.Structure(0x44, [1, 2]) + ], + [ + 'LocalDateTime with less fields', + new structure.Structure(0x64, [1]) + ], + [ + 'LocalDateTime with more fields', + new structure.Structure(0x64, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(() => unpacked instanceof structure.Structure).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'Point', + new structure.Structure(0x58, [1, 2, 3]), + new Point(1, 2, 3) + ], + [ + 'Point3D', + new structure.Structure(0x59, [1, 2, 3, 4]), + new Point(1, 2, 3, 4) + ], + [ + 'Duration', + new structure.Structure(0x45, [1, 2, 3, 4]), + new Duration(1, 2, 3, 4) + ], + [ + 'LocalTime', + new structure.Structure(0x74, [1]), + new LocalTime(0, 0, 0, 1) + ], + [ + 'Time', + new structure.Structure(0x54, [1, 2]), + new Time(0, 0, 0, 1, 2) + ], + [ + 'Date', + new structure.Structure(0x44, [1]), + new Date(1970, 1, 2) + ], + [ + 'LocalDateTime', + new structure.Structure(0x64, [1, 2]), + new LocalDateTime(1970, 1, 1, 0, 0, 1, 2) + ], + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new structure.Structure(0x49, [ + 282659759, 128000987, -150 * 60 + ]), + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId / Honolulu', + new structure.Structure(0x69, [ + 1592231400, 183_000_000, 'Pacific/Honolulu' + ]), + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, -10 * 60 * 60, 'Pacific/Honolulu') + ], + [ + 'DateTimeWithZoneId / Midnight', + new structure.Structure(0x69, [ + 1685397950, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2023, 5, 30, 0, 5, 50, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ] + ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao_Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV6x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + describe('result metadata enrichment', () => { + it('run should configure BoltProtocolV6x0._enrichMetadata as enrichMetadata', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV6x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + expect(observer._enrichMetadata).toBe(protocol._enrichMetadata) + }) + + describe('BoltProtocolV6x0._enrichMetadata', () => { + const protocol = newProtocol() + + it('should handle empty metadata', () => { + const metadata = protocol._enrichMetadata({}) + + expect(metadata).toEqual({}) + }) + + it('should handle metadata with random objects', () => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345 + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345 + }) + }) + + it('should handle metadata not change notifications ', () => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + }) + + it.each([ + [null, null], + [undefined, undefined], + [[], []], + [statusesWithDiagnosticRecord(null, null), statusesWithDiagnosticRecord(null, null)], + [statusesWithDiagnosticRecord(undefined, undefined), statusesWithDiagnosticRecord({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }, + { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + })], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }), + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }) + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + description: 'description', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description' + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + } + }] + ] + ])('should handle statuses (%o) ', (statuses, expectedStatuses) => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345, + statuses + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + statuses: expectedStatuses + }) + }) + }) + + function statusesWithDiagnosticRecord (...diagnosticRecords) { + return diagnosticRecords.map(diagnosticRecord => { + return { + gql_status: '00000', + status_description: 'note: successful completion', + diagnostic_record: diagnosticRecord + } + }) + } + }) + + function newProtocol (recorder) { + return new BoltProtocolV6x0(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0950cf37..226dc9ad9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -101,6 +101,7 @@ import * as json from './json' import resultTransformers, { ResultTransformer } from './result-transformers' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate' import * as internal from './internal' // todo: removed afterwards +import Vector, { VectorType, vector } from './vector' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -263,7 +264,9 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } export type { @@ -294,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index b45034da9..7403677c3 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -40,6 +40,7 @@ const BOLT_PROTOCOL_V5_5: number = 5.5 const BOLT_PROTOCOL_V5_6: number = 5.6 const BOLT_PROTOCOL_V5_7: number = 5.7 const BOLT_PROTOCOL_V5_8: number = 5.8 +const BOLT_PROTOCOL_V6_0: number = 6.0 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -76,5 +77,6 @@ export { BOLT_PROTOCOL_V5_6, BOLT_PROTOCOL_V5_7, BOLT_PROTOCOL_V5_8, + BOLT_PROTOCOL_V6_0, TELEMETRY_APIS } diff --git a/packages/core/src/vector.ts b/packages/core/src/vector.ts new file mode 100644 index 000000000..ad3c121af --- /dev/null +++ b/packages/core/src/vector.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { newError } from './error' + +type EnumRecord = { [key in T]: key } + +export type VectorType = 'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64' +/** + * @typedef {'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64'} VectorType + */ +const vectorTypes: EnumRecord = { + INT8: 'INT8', + INT16: 'INT16', + INT32: 'INT32', + INT64: 'INT64', + FLOAT32: 'FLOAT32', + FLOAT64: 'FLOAT64' +} +Object.freeze(vectorTypes) + +/** + * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. + * @access public + * @exports Vector + * @class A Integer class for representing a 64 bit two's-complement integer value. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + typedArray: K + type: VectorType + constructor (typedArray: K) { + if (typedArray instanceof Int8Array) { + this.type = vectorTypes.INT8 + } else if (typedArray instanceof Int16Array) { + this.type = vectorTypes.INT16 + } else if (typedArray instanceof Int32Array) { + this.type = vectorTypes.INT32 + } else if (typedArray instanceof BigInt64Array) { + this.type = vectorTypes.INT64 + } else if (typedArray instanceof Float32Array) { + this.type = vectorTypes.FLOAT32 + } else if (typedArray instanceof Float64Array) { + this.type = vectorTypes.FLOAT64 + } else { + throw newError(`The neo4j Vector class is a wrapper for TypedArrays. got ${typeof typedArray}`) + } + this.typedArray = typedArray + } + + toTypedArray (): K { + return this.typedArray + } +} + +/** + * Cast a TypedArray to a {@link Vector} + * @access public + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. + * @return {Vector} - The Neo4j Vector ready to be used as a query parameter + */ +export function vector (typedArray: K): Vector { + return new Vector(typedArray) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js new file mode 100644 index 000000000..be4e7456b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV5x8 from './bolt-protocol-v5x8.js' + +import transformersFactories from './bolt-protocol-v6x0.transformer.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V6_0 } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x8 { + get version () { + return BOLT_PROTOCOL_V6_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js new file mode 100644 index 000000000..c03e05a10 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v6x0.transformer.js @@ -0,0 +1,130 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import v5x8 from './bolt-protocol-v5x8.transformer.js' +import { TypeTransformer } from './transformer.js' +import { structure } from '../packstream/index.js' +import { Vector, newError } from '../../core/index.ts' +const VECTOR = 0x56 +const FLOAT_32 = 0xc6 +const FLOAT_64 = 0xc1 +const INT_8 = 0xc8 +const INT_16 = 0xc9 +const INT_32 = 0xca +const INT_64 = 0xcb + +function createVectorTransformer () { + return new TypeTransformer({ + signature: VECTOR, + isTypeInstance: object => object instanceof Vector, + toStructure: vector => { + const dataview = new DataView(new ArrayBuffer(vector.typedArray.byteLength)) + let set + let typeMarker + switch (vector.type) { + case 'INT8': + typeMarker = Uint8Array.from([INT_8]) + set = dataview.setUint8.bind(dataview) + break + case 'INT16': + typeMarker = Uint8Array.from([INT_16]) + set = dataview.setUint16.bind(dataview) + break + case 'INT32': + typeMarker = Uint8Array.from([INT_32]) + set = dataview.setUint32.bind(dataview) + break + case 'INT64': + typeMarker = Uint8Array.from([INT_64]) + set = dataview.setBigInt64.bind(dataview) + break + case 'FLOAT32': + typeMarker = Uint8Array.from([FLOAT_32]) + set = dataview.setFloat32.bind(dataview) + break + case 'FLOAT64': + typeMarker = Uint8Array.from([FLOAT_64]) + set = dataview.setFloat64.bind(dataview) + break + default: + throw newError(`Vector is of unsupported type ${vector.type}`) + } + for (let i = 0; i < vector.typedArray.length; i++) { + set(i * vector.typedArray.BYTES_PER_ELEMENT, vector.typedArray[i]) + } + const struct = new structure.Structure(VECTOR, [typeMarker, new Int8Array(dataview.buffer)]) + return struct + }, + fromStructure: structure => { + const isLittleEndian = checkLittleEndian() + const typeMarker = structure.fields[0][0] + const arrayBuffer = structure.fields[1] + const setview = new DataView(new ArrayBuffer(arrayBuffer.byteLength)) + const getview = new DataView(arrayBuffer.buffer) + let get + let set + let resultArray + switch (typeMarker) { + case INT_8: + return new Vector(Int8Array.from(arrayBuffer)) + case INT_16: + resultArray = new Int16Array(setview.buffer) + get = getview.getInt16.bind(getview) + set = setview.setInt16.bind(setview) + break + case INT_32: + resultArray = new Int32Array(setview.buffer) + get = getview.getInt32.bind(getview) + set = setview.setInt32.bind(setview) + break + case INT_64: + resultArray = new BigInt64Array(setview.buffer) + get = getview.getBigInt64.bind(getview) + set = setview.setBigInt64.bind(setview) + break + case FLOAT_32: + resultArray = new Float32Array(setview.buffer) + get = getview.getFloat32.bind(getview) + set = setview.setFloat32.bind(setview) + break + case FLOAT_64: + resultArray = new Float64Array(setview.buffer) + get = getview.getFloat64.bind(getview) + set = setview.setFloat64.bind(setview) + break + default: + throw newError(`Recieved Vector of unknown type ${typeMarker}`) + } + for (let i = 0; i < arrayBuffer.length; i += resultArray.BYTES_PER_ELEMENT) { + set(i, get(i), isLittleEndian) + } + return new Vector(resultArray) + } + }) +} + +function checkLittleEndian () { + const dataview = new DataView(new ArrayBuffer(2)) + dataview.setInt16(0, 1000, true) + const typeArray = new Int16Array(dataview.buffer) + return typeArray[0] === 1000 +} + +export default { + ...v5x8, + createVectorTransformer +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 188e8c6bd..cea9b1dd4 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -33,6 +33,7 @@ import BoltProtocolV5x5 from './bolt-protocol-v5x5.js' import BoltProtocolV5x6 from './bolt-protocol-v5x6.js' import BoltProtocolV5x7 from './bolt-protocol-v5x7.js' import BoltProtocolV5x8 from './bolt-protocol-v5x8.js' +import BoltProtocolV6x0 from './bolt-protocol-v6x0.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -266,6 +267,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 6.0: + return new BoltProtocolV6x0(server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js index ddf9bccca..adcf1624f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js @@ -19,7 +19,7 @@ import { alloc } from '../channel/index.js' import { newError } from '../../core/index.ts' const BOLT_MAGIC_PREAMBLE = 0x6060b017 -const AVAILABLE_BOLT_PROTOCOLS = ['5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference +const AVAILABLE_BOLT_PROTOCOLS = ['6.0', '5.8', '5.7', '5.6', '5.4', '5.3', '5.2', '5.1', '5.0', '4.4', '4.3', '4.2', '3.0'] // bolt protocols the client will accept, ordered by preference const DESIRED_CAPABILITES = 0 function version (major, minor) { diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 3a341708b..135e19afb 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -101,6 +101,7 @@ import * as json from './json.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import ClientCertificate, { clientCertificateProviders, ClientCertificateProvider, ClientCertificateProviders, RotatingClientCertificateProvider, resolveCertificateProvider } from './client-certificate.ts' import * as internal from './internal/index.ts' +import Vector, { VectorType, vector } from './vector.ts' /** * Object containing string constants representing predefined {@link Neo4jError} codes. @@ -263,7 +264,9 @@ export { notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } export type { @@ -294,7 +297,8 @@ export type { ClientCertificate, ClientCertificateProvider, ClientCertificateProviders, - RotatingClientCertificateProvider + RotatingClientCertificateProvider, + VectorType } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index b45034da9..7403677c3 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -40,6 +40,7 @@ const BOLT_PROTOCOL_V5_5: number = 5.5 const BOLT_PROTOCOL_V5_6: number = 5.6 const BOLT_PROTOCOL_V5_7: number = 5.7 const BOLT_PROTOCOL_V5_8: number = 5.8 +const BOLT_PROTOCOL_V6_0: number = 6.0 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -76,5 +77,6 @@ export { BOLT_PROTOCOL_V5_6, BOLT_PROTOCOL_V5_7, BOLT_PROTOCOL_V5_8, + BOLT_PROTOCOL_V6_0, TELEMETRY_APIS } diff --git a/packages/neo4j-driver-deno/lib/core/vector.ts b/packages/neo4j-driver-deno/lib/core/vector.ts new file mode 100644 index 000000000..4de021d9e --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/vector.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { newError } from './error.ts' + +type EnumRecord = { [key in T]: key } + +export type VectorType = 'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64' +/** + * @typedef {'INT8' | 'INT16' | 'INT32' | 'INT64' | 'FLOAT32' | 'FLOAT64'} VectorType + */ +const vectorTypes: EnumRecord = { + INT8: 'INT8', + INT16: 'INT16', + INT32: 'INT32', + INT64: 'INT64', + FLOAT32: 'FLOAT32', + FLOAT64: 'FLOAT64' +} +Object.freeze(vectorTypes) + +/** + * A wrapper class for JavaScript TypedArrays that makes the driver send them as a Vector type to the database. + * @access public + * @exports Vector + * @class A Integer class for representing a 64 bit two's-complement integer value. + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray The TypedArray to convert to a vector + * + * @constructor + * + */ +export default class Vector { + typedArray: K + type: VectorType + constructor (typedArray: K) { + if (typedArray instanceof Int8Array) { + this.type = vectorTypes.INT8 + } else if (typedArray instanceof Int16Array) { + this.type = vectorTypes.INT16 + } else if (typedArray instanceof Int32Array) { + this.type = vectorTypes.INT32 + } else if (typedArray instanceof BigInt64Array) { + this.type = vectorTypes.INT64 + } else if (typedArray instanceof Float32Array) { + this.type = vectorTypes.FLOAT32 + } else if (typedArray instanceof Float64Array) { + this.type = vectorTypes.FLOAT64 + } else { + throw newError(`The neo4j Vector class is a wrapper for TypedArrays. got ${typeof typedArray}`) + } + this.typedArray = typedArray + } + + toTypedArray (): K { + return this.typedArray + } +} + +/** + * Cast a TypedArray to a {@link Vector} + * @access public + * @param {Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | BigInt64Array} typedArray - The value to use. + * @return {Vector} - The Neo4j Vector ready to be used as a query parameter + */ +export function vector (typedArray: K): Vector { + return new Vector(typedArray) +} diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 911ad9fcd..1330ad597 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -78,7 +78,9 @@ import { notificationFilterMinimumSeverityLevel, staticAuthTokenManager, clientCertificateProviders, - resolveCertificateProvider + resolveCertificateProvider, + Vector, + vector } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -282,7 +284,8 @@ const types = { LocalDateTime, LocalTime, Time, - Integer + Integer, + Vector } /** @@ -402,7 +405,8 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector } export { @@ -474,6 +478,8 @@ export { notificationFilterDisabledCategory, notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, - clientCertificateProviders + clientCertificateProviders, + vector, + Vector } export default forExport diff --git a/packages/neo4j-driver/test/vector-examples.test.js b/packages/neo4j-driver/test/vector-examples.test.js new file mode 100644 index 000000000..e6d500a0a --- /dev/null +++ b/packages/neo4j-driver/test/vector-examples.test.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import neo4j from '../src' +import sharedNeo4j from './internal/shared-neo4j' + +describe('#integration vector type api suggestion', () => { + let driverGlobal + let protocolVersion + const uri = `bolt://${sharedNeo4j.hostnameWithBoltPort}` + + beforeAll(async () => { + driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken) + const tmpDriver = neo4j.driver( + `bolt://${sharedNeo4j.hostnameWithBoltPort}`, + sharedNeo4j.authToken + ) + protocolVersion = await sharedNeo4j.cleanupAndGetProtocolVersion(tmpDriver) + await tmpDriver.close() + }) + + beforeEach(async () => { + const driver = driverGlobal + const session = driver.session() + await session.run('MATCH (n) DETACH DELETE n') + await session.close() + }) + + afterAll(async () => { + await driverGlobal.close() + }) + + it('write and read vectors', async () => { + if (protocolVersion >= 6.0) { + const driver = driverGlobal + + const bufferWriter = Uint8Array.from([1, 1]) + await driver.executeQuery('CREATE (p:Product) SET p.vector_from_array = $vector_from_array, p.vector_from_buffer = $vector_from_buffer', { + vector_from_array: neo4j.vector(Float32Array.from([1, 2, 3, 4])), // Typed arrays can be created from a regular list of Numbers + vector_from_buffer: neo4j.vector(new Uint8Array(bufferWriter.buffer)) // Or from a bytebuffer + }) + const res = await driver.executeQuery('MATCH (p:Product) RETURN p.vector_from_array as arrayVector, p.vector_from_buffer as bufferVector') + + const arrayVec = res.records[0].get('arrayVector').toTypedArray() + const bufferVec = res.records[0].get('bufferVector').toTypedArray() + + expect(arrayVec[0]).toBe(1) + expect(bufferVec[1]).toBe(1) + } + }) +})