diff --git a/.babelrc b/.babelrc index 15ce28a39..fd7a89fc3 100644 --- a/.babelrc +++ b/.babelrc @@ -5,6 +5,13 @@ ] ], "plugins": [ - "@babel/plugin-transform-runtime" - ] + "@babel/plugin-transform-runtime", + ], + "env": { + "test": { + "plugins": [ + "istanbul" + ] + } + } } diff --git a/.gitignore b/.gitignore index dc27dcc1f..064b8e33c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ node_modules docs/build .npmrc *.iml -/lib \ No newline at end of file +/lib +.nyc_output +coverage +.vscode +*.code-workspace \ No newline at end of file diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000..8cd3fdbe9 --- /dev/null +++ b/.nycrc @@ -0,0 +1,4 @@ +{ + "extends": "@istanbuljs/nyc-config-babel", + "reporter": ["text", "lcov"] +} \ No newline at end of file diff --git a/README.md b/README.md index 1b12ae7fc..861d3e18a 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,27 @@ A database driver for Neo4j 3.0.0+. Resources to get you started: -- [Detailed docs](http://neo4j.com/docs/api/javascript-driver/current/). -- [Sample small project using the driver](https://github.com/neo4j-examples/movies-javascript-bolt) -- [Sample application using the driver](https://github.com/neo4j-examples/neo4j-movies-template) +- Detailed docs _Not available yet_ - [Neo4j Manual](https://neo4j.com/docs/) - [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) -## Include module in Node.js application +## What's New + +- Introduces a brand new reactive API (built on top of RxJS) available with 4.0 version server, which includes reactive protocol improvements. +- Session instances can now be acquired against a specific database against a multi-database server, which is available with 4.0 version server. +- A new `driver.verifyConnectivity()` method is introduced for connectivity verification purposes. + +## Breaking Changes + +- Driver API is moved from `neo4j.v1` to `neo4j` namespace. +- `driver#session()` method now makes use of object destructuring rather than positional arguments (see [Acquiring a Session](#acquiring-a-session) for examples). +- `session#close()` now returns a `Promise` and no more accepts a callback function argument. +- `driver.onError` and `driver.onCompleted` callbacks are completely removed. Errors should be monitored on related code paths (i.e. through `Promise#catch`, etc.). +- `bolt+routing` scheme is now renamed to `neo4j`. `neo4j` scheme is designed to work work with all possible 4.0 server deployments, but `bolt` scheme is still available for explicit single instance connections. + +## Including the Driver + +### In Node.js application Stable channel: @@ -39,7 +53,7 @@ driver.close() otherwise application shutdown might hang or it might exit with a non-zero exit code. -## Include in web browser +### 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: @@ -63,13 +77,11 @@ This will make a global `neo4j` object available, where you can access the drive ```javascript var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j') ) ``` -\* Since 2.0, driver API is moved from `neo4j.v1` to `neo4j`. - 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: @@ -80,13 +92,13 @@ driver.close() ## Usage examples -Driver lifecycle: +### Constructing a Driver ```javascript // Create a driver instance, for the user neo4j with password neo4j. // It should be enough to have a single driver per database per application. var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j') ) @@ -95,66 +107,162 @@ var driver = neo4j.driver( driver.close() ``` -Session API: +### 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 +}) +``` + +### Executing Statements +#### 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', { + .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { nameParam: 'Alice' }) .subscribe({ - onNext: function(record) { + onKeys: keys => { + console.log(keys) + }, + onNext: record => { console.log(record.get('name')) }, - onCompleted: function() { + onCompleted: () => { session.close() }, - onError: function(error) { + 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. -// or +#### 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', { + .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { nameParam: 'James' }) - .then(function(result) { - result.records.forEach(function(record) { + .then(result => { + result.records.forEach(record => { console.log(record.get('name')) }) - session.close() }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) ``` -Transaction functions API: +#### 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')), + concat(rxSession.close()) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(err) + }) +``` + +### 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: -neo4j.driver('bolt://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { +// Applies both to standard and reactive sessions. +neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { 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.readTransaction(function(transaction) { +var readTxResultPromise = session.readTransaction(txc => { // used transaction will be committed automatically, no need for explicit commit/rollback - var result = transaction.run( - 'MATCH (person:Person) RETURN person.name AS name' - ) + 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 @@ -162,152 +270,165 @@ var readTxResultPromise = session.readTransaction(function(transaction) { // returned Promise can be later consumed like this: readTxResultPromise - .then(function(result) { - session.close() + .then(result => { console.log(result.records) }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) +``` + +#### Reading with Reactive Session + +```javascript +rxSession + .readTransaction(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.writeTransaction(function(transaction) { +var writeTxResultPromise = session.writeTransaction(async txc => { // used transaction will be committed automatically, no need for explicit commit/rollback - var result = transaction.run( + 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(function(record) { - return record.get('name') - }) + return result.records.map(record => record.get('name')) }) // returned Promise can be later consumed like this: writeTxResultPromise - .then(function(namesArray) { - session.close() + .then(namesArray => { console.log(namesArray) }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) ``` -Explicit transactions API: +#### Writing with Reactive Session ```javascript -// run statement in a transaction -var tx = session.beginTransaction() - -tx.run('MERGE (bob:Person {name : {nameParam} }) RETURN bob.name AS name', { - nameParam: 'Bob' -}).subscribe({ - onNext: function(record) { - console.log(record.get('name')) - }, - onCompleted: function() { - console.log('First query completed') - }, - onError: function(error) { - console.log(error) - } -}) +rxSession + .writeTransaction(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) + }) +``` -tx.run('MERGE (adam:Person {name : {nameParam} }) RETURN adam.name AS name', { - nameParam: 'Adam' -}).subscribe({ - onNext: function(record) { - console.log(record.get('name')) - }, - onCompleted: function() { - console.log('Second query completed') - }, - onError: function(error) { - console.log(error) - } -}) +### Explicit Transactions -//decide if the transaction should be committed or rolled back -var success = false +#### With Async Session -if (success) { - tx.commit().subscribe({ - onCompleted: function() { - // this transaction is now committed and session can be closed - session.close() - }, - onError: function(error) { - console.log(error) +```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' } - }) -} else { - //transaction is rolled black and nothing is created in the database + ) + 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') - tx.rollback() +} finally { + await session.close() } ``` -Subscriber API allows following combinations of `onNext`, `onCompleted` and `onError` callback invocations: - -- 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 - -## Parallelization - -In a single session, multiple queries will be executed serially. In order to parallelize queries, multiple sessions are required. - -## Building - - npm install - npm run build - -This produces browser-compatible standalone files under `lib/browser` and a Node.js module version under `lib/`. -See files under `examples/` on how to use. - -## Testing - -Tests **require** latest [Boltkit](https://github.com/neo4j-contrib/boltkit) to be installed in the system. It is needed to start, stop and configure local test database. Boltkit can be installed with the following command: - - pip install --upgrade boltkit - -To run tests against "default" Neo4j version: - - ./runTests.sh +#### With Reactive Session -To run tests against specified Neo4j version: - -./runTests.sh '-e 3.1.3' - -Simple `npm test` can also be used if you already have a running version of a compatible Neo4j server. - -For development, you can have the build tool rerun the tests each time you change -the source code: - - gulp watch-n-test - -### Testing on windows - -Running tests on windows requires PhantomJS installed and its bin folder added in windows system variable `Path`. -To run the same test suite, run `.\runTest.ps1` instead in powershell with admin right. -The admin right is required to start/stop Neo4j properly as a system service. -While there is no need to grab admin right if you are running tests against an existing Neo4j server using `npm test`. +```javascript +rxSession + .beginTransaction() + .pipe( + flatMap(txc => + concat( + 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) + }) +``` -## A note on numbers and the Integer type +### Numbers and the Integer type The Neo4j type system includes 64-bit integer values. However, JavaScript can only safely represent integers between `-(2``53``- 1)` and `(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. -### Write integers +_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ -Number written directly e.g. `session.run("CREATE (n:Node {age: {age}})", {age: 22})` will be of type `Float` in Neo4j. +#### 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 @@ -324,7 +445,7 @@ session.run('CREATE (n {age: {myIntParam}})', { }) ``` -### Read integers +#### Reading integers Since Integers can be larger than can be represented as JavaScript numbers, it is only safe to convert to JavaScript numbers if you know that they will not exceed `(2``53``- 1)` in size. In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. @@ -345,7 +466,7 @@ if (!neo4j.integer.inSafeRange(aLargerInteger)) { } ``` -### Enable native numbers +#### 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 @@ -354,8 +475,45 @@ To enable potentially lossy integer values use the driver's configuration object ```javascript var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { disableLosslessIntegers: true } ) ``` + +## Building + + npm install + npm run build + +This produces browser-compatible standalone files under `lib/browser` and a Node.js module version under `lib/`. +See files under `examples/` on how to use. + +## Testing + +Tests **require** latest [Boltkit](https://github.com/neo4j-contrib/boltkit) and [Firefox](https://www.mozilla.org/firefox/) to be installed in the system. + +Boltkit is needed to start, stop and configure local test database. Boltkit can be installed with the following command: + + pip3 install --upgrade boltkit + +To run tests against "default" Neo4j version: + + ./runTests.sh + +To run tests against specified Neo4j version: + +./runTests.sh '-e 3.1.3' + +Simple `npm test` can also be used if you already have a running version of a compatible Neo4j server. + +For development, you can have the build tool rerun the tests each time you change +the source code: + + gulp watch-n-test + +### Testing on windows + +To run the same test suite, run `.\runTest.ps1` instead in powershell with admin right. +The admin right is required to start/stop Neo4j properly as a system service. +While there is no need to grab admin right if you are running tests against an existing Neo4j server using `npm test`. diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 81a7492f5..7b5814577 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -201,7 +201,9 @@ gulp.task('run-ts-declaration-tests', function (done) { target: 'es6', noImplicitAny: true, noImplicitReturns: true, - strictNullChecks: true + strictNullChecks: true, + moduleResolution: 'node', + types: [] }) ) .pipe(gulp.dest('build/test/types')) diff --git a/package-lock.json b/package-lock.json index 8c431c827..02b5c1dfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1577,6 +1577,12 @@ } } }, + "@istanbuljs/nyc-config-babel": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-babel/-/nyc-config-babel-2.1.1.tgz", + "integrity": "sha512-cWcUCqHOYB+Mpumsv03uaE7rMvtmJn7pZ3llc+9gyqMFC93IVcUuuJ/mknoWsiuajcEjRCqKmhGaiAaXG6kzLA==", + "dev": true + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1921,6 +1927,13 @@ "integrity": "sha1-S4Mee1MUFafMUYzUBOc/YZPGNJ0=", "dev": true }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -2006,6 +2019,15 @@ "buffer-equal": "^1.0.0" } }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -2415,6 +2437,63 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + } + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -3070,6 +3149,42 @@ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", "dev": true }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -3704,6 +3819,43 @@ } } }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -4037,6 +4189,15 @@ } } }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, "default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", @@ -4536,6 +4697,12 @@ "next-tick": "^1.0.0" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -5838,6 +6005,28 @@ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", "dev": true }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7370,6 +7559,26 @@ "glogg": "^1.0.0" } }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -7500,6 +7709,15 @@ "minimalistic-assert": "^1.0.1" } }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, "hat": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", @@ -8435,62 +8653,308 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "istextorbinary": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", - "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", - "dev": true, - "requires": { - "binaryextensions": "2", - "editions": "^1.3.3", - "textextensions": "2" - } - }, - "jasmine": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", - "integrity": "sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ==", + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { - "glob": "^7.1.3", - "jasmine-core": "~3.4.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } } }, - "jasmine-core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz", - "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", "dev": true }, - "jasmine-spec-reporter": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", - "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", "dev": true, "requires": { - "colors": "1.1.2" - }, - "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - } + "append-transform": "^1.0.0" } }, - "jasmine-terminal-reporter": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/jasmine-terminal-reporter/-/jasmine-terminal-reporter-1.0.3.tgz", - "integrity": "sha1-iW8eyP30v2rs3UHFA+2nNH9hUms=", + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", "dev": true, "requires": { - "indent-string": "^2.1.0", - "pluralize": "^1.2.1" + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" } }, - "js-levenshtein": { - "version": "1.1.6", + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "istextorbinary": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", + "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", + "dev": true, + "requires": { + "binaryextensions": "2", + "editions": "^1.3.3", + "textextensions": "2" + } + }, + "jasmine": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", + "integrity": "sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "jasmine-core": "~3.4.0" + } + }, + "jasmine-core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz", + "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + } + } + }, + "jasmine-terminal-reporter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/jasmine-terminal-reporter/-/jasmine-terminal-reporter-1.0.3.tgz", + "integrity": "sha1-iW8eyP30v2rs3UHFA+2nNH9hUms=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "pluralize": "^1.2.1" + } + }, + "js-levenshtein": { + "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", "dev": true @@ -9172,9 +9636,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "dev": true }, "lodash._basecopy": { @@ -9270,6 +9734,12 @@ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", "dev": true }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", @@ -9318,9 +9788,9 @@ "dev": true }, "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha1-rcJdnLmbk5HFliTzefu6YNcRHVQ=", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "lodash.pick": { @@ -9698,6 +10168,23 @@ "readable-stream": "^2.0.1" } }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "messageformat": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-1.1.1.tgz", @@ -9859,9 +10346,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -9871,7 +10358,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -10059,6 +10546,12 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -10301,6 +10794,215 @@ "dev": true, "optional": true }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -10519,6 +11221,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", @@ -10584,6 +11292,18 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", @@ -12234,6 +12954,15 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -12612,10 +13341,9 @@ } }, "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha1-87sP572n+2nerAwW8XtQsLh5BQQ=", - "dev": true, + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", "requires": { "tslib": "^1.9.0" } @@ -12702,9 +13430,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -13115,6 +13843,20 @@ "integrity": "sha1-AI22XtzmxQ7sDF4ijhlFBh3QQ3w=", "dev": true }, + "spawn-wrap": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", + "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -13662,6 +14404,127 @@ } } }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + } + } + }, "text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -13882,8 +14745,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY=", - "dev": true + "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY=" }, "tty-browserify": { "version": "0.0.1", @@ -14095,38 +14957,15 @@ "dev": true }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-filename": { @@ -14818,6 +15657,17 @@ "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", diff --git a/package.json b/package.json index 24847c0c2..5581592dc 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,10 @@ "@babel/plugin-transform-runtime": "^7.4.4", "@babel/preset-env": "^7.4.4", "@babel/register": "^7.4.4", + "@istanbuljs/nyc-config-babel": "^2.1.1", "async": "^2.6.2", "babel-eslint": "^10.0.1", + "babel-plugin-istanbul": "^5.2.0", "babelify": "^10.0.0", "browserify": "^16.2.3", "browserify-transform-tools": "^1.7.0", @@ -74,6 +76,7 @@ "gulp-uglify": "^3.0.2", "gulp-watch": "^5.0.1", "husky": "^2.3.0", + "istanbul": "^0.4.5", "jasmine-spec-reporter": "^4.2.1", "karma": "^4.1.0", "karma-browserify": "^6.0.0", @@ -85,10 +88,11 @@ "karma-source-map-support": "^1.4.0", "karma-spec-reporter": "^0.0.32", "lint-staged": "^8.1.6", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "lolex": "^4.0.1", "minimist": "^1.2.0", "mustache": "^3.0.1", + "nyc": "^14.1.1", "prettier-eslint": "^8.8.2", "prettier-eslint-cli": "^4.7.1", "run-sequence": "^2.2.1", @@ -101,6 +105,7 @@ }, "dependencies": { "@babel/runtime": "^7.4.4", + "rxjs": "^6.5.2", "text-encoding-utf-8": "^1.0.2", "uri-js": "^4.2.2" } diff --git a/src/docs.js b/src/docs.js new file mode 100644 index 000000000..2c1071bdf --- /dev/null +++ b/src/docs.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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. + */ + +/** + * Configuration object containing settings for explicit and auto-commit transactions. + *

+ * Configuration is supported for: + *

+ * @typedef {Object} TransactionConfig + * @property {number} timeout - the transaction timeout in **milliseconds**. Transactions that execute longer than the configured timeout will + * be terminated by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default timeout + * configured in the database using `dbms.transaction.timeout` setting. Value should not represent a duration of zero or negative duration. + * @property {Object} metadata - the transaction metadata. Specified metadata will be attached to the executing transaction and visible in the output of + * `dbms.listQueries` and `dbms.listTransactions` procedures. It will also get logged to the `query.log`. This functionality makes it easier to tag + * transactions and is equivalent to `dbms.setTXMetaData` procedure. + */ diff --git a/src/driver.js b/src/driver.js index 9b555b891..044809457 100644 --- a/src/driver.js +++ b/src/driver.js @@ -17,20 +17,19 @@ * limitations under the License. */ -import Session from './session' -import Pool from './internal/pool' -import ChannelConnection from './internal/connection-channel' -import { newError, SERVICE_UNAVAILABLE } from './error' -import DirectConnectionProvider from './internal/connection-provider-direct' +import { newError } from './error' +import ConnectionProvider from './internal/connection-provider' import Bookmark from './internal/bookmark' +import DirectConnectionProvider from './internal/connection-provider-direct' import ConnectivityVerifier from './internal/connectivity-verifier' -import PoolConfig, { +import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import Logger from './internal/logger' +import { DEFAULT_ACQUISITION_TIMEOUT, DEFAULT_MAX_SIZE } from './internal/pool-config' -import Logger from './internal/logger' -import ConnectionErrorHandler from './internal/connection-error-handler' -import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import Session from './session' +import RxSession from './session-rx' const DEFAULT_MAX_CONNECTION_LIFETIME = 60 * 60 * 1000 // 1 hour @@ -64,11 +63,11 @@ class Driver { /** * You should not be calling this directly, instead use {@link driver}. * @constructor + * @protected * @param {ServerAddress} address * @param {string} userAgent - * @param {object} authToken - * @param {object} config - * @protected + * @param {Object} authToken + * @param {Object} config */ constructor (address, userAgent, authToken = {}, config = {}) { sanitizeConfig(config) @@ -90,19 +89,13 @@ class Driver { this._afterConstruction() } - /** - * @protected - */ - _afterConstruction () { - this._log.info( - `Direct driver ${this._id} created for server address ${this._address}` - ) - } - /** * Verifies connectivity of this driver by trying to open a connection with the provided driver options. - * @param {string} [database=''] the target database to verify connectivity for. - * @returns {Promise} promise resolved with server info or rejected with error. + * + * @public + * @param {Object} param - The object parameter + * @param {string} param.database - the target database to verify connectivity for. + * @returns {Promise} promise resolved with server info or rejected with error. */ verifyConnectivity ({ database = '' } = {}) { const connectionProvider = this._getOrCreateConnectionProvider() @@ -121,10 +114,12 @@ class Driver { * it is closed, the underlying connection will be released to the connection * pool and made available for others to use. * - * @param {string} [defaultAccessMode=WRITE] the access mode of this session, allowed values are {@link READ} and {@link WRITE}. - * @param {string|string[]} [bookmarks=null] the initial reference or references to some previous + * @public + * @param {Object} param - The object parameter + * @param {string} param.defaultAccessMode=WRITE - the access mode of this session, allowed values are {@link READ} and {@link WRITE}. + * @param {string|string[]} param.bookmarks - the initial reference or references to some previous * transactions. Value is optional and absence indicates that that the bookmarks do not exist or are unknown. - * @param {string} [database=''] the database this session will connect to. + * @param {string} param.database - the database this session will operate on. * @return {Session} new session. */ session ({ @@ -132,29 +127,69 @@ class Driver { bookmarks: bookmarkOrBookmarks, database = '' } = {}) { - const sessionMode = Driver._validateSessionMode(defaultAccessMode) - const connectionProvider = this._getOrCreateConnectionProvider() - const bookmark = bookmarkOrBookmarks - ? new Bookmark(bookmarkOrBookmarks) - : Bookmark.empty() - return new Session({ - mode: sessionMode, + return this._newSession({ + defaultAccessMode, + bookmarkOrBookmarks, database, - connectionProvider, - bookmark, + reactive: false + }) + } + + /** + * Acquire a reactive session to communicate with the database. The session will + * borrow connections from the underlying connection pool as required and + * should be considered lightweight and disposable. + * + * This comes with some responsibility - make sure you always call + * {@link close} when you are done using a session, and likewise, + * make sure you don't close your session before you are done using it. Once + * it is closed, the underlying connection will be released to the connection + * pool and made available for others to use. + * + * @public + * @param {Object} param + * @param {string} param.defaultAccessMode=WRITE - the access mode of this session, allowed values are {@link READ} and {@link WRITE} + * @param {string|string[]} param.bookmarks - the initial reference or references to some previous transactions. Value is optional and + * absence indicates that the bookmarks do not exist or are unknown. + * @param {string} param.database - the database this session will operate on. + * @returns {RxSession} new reactive session. + */ + rxSession ({ defaultAccessMode = WRITE, bookmarks, database = '' } = {}) { + return new RxSession({ + session: this._newSession({ + defaultAccessMode, + bookmarks, + database, + reactive: true + }), config: this._config }) } - static _validateSessionMode (rawMode) { - const mode = rawMode || WRITE - if (mode !== ACCESS_MODE_READ && mode !== ACCESS_MODE_WRITE) { - throw newError('Illegal session mode ' + mode) + /** + * Close all open sessions and other associated resources. You should + * make sure to use this when you are done with this driver instance. + * @public + */ + close () { + this._log.info(`Driver ${this._id} closing`) + if (this._connectionProvider) { + this._connectionProvider.close() } - return mode } - // Extension point + /** + * @protected + */ + _afterConstruction () { + this._log.info( + `Direct driver ${this._id} created for server address ${this._address}` + ) + } + + /** + * @protected + */ _createConnectionProvider (address, userAgent, authToken) { return new DirectConnectionProvider({ id: this._id, @@ -166,6 +201,39 @@ class Driver { }) } + /** + * @protected + */ + static _validateSessionMode (rawMode) { + const mode = rawMode || WRITE + if (mode !== ACCESS_MODE_READ && mode !== ACCESS_MODE_WRITE) { + throw newError('Illegal session mode ' + mode) + } + return mode + } + + /** + * @private + */ + _newSession ({ defaultAccessMode, bookmarkOrBookmarks, database, reactive }) { + const sessionMode = Driver._validateSessionMode(defaultAccessMode) + const connectionProvider = this._getOrCreateConnectionProvider() + const bookmark = bookmarkOrBookmarks + ? new Bookmark(bookmarkOrBookmarks) + : Bookmark.empty() + return new Session({ + mode: sessionMode, + database, + connectionProvider, + bookmark, + config: this._config, + reactive + }) + } + + /** + * @private + */ _getOrCreateConnectionProvider () { if (!this._connectionProvider) { this._connectionProvider = this._createConnectionProvider( @@ -177,18 +245,6 @@ class Driver { return this._connectionProvider } - - /** - * Close all open sessions and other associated resources. You should - * make sure to use this when you are done with this driver instance. - * @return undefined - */ - close () { - this._log.info(`Driver ${this._id} closing`) - if (this._connectionProvider) { - this._connectionProvider.close() - } - } } /** @@ -209,6 +265,9 @@ function sanitizeConfig (config) { ) } +/** + * @private + */ function sanitizeIntValue (rawValue, defaultWhenAbsent) { const sanitizedValue = parseInt(rawValue, 10) if (sanitizedValue > 0 || sanitizedValue === 0) { diff --git a/src/graph-types.js b/src/graph-types.js index 0a355c240..a87bec704 100644 --- a/src/graph-types.js +++ b/src/graph-types.js @@ -23,16 +23,32 @@ class Node { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {Array} labels - Array for all labels * @param {Object} properties - Map with node properties */ constructor (identity, labels, properties) { + /** + * Identity of the node. + * @type {Integer} + */ this.identity = identity + /** + * Labels of the node. + * @type {string[]} + */ this.labels = labels + /** + * Properties of the node. + * @type {Object} + */ this.properties = properties } + /** + * @ignore + */ toString () { let s = '(' + this.identity for (let i = 0; i < this.labels.length; i++) { @@ -58,6 +74,7 @@ class Node { class Relationship { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {Integer} start - Identity of start Node * @param {Integer} end - Identity of end Node @@ -65,13 +82,36 @@ class Relationship { * @param {Object} properties - Map with relationship properties */ constructor (identity, start, end, type, properties) { + /** + * Identity of the relationship. + * @type {Integer} + */ this.identity = identity + /** + * Identity of the start node. + * @type {Integer} + */ this.start = start + /** + * Identity of the end node. + * @type {Integer} + */ this.end = end + /** + * Type of the relationship. + * @type {string} + */ this.type = type + /** + * Properties of the relationship. + * @type {Object} + */ this.properties = properties } + /** + * @ignore + */ toString () { let s = '(' + this.start + ')-[:' + this.type let keys = Object.keys(this.properties) @@ -95,18 +135,33 @@ class Relationship { class UnboundRelationship { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {string} type - Relationship type * @param {Object} properties - Map with relationship properties */ constructor (identity, type, properties) { + /** + * Identity of the relationship. + * @type {Integer} + */ this.identity = identity + /** + * Type of the relationship. + * @type {string} + */ this.type = type + /** + * Properties of the relationship. + * @type {Object} + */ this.properties = properties } /** * Bind relationship + * + * @protected * @param {Integer} start - Identity of start node * @param {Integer} end - Identity of end node * @return {Relationship} - Created relationship @@ -121,6 +176,9 @@ class UnboundRelationship { ) } + /** + * @ignore + */ toString () { let s = '-[:' + this.type let keys = Object.keys(this.properties) @@ -143,13 +201,26 @@ class UnboundRelationship { class PathSegment { /** * @constructor + * @protected * @param {Node} start - start node * @param {Relationship} rel - relationship that connects start and end node * @param {Node} end - end node */ constructor (start, rel, end) { + /** + * Start node. + * @type {Node} + */ this.start = start + /** + * Relationship. + * @type {Relationship} + */ this.relationship = rel + /** + * End node. + * @type {Node} + */ this.end = end } } @@ -160,14 +231,31 @@ class PathSegment { class Path { /** * @constructor + * @protected * @param {Node} start - start node * @param {Node} end - end node * @param {Array} segments - Array of Segments */ constructor (start, end, segments) { + /** + * Start node. + * @type {Node} + */ this.start = start + /** + * End node. + * @type {Node} + */ this.end = end + /** + * Segments. + * @type {Array} + */ this.segments = segments + /** + * Length of the segments. + * @type {Number} + */ this.length = segments.length } } diff --git a/src/index.js b/src/index.js index de81a1cb0..f949750ae 100644 --- a/src/index.js +++ b/src/index.js @@ -232,7 +232,7 @@ const logging = { * } * * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. * @param {Object} config Configuration object. See the configuration section above for details. * @returns {Driver} */ diff --git a/src/integer.js b/src/integer.js index ccbae3223..c4ee7edc0 100644 --- a/src/integer.js +++ b/src/integer.js @@ -854,7 +854,7 @@ Integer.toNumber = function (val) { * @access private * @param {!Integer|number|string|!{low: number, high: number}} val Value * @param {number} radix optional radix for string conversion, defaults to 10 - * @returns {String} + * @returns {string} * @expose */ Integer.toString = function (val, radix) { @@ -1007,7 +1007,7 @@ let toNumber = Integer.toNumber * @access public * @param {Mixed} value - The variable to convert * @param {number} radix - radix to use in string conversion, defaults to 10 - * @return {String} - returns a string representation of the integer + * @return {string} - returns a string representation of the integer */ let toString = Integer.toString diff --git a/src/internal/bolt-protocol-util.js b/src/internal/bolt-protocol-util.js index 07d51cec7..5b8031d5b 100644 --- a/src/internal/bolt-protocol-util.js +++ b/src/internal/bolt-protocol-util.js @@ -17,11 +17,12 @@ * limitations under the License. */ import { newError } from '../error' +import { ResultStreamObserver } from './stream-observers' /** * @param {TxConfig} txConfig the auto-commit transaction configuration. * @param {Connection} connection the connection. - * @param {StreamObserver} observer the response observer. + * @param {ResultStreamObserver} observer the response observer. */ function assertTxConfigIsEmpty (txConfig, connection, observer) { if (txConfig && !txConfig.isEmpty()) { @@ -41,7 +42,6 @@ function assertTxConfigIsEmpty (txConfig, connection, observer) { * Asserts that the passed-in database name is empty. * @param {string} database * @param {Connection} connection - * @param {StreamObserver} observer */ function assertDatabaseIsEmpty (database, connection, observer) { if (database) { diff --git a/src/internal/bolt-protocol-v1.js b/src/internal/bolt-protocol-v1.js index db23b231b..6b2d3de89 100644 --- a/src/internal/bolt-protocol-v1.js +++ b/src/internal/bolt-protocol-v1.js @@ -21,10 +21,19 @@ import * as v1 from './packstream-v1' import Bookmark from './bookmark' import TxConfig from './tx-config' import { ACCESS_MODE_WRITE } from './constants' +import Connection from './connection' +import { Chunker } from './chunking' +import { Packer } from './packstream-v1' import { assertDatabaseIsEmpty, assertTxConfigIsEmpty } from './bolt-protocol-util' +import { + ResultStreamObserver, + LoginObserver, + ResetObserver, + StreamObserver +} from './stream-observers' export default class BoltProtocol { /** @@ -57,8 +66,8 @@ export default class BoltProtocol { /** * Transform metadata received in SUCCESS message before it is passed to the handler. - * @param {object} metadata the received metadata. - * @return {object} transformed metadata. + * @param {Object} metadata the received metadata. + * @return {Object} transformed metadata. */ transformMetadata (metadata) { return metadata @@ -66,99 +75,217 @@ export default class BoltProtocol { /** * Perform initialization and authentication of the underlying connection. - * @param {string} clientName the client name. - * @param {object} authToken the authentication token. - * @param {StreamObserver} observer the response observer. + * @param {Object} param + * @param {string} param.userAgent the user agent. + * @param {Object} param.authToken the authentication token. + * @param {function(err: Error)} param.onError the callback to invoke on error. + * @param {function()} param.onComplete the callback to invoke on completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - initialize (clientName, authToken, observer) { - const message = RequestMessage.init(clientName, authToken) - this._connection.write(message, observer, true) + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + connection: this._connection, + afterError: onError, + afterComplete: onComplete + }) + + this._connection.write( + RequestMessage.init(userAgent, authToken), + observer, + true + ) + + return observer } - prepareToClose (observer) { + /** + * Perform protocol related operations for closing this connection + */ + prepareToClose () { // no need to notify the database in this protocol version } /** * Begin an explicit transaction. - * @param {StreamObserver} observer the response observer. - * @param {Bookmark} bookmark the bookmark. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the target database name. - * @param {string} mode the access mode. + * @param {Object} param + * @param {Bookmark} param.bookmark the bookmark. + * @param {TxConfig} param.txConfig the configuration. + * @param {string} param.database the target database name. + * @param {string} param.mode the access mode. + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - beginTransaction (observer, { bookmark, txConfig, database, mode }) { - assertTxConfigIsEmpty(txConfig, this._connection, observer) - assertDatabaseIsEmpty(database, this._connection, observer) - - const runMessage = RequestMessage.run( + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + return this.run( 'BEGIN', - bookmark.asBeginTransactionParameters() + bookmark ? bookmark.asBeginTransactionParameters() : {}, + { + bookmark: bookmark, + txConfig: txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush: false + } ) - const pullAllMessage = RequestMessage.pullAll() - - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, false) } /** * Commit the explicit transaction. - * @param {StreamObserver} observer the response observer. + * @param {Object} param + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - commitTransaction (observer) { + commitTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { // WRITE access mode is used as a place holder here, it has // no effect on behaviour for Bolt V1 & V2 - this.run('COMMIT', {}, observer, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: ACCESS_MODE_WRITE - }) + return this.run( + 'COMMIT', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) } /** * Rollback the explicit transaction. - * @param {StreamObserver} observer the response observer. + * @param {Object} param + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - rollbackTransaction (observer) { + rollbackTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { // WRITE access mode is used as a place holder here, it has // no effect on behaviour for Bolt V1 & V2 - this.run('ROLLBACK', {}, observer, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: ACCESS_MODE_WRITE - }) + return this.run( + 'ROLLBACK', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) } /** * Send a Cypher statement through the underlying connection. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. - * @param {StreamObserver} observer the response observer. - * @param {Bookmark} bookmark the bookmark. - * @param {TxConfig} txConfig the auto-commit transaction configuration. - * @param {string} database the target database name. - * @param {string} mode the access mode. + * @param {Object} parameters the statement parameters. + * @param {Object} param + * @param {Bookmark} param.bookmark the bookmark. + * @param {TxConfig} param.txConfig the transaction configuration. + * @param {string} param.database the target database name. + * @param {string} param.mode the access mode. + * @param {function(keys: string[])} param.beforeKeys the callback to invoke before handling the keys. + * @param {function(keys: string[])} param.afterKeys the callback to invoke after handling the keys. + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @param {boolean} param.flush whether to flush the buffered messages. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - run (statement, parameters, observer, { bookmark, txConfig, database, mode }) { + run ( + statement, + parameters, + { + bookmark, + txConfig, + database, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + // bookmark and mode are ignored in this version of the protocol assertTxConfigIsEmpty(txConfig, this._connection, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._connection, observer) - const runMessage = RequestMessage.run(statement, parameters) - const pullAllMessage = RequestMessage.pullAll() + this._connection.write( + RequestMessage.run(statement, parameters), + observer, + false + ) + this._connection.write(RequestMessage.pullAll(), observer, flush) - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, true) + return observer } /** * Send a RESET through the underlying connection. - * @param {StreamObserver} observer the response observer. + * @param {Object} param + * @param {function(err: Error)} param.onError the callback to invoke on error. + * @param {function()} param.onComplete the callback to invoke on completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - reset (observer) { - const message = RequestMessage.reset() - this._connection.write(message, observer, true) + reset ({ onError, onComplete } = {}) { + const observer = new ResetObserver({ + connection: this._connection, + onError, + onComplete + }) + + this._connection.write(RequestMessage.reset(), observer, true) + + return observer } _createPacker (chunker) { diff --git a/src/internal/bolt-protocol-v3.js b/src/internal/bolt-protocol-v3.js index 5d0394f01..118b08350 100644 --- a/src/internal/bolt-protocol-v3.js +++ b/src/internal/bolt-protocol-v3.js @@ -19,6 +19,13 @@ import BoltProtocolV2 from './bolt-protocol-v2' import RequestMessage from './request-message' import { assertDatabaseIsEmpty } from './bolt-protocol-util' +import { + StreamObserver, + LoginObserver, + ResultStreamObserver +} from './stream-observers' + +const noOpObserver = new StreamObserver() export default class BoltProtocol extends BoltProtocolV2 { transformMetadata (metadata) { @@ -37,57 +44,138 @@ export default class BoltProtocol extends BoltProtocolV2 { return metadata } - initialize (userAgent, authToken, observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.hello(userAgent, authToken) - this._connection.write(message, observer, true) + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + connection: this._connection, + afterError: onError, + afterComplete: onComplete + }) + + this._connection.write( + RequestMessage.hello(userAgent, authToken), + observer, + true + ) + + return observer } - prepareToClose (observer) { - const message = RequestMessage.goodbye() - this._connection.write(message, observer, true) + prepareToClose () { + this._connection.write(RequestMessage.goodbye(), noOpObserver, true) } - beginTransaction (observer, { bookmark, txConfig, database, mode }) { + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._connection, observer) - prepareToHandleSingleResponse(observer) - const message = RequestMessage.begin({ bookmark, txConfig, mode }) - this._connection.write(message, observer, true) - } - commitTransaction (observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.commit() - this._connection.write(message, observer, true) + this._connection.write( + RequestMessage.begin({ bookmark, txConfig, mode }), + observer, + true + ) + + return observer } - rollbackTransaction (observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.rollback() - this._connection.write(message, observer, true) + commitTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write(RequestMessage.commit(), observer, true) + + return observer } - run (statement, parameters, observer, { bookmark, txConfig, database, mode }) { - // passing in a database name on this protocol version throws an error - assertDatabaseIsEmpty(database, this._connection, observer) + rollbackTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write(RequestMessage.rollback(), observer, true) - const runMessage = RequestMessage.runWithMetadata(statement, parameters, { + return observer + } + + run ( + statement, + parameters, + { bookmark, txConfig, - mode + database, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete }) - const pullAllMessage = RequestMessage.pullAll() - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, true) - } -} + // passing in a database name on this protocol version throws an error + assertDatabaseIsEmpty(database, this._connection, observer) -function prepareToHandleSingleResponse (observer) { - if ( - observer && - typeof observer.prepareToHandleSingleResponse === 'function' - ) { - observer.prepareToHandleSingleResponse() + this._connection.write( + RequestMessage.runWithMetadata(statement, parameters, { + bookmark, + txConfig, + mode + }), + observer, + false + ) + this._connection.write(RequestMessage.pullAll(), observer, flush) + + return observer } } diff --git a/src/internal/bolt-protocol-v4.js b/src/internal/bolt-protocol-v4.js index ca75ada8a..4a48476a5 100644 --- a/src/internal/bolt-protocol-v4.js +++ b/src/internal/bolt-protocol-v4.js @@ -18,23 +18,94 @@ */ import BoltProtocolV3 from './bolt-protocol-v3' import RequestMessage from './request-message' +import { ResultStreamObserver } from './stream-observers' export default class BoltProtocol extends BoltProtocolV3 { - beginTransaction (observer, { bookmark, txConfig, database, mode }) { - const message = RequestMessage.begin({ bookmark, txConfig, database, mode }) - this._connection.write(message, observer, true) + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write( + RequestMessage.begin({ bookmark, txConfig, database, mode }), + observer, + true + ) + + return observer } - run (statement, parameters, observer, { bookmark, txConfig, database, mode }) { - const runMessage = RequestMessage.runWithMetadata(statement, parameters, { + run ( + statement, + parameters, + { bookmark, txConfig, database, - mode + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + reactive = false + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + reactive: reactive, + moreFunction: reactive ? this._requestMore : this._noOp, + discardFunction: reactive ? this._requestDiscard : this._noOp, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete }) - const pullMessage = RequestMessage.pull() - this._connection.write(runMessage, observer, false) - this._connection.write(pullMessage, observer, true) + const flushRun = reactive + this._connection.write( + RequestMessage.runWithMetadata(statement, parameters, { + bookmark, + txConfig, + database, + mode + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this._connection.write(RequestMessage.pull(), observer, flush) + } + + return observer } + + _requestMore (connection, stmtId, n, observer) { + connection.write(RequestMessage.pull({ stmtId, n }), observer, true) + } + + _requestDiscard (connection, stmtId, observer) { + connection.write(RequestMessage.discard({ stmtId }), observer, true) + } + + _noOp () {} } diff --git a/src/internal/bookmark.js b/src/internal/bookmark.js index 94de52fa1..5d6bdc672 100644 --- a/src/internal/bookmark.js +++ b/src/internal/bookmark.js @@ -65,7 +65,7 @@ export default class Bookmark { /** * Get this bookmark as an object for begin transaction call. - * @return {object} the value of this bookmark as object. + * @return {Object} the value of this bookmark as object. */ asBeginTransactionParameters () { if (this.isEmpty()) { diff --git a/src/internal/channel-config.js b/src/internal/channel-config.js index 8436da7c5..598e8707b 100644 --- a/src/internal/channel-config.js +++ b/src/internal/channel-config.js @@ -43,7 +43,7 @@ export default class ChannelConfig { /** * @constructor * @param {ServerAddress} address the address for the channel to connect to. - * @param {object} driverConfig the driver config provided by the user when driver is created. + * @param {Object} driverConfig the driver config provided by the user when driver is created. * @param {string} connectionErrorCode the default error code to use on connection errors. */ constructor (address, driverConfig, connectionErrorCode) { diff --git a/src/internal/connection-channel.js b/src/internal/connection-channel.js index 4a6b0454f..64d6c8da6 100644 --- a/src/internal/connection-channel.js +++ b/src/internal/connection-channel.js @@ -23,6 +23,8 @@ import { newError, PROTOCOL_ERROR } from '../error' import ChannelConfig from './channel-config' import ProtocolHandshaker from './protocol-handshaker' import Connection from './connection' +import BoltProtocol from './bolt-protocol-v1' +import { ResultStreamObserver } from './stream-observers' // Signature bytes for each response message type const SUCCESS = 0x70 // 0111 0000 // SUCCESS @@ -74,6 +76,10 @@ export default class ChannelConnection extends Connection { this._dbConnectionId = null // bolt protocol is initially not initialized + /** + * @private + * @type {BoltProtocol} + */ this._protocol = null // error extracted from a FAILURE message @@ -90,7 +96,7 @@ export default class ChannelConnection extends Connection { /** * Crete new connection to the provided address. Returned connection is not connected. * @param {ServerAddress} address - the Bolt endpoint to connect to. - * @param {object} config - the driver configuration. + * @param {Object} config - the driver configuration. * @param {ConnectionErrorHandler} errorHandler - the error handler for connection errors. * @param {Logger} log - configured logger. * @return {Connection} - new connection. @@ -125,7 +131,7 @@ export default class ChannelConnection extends Connection { /** * Connect to the target address, negotiate Bolt protocol and send initialization message. * @param {string} userAgent the user agent for this driver. - * @param {object} authToken the object containing auth information. + * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ connect (userAgent, authToken) { @@ -193,13 +199,18 @@ export default class ChannelConnection extends Connection { /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. - * @param {object} authToken the object containing auth information. + * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if initialization is successful. Rejected promise otherwise. */ _initialize (userAgent, authToken) { + const self = this return new Promise((resolve, reject) => { - const observer = new InitializationObserver(this, resolve, reject) - this._protocol.initialize(userAgent, authToken, observer) + this._protocol.initialize({ + userAgent, + authToken, + onError: err => reject(err), + onComplete: () => resolve(self) + }) }) } @@ -236,7 +247,7 @@ export default class ChannelConnection extends Connection { /** * Write a message to the network channel. * @param {RequestMessage} message the message to write. - * @param {StreamObserver} observer the response observer. + * @param {ResultStreamObserver} observer the response observer. * @param {boolean} flush `true` if flush should happen after the message is written to the buffer. */ write (message, observer, flush) { @@ -251,8 +262,7 @@ export default class ChannelConnection extends Connection { .packer() .packStruct( message.signature, - message.fields.map(field => this._packable(field)), - err => this._handleFatalError(err) + message.fields.map(field => this._packable(field)) ) this._chunker.messageBoundary() @@ -365,12 +375,6 @@ export default class ChannelConnection extends Connection { resetAndFlush () { return new Promise((resolve, reject) => { this._protocol.reset({ - onNext: record => { - const neo4jError = this._handleProtocolError( - 'Received RECORD as a response for RESET: ' + JSON.stringify(record) - ) - reject(neo4jError) - }, onError: error => { if (this._isBroken) { // handling a fatal error, no need to raise a protocol violation @@ -382,7 +386,7 @@ export default class ChannelConnection extends Connection { reject(neo4jError) } }, - onCompleted: () => { + onComplete: () => { resolve() } }) @@ -391,16 +395,10 @@ export default class ChannelConnection extends Connection { _resetOnFailure () { this._protocol.reset({ - onNext: record => { - this._handleProtocolError( - 'Received RECORD as a response for RESET: ' + JSON.stringify(record) - ) - }, - // clear the current failure when response for RESET is received onError: () => { this._currentFailure = null }, - onCompleted: () => { + onComplete: () => { this._currentFailure = null } }) @@ -450,7 +448,7 @@ export default class ChannelConnection extends Connection { if (this._protocol && this.isOpen()) { // protocol has been initialized and this connection is healthy // notify the database about the upcoming close of the connection - this._protocol.prepareToClose(NO_OP_OBSERVER) + this._protocol.prepareToClose() } this._ch.close(() => { @@ -466,9 +464,7 @@ export default class ChannelConnection extends Connection { } _packable (value) { - return this._protocol - .packer() - .packable(value, err => this._handleFatalError(err)) + return this._protocol.packer().packable(value) } _handleProtocolError (message) { @@ -479,42 +475,3 @@ export default class ChannelConnection extends Connection { return error } } - -class InitializationObserver { - constructor (connection, onSuccess, onError) { - this._connection = connection - this._onSuccess = onSuccess - this._onError = onError - } - - onNext (record) { - this.onError( - newError('Received RECORD when initializing ' + JSON.stringify(record)) - ) - } - - onError (error) { - this._connection._updateCurrentObserver() // make sure this exact observer will not be called again - this._connection._handleFatalError(error) // initialization errors are fatal - - this._onError(error) - } - - onCompleted (metadata) { - if (metadata) { - // read server version from the response metadata, if it is available - const serverVersion = metadata.server - if (!this._connection.version) { - this._connection.version = serverVersion - } - - // read database connection id from the response metadata, if it is available - const dbConnectionId = metadata.connection_id - if (!this._connection.databaseId) { - this._connection.databaseId = dbConnectionId - } - } - - this._onSuccess(this._connection) - } -} diff --git a/src/internal/connection-holder.js b/src/internal/connection-holder.js index bd6cbba6b..84a7457f1 100644 --- a/src/internal/connection-holder.js +++ b/src/internal/connection-holder.js @@ -75,14 +75,10 @@ export default class ConnectionHolder { /** * Get the current connection promise. - * @param {StreamObserver} streamObserver an observer for this connection. * @return {Promise} promise resolved with the current connection. */ - getConnection (streamObserver) { - return this._connectionPromise.then(connection => { - streamObserver.resolveConnection(connection) - return connection - }) + getConnection () { + return this._connectionPromise } /** @@ -143,7 +139,7 @@ class EmptyConnectionHolder extends ConnectionHolder { // nothing to initialize } - getConnection (streamObserver) { + getConnection () { return Promise.reject( newError('This connection holder does not serve connections') ) diff --git a/src/internal/connection.js b/src/internal/connection.js index 5383beb76..64e510c5f 100644 --- a/src/internal/connection.js +++ b/src/internal/connection.js @@ -17,6 +17,9 @@ * limitations under the License. */ +import { ResultStreamObserver } from './stream-observers' +import BoltProtocol from './bolt-protocol-v1' + export default class Connection { /** * @param {ConnectionErrorHandler} errorHandler the error handler @@ -76,7 +79,7 @@ export default class Connection { /** * Connect to the target address, negotiate Bolt protocol and send initialization message. * @param {string} userAgent the user agent for this driver. - * @param {object} authToken the object containing auth information. + * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ connect (userAgent, authToken) { @@ -86,7 +89,7 @@ export default class Connection { /** * Write a message to the network channel. * @param {RequestMessage} message the message to write. - * @param {StreamObserver} observer the response observer. + * @param {ResultStreamObserver} observer the response observer. * @param {boolean} flush `true` if flush should happen after the message is written to the buffer. */ write (message, observer, flush) { diff --git a/src/internal/connectivity-verifier.js b/src/internal/connectivity-verifier.js index b47d6f24b..b05332fc7 100644 --- a/src/internal/connectivity-verifier.js +++ b/src/internal/connectivity-verifier.js @@ -19,7 +19,7 @@ import ConnectionHolder from './connection-holder' import { READ } from '../driver' -import StreamObserver from './stream-observer' +import { ResultStreamObserver } from './stream-observers' /** * Verifies connectivity using the given connection provider. @@ -54,10 +54,9 @@ function acquireAndReleaseDummyConnection (connectionProvider, database) { connectionProvider }) connectionHolder.initializeConnection() - const dummyObserver = new StreamObserver() - const connectionPromise = connectionHolder.getConnection(dummyObserver) - return connectionPromise + return connectionHolder + .getConnection() .then(connection => { // able to establish a connection return connectionHolder.close().then(() => connection.server) diff --git a/src/internal/logger.js b/src/internal/logger.js index be6b47d49..1bd56ee6c 100644 --- a/src/internal/logger.js +++ b/src/internal/logger.js @@ -48,7 +48,7 @@ class Logger { /** * Create a new logger based on the given driver configuration. - * @param {object} driverConfig the driver configuration as supplied by the user. + * @param {Object} driverConfig the driver configuration as supplied by the user. * @return {Logger} a new logger instance or a no-op logger when not configured. */ static create (driverConfig) { @@ -186,7 +186,7 @@ function isLevelEnabled (configuredLevel, targetLevel) { /** * Extract the configured logging level from the driver's logging configuration. - * @param {object} loggingConfig the logging configuration. + * @param {Object} loggingConfig the logging configuration. * @return {string} the configured log level or default when none configured. */ function extractConfiguredLevel (loggingConfig) { @@ -207,7 +207,7 @@ function extractConfiguredLevel (loggingConfig) { /** * Extract the configured logger function from the driver's logging configuration. - * @param {object} loggingConfig the logging configuration. + * @param {Object} loggingConfig the logging configuration. * @return {function(level: string, message: string)} the configured logging function. */ function extractConfiguredLogger (loggingConfig) { diff --git a/src/internal/node/node-channel.js b/src/internal/node/node-channel.js index cd6308349..1511b7261 100644 --- a/src/internal/node/node-channel.js +++ b/src/internal/node/node-channel.js @@ -181,7 +181,7 @@ function isEncrypted (config) { config.encrypted == null || config.encrypted === undefined if (encryptionNotConfigured) { // default to using encryption if trust-all-certificates is available - return true + return false } return config.encrypted === true || config.encrypted === ENCRYPTION_ON } @@ -197,7 +197,7 @@ function trustStrategyName (config) { * Create a new configuration options object for the {@code tls.connect()} call. * @param {string} hostname the target hostname. * @param {string|undefined} ca an optional CA. - * @return {object} a new options object. + * @return {Object} a new options object. */ function newTlsOptions (hostname, ca = undefined) { return { @@ -290,7 +290,7 @@ export default class NodeChannel { /** * Setup connection timeout on the socket, if configured. * @param {ChannelConfig} config - configuration of this channel. - * @param {object} socket - `net.Socket` or `tls.TLSSocket` object. + * @param {Object} socket - `net.Socket` or `tls.TLSSocket` object. * @private */ _setupConnectionTimeout (config, socket) { diff --git a/src/internal/packstream-v1.js b/src/internal/packstream-v1.js index 596734551..c9783fc12 100644 --- a/src/internal/packstream-v1.js +++ b/src/internal/packstream-v1.js @@ -108,10 +108,9 @@ class Packer { /** * Creates a packable function out of the provided value * @param x the value to pack - * @param onError callback for the case when value cannot be packed * @returns Function */ - packable (x, onError) { + packable (x) { if (x === null) { return () => this._ch.writeUInt8(NULL) } else if (x === true) { @@ -121,39 +120,36 @@ class Packer { } else if (typeof x === 'number') { return () => this.packFloat(x) } else if (typeof x === 'string') { - return () => this.packString(x, onError) + return () => this.packString(x) } else if (isInt(x)) { return () => this.packInteger(x) } else if (x instanceof Int8Array) { - return () => this.packBytes(x, onError) + return () => this.packBytes(x) } else if (x instanceof Array) { return () => { - this.packListHeader(x.length, onError) + this.packListHeader(x.length) for (let i = 0; i < x.length; i++) { - this.packable(x[i] === undefined ? null : x[i], onError)() + this.packable(x[i] === undefined ? null : x[i])() } } } else if (isIterable(x)) { - return this.packableIterable(x, onError) + return this.packableIterable(x) } else if (x instanceof Node) { return this._nonPackableValue( - `It is not allowed to pass nodes in query parameters, given: ${x}`, - onError + `It is not allowed to pass nodes in query parameters, given: ${x}` ) } else if (x instanceof Relationship) { return this._nonPackableValue( - `It is not allowed to pass relationships in query parameters, given: ${x}`, - onError + `It is not allowed to pass relationships in query parameters, given: ${x}` ) } else if (x instanceof Path) { return this._nonPackableValue( - `It is not allowed to pass paths in query parameters, given: ${x}`, - onError + `It is not allowed to pass paths in query parameters, given: ${x}` ) } else if (x instanceof Structure) { var packableFields = [] for (var i = 0; i < x.fields.length; i++) { - packableFields[i] = this.packable(x.fields[i], onError) + packableFields[i] = this.packable(x.fields[i]) } return () => this.packStruct(x.signature, packableFields) } else if (typeof x === 'object') { @@ -166,30 +162,27 @@ class Packer { count++ } } - this.packMapHeader(count, onError) + this.packMapHeader(count) for (let i = 0; i < keys.length; i++) { let key = keys[i] if (x[key] !== undefined) { this.packString(key) - this.packable(x[key], onError)() + this.packable(x[key])() } } } } else { - return this._nonPackableValue( - `Unable to pack the given value: ${x}`, - onError - ) + return this._nonPackableValue(`Unable to pack the given value: ${x}`) } } - packableIterable (iterable, onError) { + packableIterable (iterable) { try { const array = Array.from(iterable) - return this.packable(array, onError) + return this.packable(array) } catch (e) { // handle errors from iterable to array conversion - onError(newError(`Cannot pack given iterable, ${e.message}: ${iterable}`)) + throw newError(`Cannot pack given iterable, ${e.message}: ${iterable}`) } } @@ -198,9 +191,9 @@ class Packer { * @param signature the signature of the struct * @param packableFields the fields of the struct, make sure you call `packable on all fields` */ - packStruct (signature, packableFields, onError) { + packStruct (signature, packableFields) { packableFields = packableFields || [] - this.packStructHeader(packableFields.length, signature, onError) + this.packStructHeader(packableFields.length, signature) for (let i = 0; i < packableFields.length; i++) { packableFields[i]() } @@ -232,7 +225,7 @@ class Packer { this._ch.writeFloat64(x) } - packString (x, onError) { + packString (x) { let bytes = utf8.encode(x) let size = bytes.length if (size < 0x10) { @@ -255,11 +248,11 @@ class Packer { this._ch.writeUInt8(size % 256) this._ch.writeBytes(bytes) } else { - onError(newError('UTF-8 strings of size ' + size + ' are not supported')) + throw newError('UTF-8 strings of size ' + size + ' are not supported') } } - packListHeader (size, onError) { + packListHeader (size) { if (size < 0x10) { this._ch.writeUInt8(TINY_LIST | size) } else if (size < 0x100) { @@ -276,26 +269,24 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Lists of size ' + size + ' are not supported')) + throw newError('Lists of size ' + size + ' are not supported') } } - packBytes (array, onError) { + packBytes (array) { if (this._byteArraysSupported) { - this.packBytesHeader(array.length, onError) + this.packBytesHeader(array.length) for (let i = 0; i < array.length; i++) { this._ch.writeInt8(array[i]) } } else { - onError( - newError( - 'Byte arrays are not supported by the database this driver is connected to' - ) + throw newError( + 'Byte arrays are not supported by the database this driver is connected to' ) } } - packBytesHeader (size, onError) { + packBytesHeader (size) { if (size < 0x100) { this._ch.writeUInt8(BYTES_8) this._ch.writeUInt8(size) @@ -310,11 +301,11 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Byte arrays of size ' + size + ' are not supported')) + throw newError('Byte arrays of size ' + size + ' are not supported') } } - packMapHeader (size, onError) { + packMapHeader (size) { if (size < 0x10) { this._ch.writeUInt8(TINY_MAP | size) } else if (size < 0x100) { @@ -331,11 +322,11 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Maps of size ' + size + ' are not supported')) + throw newError('Maps of size ' + size + ' are not supported') } } - packStructHeader (size, signature, onError) { + packStructHeader (size, signature) { if (size < 0x10) { this._ch.writeUInt8(TINY_STRUCT | size) this._ch.writeUInt8(signature) @@ -348,7 +339,7 @@ class Packer { this._ch.writeUInt8((size / 256) >> 0) this._ch.writeUInt8(size % 256) } else { - onError(newError('Structures of size ' + size + ' are not supported')) + throw newError('Structures of size ' + size + ' are not supported') } } @@ -356,11 +347,10 @@ class Packer { this._byteArraysSupported = false } - _nonPackableValue (message, onError) { - if (onError) { - onError(newError(message, PROTOCOL_ERROR)) + _nonPackableValue (message) { + return () => { + throw newError(message, PROTOCOL_ERROR) } - return () => undefined } } diff --git a/src/internal/packstream-v2.js b/src/internal/packstream-v2.js index 641576183..d359d5018 100644 --- a/src/internal/packstream-v2.js +++ b/src/internal/packstream-v2.js @@ -72,23 +72,23 @@ export class Packer extends v1.Packer { throw new Error('Bolt V2 should always support byte arrays') } - packable (obj, onError) { + packable (obj) { if (isPoint(obj)) { - return () => packPoint(obj, this, onError) + return () => packPoint(obj, this) } else if (isDuration(obj)) { - return () => packDuration(obj, this, onError) + return () => packDuration(obj, this) } else if (isLocalTime(obj)) { - return () => packLocalTime(obj, this, onError) + return () => packLocalTime(obj, this) } else if (isTime(obj)) { - return () => packTime(obj, this, onError) + return () => packTime(obj, this) } else if (isDate(obj)) { - return () => packDate(obj, this, onError) + return () => packDate(obj, this) } else if (isLocalDateTime(obj)) { - return () => packLocalDateTime(obj, this, onError) + return () => packLocalDateTime(obj, this) } else if (isDateTime(obj)) { - return () => packDateTime(obj, this, onError) + return () => packDateTime(obj, this) } else { - return super.packable(obj, onError) + return super.packable(obj) } } } @@ -156,14 +156,13 @@ export class Unpacker extends v1.Unpacker { * Pack given 2D or 3D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint (point, packer, onError) { +function packPoint (point, packer) { const is2DPoint = point.z === null || point.z === undefined if (is2DPoint) { - packPoint2D(point, packer, onError) + packPoint2D(point, packer) } else { - packPoint3D(point, packer, onError) + packPoint3D(point, packer) } } @@ -171,31 +170,29 @@ function packPoint (point, packer, onError) { * Pack given 2D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint2D (point, packer, onError) { +function packPoint2D (point, packer) { const packableStructFields = [ - packer.packable(int(point.srid), onError), - packer.packable(point.x, onError), - packer.packable(point.y, onError) + packer.packable(int(point.srid)), + packer.packable(point.x), + packer.packable(point.y) ] - packer.packStruct(POINT_2D, packableStructFields, onError) + packer.packStruct(POINT_2D, packableStructFields) } /** * Pack given 3D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint3D (point, packer, onError) { +function packPoint3D (point, packer) { const packableStructFields = [ - packer.packable(int(point.srid), onError), - packer.packable(point.x, onError), - packer.packable(point.y, onError), - packer.packable(point.z, onError) + packer.packable(int(point.srid)), + packer.packable(point.x), + packer.packable(point.y), + packer.packable(point.z) ] - packer.packStruct(POINT_3D, packableStructFields, onError) + packer.packStruct(POINT_3D, packableStructFields) } /** @@ -238,21 +235,20 @@ function unpackPoint3D (unpacker, structSize, buffer) { * Pack given duration. * @param {Duration} value the duration value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDuration (value, packer, onError) { +function packDuration (value, packer) { const months = int(value.months) const days = int(value.days) const seconds = int(value.seconds) const nanoseconds = int(value.nanoseconds) const packableStructFields = [ - packer.packable(months, onError), - packer.packable(days, onError), - packer.packable(seconds, onError), - packer.packable(nanoseconds, onError) + packer.packable(months), + packer.packable(days), + packer.packable(seconds), + packer.packable(nanoseconds) ] - packer.packStruct(DURATION, packableStructFields, onError) + packer.packStruct(DURATION, packableStructFields) } /** @@ -277,9 +273,8 @@ function unpackDuration (unpacker, structSize, buffer) { * Pack given local time. * @param {LocalTime} value the local time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packLocalTime (value, packer, onError) { +function packLocalTime (value, packer) { const nanoOfDay = localTimeToNanoOfDay( value.hour, value.minute, @@ -287,8 +282,8 @@ function packLocalTime (value, packer, onError) { value.nanosecond ) - const packableStructFields = [packer.packable(nanoOfDay, onError)] - packer.packStruct(LOCAL_TIME, packableStructFields, onError) + const packableStructFields = [packer.packable(nanoOfDay)] + packer.packStruct(LOCAL_TIME, packableStructFields) } /** @@ -316,9 +311,8 @@ function unpackLocalTime ( * Pack given time. * @param {Time} value the time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packTime (value, packer, onError) { +function packTime (value, packer) { const nanoOfDay = localTimeToNanoOfDay( value.hour, value.minute, @@ -328,10 +322,10 @@ function packTime (value, packer, onError) { const offsetSeconds = int(value.timeZoneOffsetSeconds) const packableStructFields = [ - packer.packable(nanoOfDay, onError), - packer.packable(offsetSeconds, onError) + packer.packable(nanoOfDay), + packer.packable(offsetSeconds) ] - packer.packStruct(TIME, packableStructFields, onError) + packer.packStruct(TIME, packableStructFields) } /** @@ -363,13 +357,12 @@ function unpackTime (unpacker, structSize, buffer, disableLosslessIntegers) { * Pack given neo4j date. * @param {Date} value the date value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDate (value, packer, onError) { +function packDate (value, packer) { const epochDay = dateToEpochDay(value.year, value.month, value.day) - const packableStructFields = [packer.packable(epochDay, onError)] - packer.packStruct(DATE, packableStructFields, onError) + const packableStructFields = [packer.packable(epochDay)] + packer.packStruct(DATE, packableStructFields) } /** @@ -392,9 +385,8 @@ function unpackDate (unpacker, structSize, buffer, disableLosslessIntegers) { * Pack given local date time. * @param {LocalDateTime} value the local date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packLocalDateTime (value, packer, onError) { +function packLocalDateTime (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -407,10 +399,10 @@ function packLocalDateTime (value, packer, onError) { const nano = int(value.nanosecond) const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError) + packer.packable(epochSecond), + packer.packable(nano) ] - packer.packStruct(LOCAL_DATE_TIME, packableStructFields, onError) + packer.packStruct(LOCAL_DATE_TIME, packableStructFields) } /** @@ -443,13 +435,12 @@ function unpackLocalDateTime ( * Pack given date time. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTime (value, packer, onError) { +function packDateTime (value, packer) { if (value.timeZoneId) { - packDateTimeWithZoneId(value, packer, onError) + packDateTimeWithZoneId(value, packer) } else { - packDateTimeWithZoneOffset(value, packer, onError) + packDateTimeWithZoneOffset(value, packer) } } @@ -457,9 +448,8 @@ function packDateTime (value, packer, onError) { * Pack given date time with zone offset. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTimeWithZoneOffset (value, packer, onError) { +function packDateTimeWithZoneOffset (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -473,11 +463,11 @@ function packDateTimeWithZoneOffset (value, packer, onError) { const timeZoneOffsetSeconds = int(value.timeZoneOffsetSeconds) const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError), - packer.packable(timeZoneOffsetSeconds, onError) + packer.packable(epochSecond), + packer.packable(nano), + packer.packable(timeZoneOffsetSeconds) ] - packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields, onError) + packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields) } /** @@ -523,9 +513,8 @@ function unpackDateTimeWithZoneOffset ( * Pack given date time with zone id. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTimeWithZoneId (value, packer, onError) { +function packDateTimeWithZoneId (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -539,11 +528,11 @@ function packDateTimeWithZoneId (value, packer, onError) { const timeZoneId = value.timeZoneId const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError), - packer.packable(timeZoneId, onError) + packer.packable(epochSecond), + packer.packable(nano), + packer.packable(timeZoneId) ] - packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields, onError) + packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields) } /** diff --git a/src/internal/pool.js b/src/internal/pool.js index 4a862f0ec..73effd895 100644 --- a/src/internal/pool.js +++ b/src/internal/pool.js @@ -62,7 +62,7 @@ class Pool { /** * Acquire and idle resource fom the pool or create a new one. * @param {ServerAddress} address the address for which we're acquiring. - * @return {object} resource that is ready to use. + * @return {Object} resource that is ready to use. */ acquire (address) { return this._acquire(address).then(resource => { diff --git a/src/internal/request-message.js b/src/internal/request-message.js index 4faa9783f..e310e156c 100644 --- a/src/internal/request-message.js +++ b/src/internal/request-message.js @@ -55,7 +55,7 @@ export default class RequestMessage { /** * Create a new INIT message. * @param {string} clientName the client name. - * @param {object} authToken the authentication token. + * @param {Object} authToken the authentication token. * @return {RequestMessage} new INIT message. */ static init (clientName, authToken) { @@ -69,7 +69,7 @@ export default class RequestMessage { /** * Create a new RUN message. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. + * @param {Object} parameters the statement parameters. * @return {RequestMessage} new RUN message. */ static run (statement, parameters) { @@ -99,7 +99,7 @@ export default class RequestMessage { /** * Create a new HELLO message. * @param {string} userAgent the user agent. - * @param {object} authToken the authentication token. + * @param {Object} authToken the authentication token. * @return {RequestMessage} new HELLO message. */ static hello (userAgent, authToken) { @@ -147,7 +147,7 @@ export default class RequestMessage { /** * Create a new RUN message with additional metadata. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. + * @param {Object} parameters the statement parameters. * @param {Bookmark} bookmark the bookmark. * @param {TxConfig} txConfig the configuration. * @param {string} database the database name. @@ -185,7 +185,7 @@ export default class RequestMessage { * @return {RequestMessage} the PULL message. */ static pull ({ stmtId = NO_STATEMENT_ID, n = ALL } = {}) { - const metadata = buildStreamMetadata(stmtId, n) + const metadata = buildStreamMetadata(stmtId || NO_STATEMENT_ID, n || ALL) return new RequestMessage( PULL, [metadata], @@ -200,7 +200,7 @@ export default class RequestMessage { * @return {RequestMessage} the PULL message. */ static discard ({ stmtId = NO_STATEMENT_ID, n = ALL } = {}) { - const metadata = buildStreamMetadata(stmtId, n) + const metadata = buildStreamMetadata(stmtId || NO_STATEMENT_ID, n || ALL) return new RequestMessage( DISCARD, [metadata], @@ -215,7 +215,7 @@ export default class RequestMessage { * @param {TxConfig} txConfig the configuration. * @param {string} database the database name. * @param {string} mode the access mode. - * @return {object} a metadata object. + * @return {Object} a metadata object. */ function buildTxMetadata (bookmark, txConfig, database, mode) { const metadata = {} @@ -241,12 +241,12 @@ function buildTxMetadata (bookmark, txConfig, database, mode) { * Create an object that represents streaming metadata. * @param {Integer|number} stmtId The statement id to stream its results. * @param {Integer|number} n The number of records to stream. - * @returns {object} a metadata object. + * @returns {Object} a metadata object. */ function buildStreamMetadata (stmtId, n) { const metadata = { n: int(n) } if (stmtId !== NO_STATEMENT_ID) { - metadata['stmt_id'] = int(stmtId) + metadata['qid'] = int(stmtId) } return metadata } diff --git a/src/internal/retry-logic-rx.js b/src/internal/retry-logic-rx.js new file mode 100644 index 000000000..46aa98e29 --- /dev/null +++ b/src/internal/retry-logic-rx.js @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../error' +import { Observable, throwError, of } from 'rxjs' +import { retryWhen, flatMap, delay } from 'rxjs/operators' +import Logger from './logger' + +const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds +const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds +const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0 +const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2 + +export default class RxRetryLogic { + /** + * + * @param {Object} args + * @param {Logger} args.logger + */ + constructor ({ + maxRetryTimeout = DEFAULT_MAX_RETRY_TIME_MS, + initialDelay = DEFAULT_INITIAL_RETRY_DELAY_MS, + delayMultiplier = DEFAULT_RETRY_DELAY_MULTIPLIER, + delayJitter = DEFAULT_RETRY_DELAY_JITTER_FACTOR, + logger = null + } = {}) { + this._maxRetryTimeout = valueOrDefault( + maxRetryTimeout, + DEFAULT_MAX_RETRY_TIME_MS + ) + this._initialDelay = valueOrDefault( + initialDelay, + DEFAULT_INITIAL_RETRY_DELAY_MS + ) + this._delayMultiplier = valueOrDefault( + delayMultiplier, + DEFAULT_RETRY_DELAY_MULTIPLIER + ) + this._delayJitter = valueOrDefault( + delayJitter, + DEFAULT_RETRY_DELAY_JITTER_FACTOR + ) + this._logger = logger + } + + /** + * + * @param {Observable} work + */ + retry (work) { + return work.pipe( + retryWhen(failedWork => { + const handledExceptions = [] + const startTime = Date.now() + let retryCount = 1 + let delayDuration = this._initialDelay + + return failedWork.pipe( + flatMap(err => { + if (!RxRetryLogic._canRetryOn(err)) { + return throwError(err) + } + + handledExceptions.push(err) + + if ( + retryCount >= 2 && + Date.now() - startTime >= this._maxRetryTimeout + ) { + const error = newError( + `Failed after retried for ${retryCount} times in ${ + this._maxRetryTimeout + } ms. Make sure that your database is online and retry again.`, + SERVICE_UNAVAILABLE + ) + + error.seenErrors = handledExceptions + + return throwError(error) + } + + const nextDelayDuration = this._computeNextDelay(delayDuration) + delayDuration = delayDuration * this._delayMultiplier + retryCount++ + if (this._logger) { + this._logger.warn( + `Transaction failed and will be retried in ${nextDelayDuration}` + ) + } + return of(1).pipe(delay(nextDelayDuration)) + }) + ) + }) + ) + } + + _computeNextDelay (delay) { + const jitter = delay * this._delayJitter + return delay - jitter + 2 * jitter * Math.random() + } + + static _canRetryOn (error) { + return ( + error && + error.code && + (error.code === SERVICE_UNAVAILABLE || + error.code === SESSION_EXPIRED || + this._isTransientError(error)) + ) + } + + static _isTransientError (error) { + // Retries should not happen when transaction was explicitly terminated by the user. + // Termination of transaction might result in two different error codes depending on where it was + // terminated. These are really client errors but classification on the server is not entirely correct and + // they are classified as transient. + + const code = error.code + if (code.indexOf('TransientError') >= 0) { + if ( + code === 'Neo.TransientError.Transaction.Terminated' || + code === 'Neo.TransientError.Transaction.LockClientStopped' + ) { + return false + } + return true + } + return false + } +} + +function valueOrDefault (value, defaultValue) { + if (value || value === 0) { + return value + } + return defaultValue +} diff --git a/src/internal/routing-table.js b/src/internal/routing-table.js index 9516b8703..ce12ec6f5 100644 --- a/src/internal/routing-table.js +++ b/src/internal/routing-table.js @@ -81,7 +81,7 @@ export default class RoutingTable { /** * Remove all occurrences of the element in the array. * @param {Array} array the array to filter. - * @param {object} element the element to remove. + * @param {Object} element the element to remove. * @return {Array} new filtered array. */ function removeFromArray (array, element) { diff --git a/src/internal/routing-util.js b/src/internal/routing-util.js index 8ab0ec835..de09f8826 100644 --- a/src/internal/routing-util.js +++ b/src/internal/routing-util.js @@ -132,7 +132,7 @@ export default class RoutingUtil { } _callAvailableRoutingProcedure (session, database) { - return session._run(null, null, (connection, streamObserver) => { + return session._run(null, null, connection => { let query let params @@ -148,11 +148,12 @@ export default class RoutingUtil { params = { context: this._routingContext } } - connection.protocol().run(query, params, streamObserver, { + return connection.protocol().run(query, params, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), mode: session._mode, - database: session._database + database: session._database, + afterComplete: session._onComplete }) }) } diff --git a/src/internal/stream-observer.js b/src/internal/stream-observer.js deleted file mode 100644 index 0bcc98df1..000000000 --- a/src/internal/stream-observer.js +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (c) 2002-2019 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * 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 Record from '../record' - -/** - * Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses - * in a way that a user-provided observer can see these as a clean Stream - * of records. - * This class will queue up incoming messages until a user-provided observer - * for the incoming stream is registered. Thus, we keep fields around - * for tracking head/records/tail. These are only used if there is no - * observer registered. - * @access private - */ -class StreamObserver { - constructor () { - this._fieldKeys = null - this._fieldLookup = null - this._queuedRecords = [] - this._tail = null - this._error = null - this._hasFailed = false - this._observer = null - this._conn = null - this._meta = {} - } - - /** - * Will be called on every record that comes in and transform a raw record - * to a Object. If user-provided observer is present, pass transformed record - * to it's onNext method, otherwise, push to record que. - * @param {Array} rawRecord - An array with the raw record - */ - onNext (rawRecord) { - let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) - if (this._observer) { - this._observer.onNext(record) - } else { - this._queuedRecords.push(record) - } - } - - onCompleted (meta) { - if (this._fieldKeys === null) { - // Stream header, build a name->index field lookup table - // to be used by records. This is an optimization to make it - // faster to look up fields in a record by name, rather than by index. - // Since the records we get back via Bolt are just arrays of values. - this._fieldKeys = [] - this._fieldLookup = {} - if (meta.fields && meta.fields.length > 0) { - this._fieldKeys = meta.fields - for (let i = 0; i < meta.fields.length; i++) { - this._fieldLookup[meta.fields[i]] = i - } - } - } else { - // End of stream - if (this._observer) { - this._observer.onCompleted(meta) - } else { - this._tail = meta - } - } - this._copyMetadataOnCompletion(meta) - } - - _copyMetadataOnCompletion (meta) { - for (var key in meta) { - if (meta.hasOwnProperty(key)) { - this._meta[key] = meta[key] - } - } - } - - serverMetadata () { - const serverMeta = { server: this._conn.server } - return Object.assign({}, this._meta, serverMeta) - } - - resolveConnection (conn) { - this._conn = conn - } - - /** - * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. - * Response for RUN initializes statement keys. Response for PULL_ALL / DISCARD_ALL exposes the result stream. - * - * However, some operations can be represented as a single message which receives full metadata in a single response. - * For example, operations to begin, commit and rollback an explicit transaction use two messages in Bolt V1 but a single message in Bolt V3. - * Messages are `RUN "BEGIN" {}` + `PULL_ALL` in Bolt V1 and `BEGIN` in Bolt V3. - * - * This function prepares the observer to only handle a single response message. - */ - prepareToHandleSingleResponse () { - this._fieldKeys = [] - } - - /** - * Mark this observer as if it has completed with no metadata. - */ - markCompleted () { - this._fieldKeys = [] - this._tail = {} - } - - /** - * Will be called on errors. - * If user-provided observer is present, pass the error - * to it's onError method, otherwise set instance variable _error. - * @param {Object} error - An error object - */ - onError (error) { - if (this._hasFailed) { - return - } - this._hasFailed = true - - if (this._observer) { - if (this._observer.onError) { - this._observer.onError(error) - } else { - console.log(error) - } - } else { - this._error = error - } - } - - /** - * Subscribe to events with provided observer. - * @param {Object} observer - Observer object - * @param {function(record: Object)} observer.onNext - Handle records, one by one. - * @param {function(metadata: Object)} observer.onComplete - Handle stream tail, the metadata. - * @param {function(error: Object)} observer.onError - Handle errors. - */ - subscribe (observer) { - if (this._error) { - observer.onError(this._error) - return - } - if (this._queuedRecords.length > 0) { - for (let i = 0; i < this._queuedRecords.length; i++) { - observer.onNext(this._queuedRecords[i]) - } - } - if (this._tail) { - observer.onCompleted(this._tail) - } - this._observer = observer - } - - hasFailed () { - return this._hasFailed - } -} - -export default StreamObserver diff --git a/src/internal/stream-observers.js b/src/internal/stream-observers.js new file mode 100644 index 000000000..b472585ed --- /dev/null +++ b/src/internal/stream-observers.js @@ -0,0 +1,509 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 Record from '../record' +import Connection from './connection' +import { newError, PROTOCOL_ERROR } from '../error' +import { isString } from './util' +import Integer from '../integer' + +const DefaultBatchSize = 50 + +class StreamObserver { + onNext (rawRecord) {} + + onError (error) {} + + onCompleted (meta) {} +} + +/** + * Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses + * in a way that a user-provided observer can see these as a clean Stream + * of records. + * This class will queue up incoming messages until a user-provided observer + * for the incoming stream is registered. Thus, we keep fields around + * for tracking head/records/tail. These are only used if there is no + * observer registered. + * @access private + */ +class ResultStreamObserver extends StreamObserver { + /** + * + * @param {Object} param + * @param {Connection} param.connection + * @param {boolean} param.reactive + * @param {function(connection: Connection, stmtId: number|Integer, n: number|Integer, observer: StreamObserver)} param.moreFunction - + * @param {function(connection: Connection, stmtId: number|Integer, observer: StreamObserver)} param.discardFunction - + * @param {number|Integer} param.batchSize - + * @param {function(err: Error): Promise|void} param.beforeError - + * @param {function(err: Error): Promise|void} param.afterError - + * @param {function(keys: string[]): Promise|void} param.beforeKeys - + * @param {function(keys: string[]): Promise|void} param.afterKeys - + * @param {function(metadata: Object): Promise|void} param.beforeComplete - + * @param {function(metadata: Object): Promise|void} param.afterComplete - + */ + constructor ({ + connection, + reactive = false, + moreFunction, + discardFunction, + batchSize = DefaultBatchSize, + beforeError, + afterError, + beforeKeys, + afterKeys, + beforeComplete, + afterComplete + } = {}) { + super() + + this._connection = connection + this._reactive = reactive + this._streaming = false + + this._fieldKeys = null + this._fieldLookup = null + this._head = null + this._queuedRecords = [] + this._tail = null + this._error = null + this._hasFailed = false + this._observers = [] + this._meta = {} + + this._beforeError = beforeError + this._afterError = afterError + this._beforeKeys = beforeKeys + this._afterKeys = afterKeys + this._beforeComplete = beforeComplete + this._afterComplete = afterComplete + + this._statementId = null + this._moreFunction = moreFunction + this._discardFunction = discardFunction + this._discard = false + this._batchSize = batchSize + } + + /** + * Will be called on every record that comes in and transform a raw record + * to a Object. If user-provided observer is present, pass transformed record + * to it's onNext method, otherwise, push to record que. + * @param {Array} rawRecord - An array with the raw record + */ + onNext (rawRecord) { + let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) + if (this._observers.some(o => o.onNext)) { + this._observers.forEach(o => { + if (o.onNext) { + o.onNext(record) + } + }) + } else { + this._queuedRecords.push(record) + } + } + + onCompleted (meta) { + if (this._fieldKeys === null) { + // Stream header, build a name->index field lookup table + // to be used by records. This is an optimization to make it + // faster to look up fields in a record by name, rather than by index. + // Since the records we get back via Bolt are just arrays of values. + this._fieldKeys = [] + this._fieldLookup = {} + if (meta.fields && meta.fields.length > 0) { + this._fieldKeys = meta.fields + for (let i = 0; i < meta.fields.length; i++) { + this._fieldLookup[meta.fields[i]] = i + } + + // remove fields key from metadata object + delete meta.fields + } + + // Extract server generated query id for use in requestMore and discard + // functions + if (meta.qid) { + this._statementId = meta.qid + + // remove qid from metadata object + delete meta.qid + } + + this._storeMetadataForCompletion(meta) + + let beforeHandlerResult = null + if (this._beforeKeys) { + beforeHandlerResult = this._beforeKeys(this._fieldKeys) + } + + const continuation = () => { + this._head = this._fieldKeys + + if (this._observers.some(o => o.onKeys)) { + this._observers.forEach(o => { + if (o.onKeys) { + o.onKeys(this._fieldKeys) + } + }) + } + + if (this._afterKeys) { + this._afterKeys(this._fieldKeys) + } + + if (this._reactive) { + this._handleStreaming() + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } else { + this._streaming = false + + if (meta.has_more) { + // We've consumed current batch and server notified us that there're more + // records to stream. Let's invoke more or discard function based on whether + // the user wants to discard streaming or not + this._handleStreaming() + + delete meta.has_more + } else { + const completionMetadata = Object.assign( + this._connection ? { server: this._connection.server } : {}, + this._meta, + meta + ) + + let beforeHandlerResult = null + if (this._beforeComplete) { + beforeHandlerResult = this._beforeComplete(completionMetadata) + } + + const continuation = () => { + // End of stream + this._tail = completionMetadata + + if (this._observers.some(o => o.onCompleted)) { + this._observers.forEach(o => { + if (o.onCompleted) { + o.onCompleted(completionMetadata) + } + }) + } + + if (this._afterComplete) { + this._afterComplete(completionMetadata) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + } + } + + _handleStreaming () { + if ( + this._reactive && + this._head && + this._observers.some(o => o.onNext || o.onCompleted) && + !this._streaming + ) { + this._streaming = true + + if (this._discard) { + this._discardFunction(this._connection, this._statementId, this) + } else { + this._moreFunction( + this._connection, + this._statementId, + this._batchSize, + this + ) + } + } + } + + _storeMetadataForCompletion (meta) { + const keys = Object.keys(meta) + let index = keys.length + let key = '' + + while (index--) { + key = keys[index] + this._meta[key] = meta[key] + } + } + + /** + * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. + * Response for RUN initializes statement keys. Response for PULL_ALL / DISCARD_ALL exposes the result stream. + * + * However, some operations can be represented as a single message which receives full metadata in a single response. + * For example, operations to begin, commit and rollback an explicit transaction use two messages in Bolt V1 but a single message in Bolt V3. + * Messages are `RUN "BEGIN" {}` + `PULL_ALL` in Bolt V1 and `BEGIN` in Bolt V3. + * + * This function prepares the observer to only handle a single response message. + */ + prepareToHandleSingleResponse () { + this._head = [] + this._fieldKeys = [] + } + + /** + * Mark this observer as if it has completed with no metadata. + */ + markCompleted () { + this._head = [] + this._fieldKeys = [] + this._tail = {} + } + + /** + * Discard pending record stream + */ + discard () { + this._discard = true + } + + /** + * Will be called on errors. + * If user-provided observer is present, pass the error + * to it's onError method, otherwise set instance variable _error. + * @param {Object} error - An error object + */ + onError (error) { + if (this._hasFailed) { + return + } + + this._hasFailed = true + this._error = error + + let beforeHandlerResult = null + if (this._beforeError) { + beforeHandlerResult = this._beforeError(error) + } + + const continuation = () => { + if (this._observers.some(o => o.onError)) { + this._observers.forEach(o => { + if (o.onError) { + o.onError(error) + } + }) + } + + if (this._afterError) { + this._afterError(error) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + + /** + * Subscribe to events with provided observer. + * @param {Object} observer - Observer object + * @param {function(keys: String[])} observer.onKeys - Handle stream header, field keys. + * @param {function(record: Object)} observer.onNext - Handle records, one by one. + * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the metadata. + * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. + */ + subscribe (observer) { + if (this._error) { + observer.onError(this._error) + return + } + if (this._head && observer.onKeys) { + observer.onKeys(this._head) + } + if (this._queuedRecords.length > 0 && observer.onNext) { + for (let i = 0; i < this._queuedRecords.length; i++) { + observer.onNext(this._queuedRecords[i]) + } + } + if (this._tail && observer.onCompleted) { + observer.onCompleted(this._tail) + } + this._observers.push(observer) + + if (this._reactive) { + this._handleStreaming() + } + } + + hasFailed () { + return this._hasFailed + } +} + +class LoginObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {Connection} param.connection + * @param {function(err: Error)} param.beforeError + * @param {function(err: Error)} param.afterError + * @param {function(metadata)} param.beforeComplete + * @param {function(metadata)} param.afterComplete + */ + constructor ({ + connection, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + super() + + this._connection = connection + this._beforeError = beforeError + this._afterError = afterError + this._beforeComplete = beforeComplete + this._afterComplete = afterComplete + } + + onNext (record) { + this.onError( + newError('Received RECORD when initializing ' + JSON.stringify(record)) + ) + } + + onError (error) { + if (this._beforeError) { + this._beforeError(error) + } + + this._connection._updateCurrentObserver() // make sure this exact observer will not be called again + this._connection._handleFatalError(error) // initialization errors are fatal + + if (this._afterError) { + this._afterError(error) + } + } + + onCompleted (metadata) { + if (this._beforeComplete) { + this._beforeComplete(metadata) + } + + if (metadata) { + // read server version from the response metadata, if it is available + const serverVersion = metadata.server + if (!this._connection.version) { + this._connection.version = serverVersion + } + + // read database connection id from the response metadata, if it is available + const dbConnectionId = metadata.connection_id + if (!this._connection.databaseId) { + this._connection.databaseId = dbConnectionId + } + } + + if (this._afterComplete) { + this._afterComplete(metadata) + } + } +} + +class ResetObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {Connection} param.connection + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onComplete + */ + constructor ({ connection, onError, onComplete } = {}) { + super() + + this._connection = connection + this._onError = onError + this._onComplete = onComplete + } + + onNext (record) { + this.onError( + newError( + 'Received RECORD when resetting: received record is: ' + + JSON.stringify(record), + PROTOCOL_ERROR + ) + ) + } + + onError (error) { + if (error.code === PROTOCOL_ERROR) { + this._connection._handleProtocolError(error.message) + } + + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onComplete) { + this._onComplete(metadata) + } + } +} + +class FailedObserver extends ResultStreamObserver { + constructor ({ error, onError }) { + super({ beforeError: onError }) + + if (error instanceof Error) { + this.onError(error) + } else if (isString(error)) { + this.onError({ error: error }) + } + } +} + +class CompletedObserver extends ResultStreamObserver { + constructor () { + super() + super.markCompleted() + } +} + +export { + StreamObserver, + ResultStreamObserver, + LoginObserver, + ResetObserver, + FailedObserver, + CompletedObserver +} diff --git a/src/internal/tx-config.js b/src/internal/tx-config.js index c505e7b30..0f5b5ea90 100644 --- a/src/internal/tx-config.js +++ b/src/internal/tx-config.js @@ -30,7 +30,7 @@ import { newError } from '../error' export default class TxConfig { /** * @constructor - * @param {object} config the raw configuration object. + * @param {Object} config the raw configuration object. */ constructor (config) { assertValidConfig(config) diff --git a/src/internal/url-util.js b/src/internal/url-util.js index f2d89d933..a9bbff8a3 100644 --- a/src/internal/url-util.js +++ b/src/internal/url-util.js @@ -58,7 +58,7 @@ class Url { /** * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. * Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc. - * @type {object} + * @type {Object} */ this.query = query } diff --git a/src/internal/util.js b/src/internal/util.js index f08ce7a97..a55cb7d2a 100644 --- a/src/internal/util.js +++ b/src/internal/util.js @@ -47,7 +47,7 @@ function isObject (obj) { /** * Check and normalize given statement and parameters. * @param {string|{text: string, parameters: object}} statement the statement to check. - * @param {object} parameters + * @param {Object} parameters * @return {{query: string, params: object}} the normalized query with parameters. * @throws TypeError when either given query or parameters are invalid. */ diff --git a/src/record.js b/src/record.js index 29bd7f9de..0fc9fffba 100644 --- a/src/record.js +++ b/src/record.js @@ -50,7 +50,7 @@ class Record { /** * Create a new record object. * @constructor - * @access private + * @protected * @param {string[]} keys An array of field keys, in the order the fields appear in the record * @param {Array} fields An array of field values * @param {Object} fieldLookup An object of fieldName -> value index, used to map @@ -58,7 +58,15 @@ class Record { * generated. */ constructor (keys, fields, fieldLookup = null) { + /** + * Field keys, in the order the fields appear in the record. + * @type {string[]} + */ this.keys = keys + /** + * Number of fields + * @type {Number} + */ this.length = keys.length this._fields = fields this._fieldLookup = fieldLookup || generateFieldLookup(keys) diff --git a/src/result-rx.js b/src/result-rx.js new file mode 100644 index 000000000..bb5f3b87c --- /dev/null +++ b/src/result-rx.js @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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' +import ResultSummary from './result-summary' +import { Observable, Subject, ReplaySubject, from } from 'rxjs' +import { flatMap, publishReplay, refCount, shareReplay } from 'rxjs/operators' +import Record from './record' + +const States = { + READY: 0, + STREAMING: 1, + COMPLETED: 2 +} + +/** + * The reactive result interface. + */ +export default class RxResult { + /** + * @constructor + * @protected + * @param {Observable} result - An observable of single Result instance to relay requests. + */ + constructor (result) { + const replayedResult = result.pipe( + publishReplay(1), + refCount() + ) + + this._result = replayedResult + this._keys = replayedResult.pipe( + flatMap(r => from(r.keys())), + publishReplay(1), + refCount() + ) + this._records = new Subject() + this._summary = new ReplaySubject() + this._state = States.READY + } + + /** + * Returns an observable that exposes a single item containing field names + * returned by the executing statement. + * + * Errors raised by actual statement execution can surface on the returned + * observable stream. + * + * @public + * @returns {Observable} - An observable stream (with exactly one element) of field names. + */ + keys () { + return this._keys + } + + /** + * Returns an observable that exposes each record returned by the executing statement. + * + * Errors raised during the streaming phase can surface on the returned observable stream. + * + * @public + * @returns {Observable} - An observable stream of records. + */ + records () { + return this._result.pipe( + flatMap( + result => + new Observable(recordsObserver => + this._startStreaming({ result, recordsObserver }) + ) + ) + ) + } + + /** + * Returns an observable that exposes a single item of {@link ResultSummary} that is generated by + * the server after the streaming of the executing statement is completed. + * + * *Subscribing to this stream before subscribing to records() stream causes the results to be discarded on the server.* + * + * @public + * @returns {Observable} - An observable stream (with exactly one element) of result summary. + */ + summary () { + return this._result.pipe( + flatMap( + result => + new Observable(summaryObserver => + this._startStreaming({ result, summaryObserver }) + ) + ) + ) + } + + _startStreaming ({ + result, + recordsObserver = null, + summaryObserver = null + } = {}) { + const subscriptions = [] + + if (recordsObserver) { + subscriptions.push(this._records.subscribe(recordsObserver)) + } + + if (summaryObserver) { + subscriptions.push(this._summary.subscribe(summaryObserver)) + } + + if (this._state < States.STREAMING) { + this._state = States.STREAMING + + subscriptions.push({ + unsubscribe: () => { + if (result.discard) { + result.discard() + } + } + }) + + if (this._records.observers.length === 0) { + result._discard() + } + + result.subscribe({ + onNext: record => { + this._records.next(record) + }, + onCompleted: summary => { + this._records.complete() + + this._summary.next(summary) + this._summary.complete() + + this._state = States.COMPLETED + }, + onError: err => { + this._records.error(err) + this._summary.error(err) + + this._state = States.COMPLETED + } + }) + } else if (this._state === States.STREAMING && recordsObserver) { + recordsObserver.error( + newError( + 'Streaming has already started with a previous records or summary subscription.' + ) + ) + } + + return () => { + subscriptions.forEach(s => s.unsubscribe()) + } + } +} diff --git a/src/result.js b/src/result.js index a09f55b0c..47881796f 100644 --- a/src/result.js +++ b/src/result.js @@ -19,11 +19,13 @@ import ResultSummary from './result-summary' import { EMPTY_CONNECTION_HOLDER } from './internal/connection-holder' +import { ResultStreamObserver } from './internal/stream-observers' const DEFAULT_ON_ERROR = error => { console.log('Uncaught error when processing result: ' + error) } const DEFAULT_ON_COMPLETED = summary => {} +const DEFAULT_METADATA_SUPPLIER = metadata => {} /** * A stream of {@link Record} representing the result of a statement. @@ -37,62 +39,92 @@ class Result { * Inject the observer to be used. * @constructor * @access private - * @param {StreamObserver} streamObserver + * @param {Promise} streamObserverPromise * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement - * @param metaSupplier function, when called provides metadata * @param {ConnectionHolder} connectionHolder - to be notified when result is either fully consumed or error happened. */ - constructor ( - streamObserver, - statement, - parameters, - metaSupplier, - connectionHolder - ) { + constructor (streamObserverPromise, statement, parameters, connectionHolder) { this._stack = captureStacktrace() - this._streamObserver = streamObserver + this._streamObserverPromise = streamObserverPromise this._p = null this._statement = statement this._parameters = parameters || {} - this._metaSupplier = - metaSupplier || - function () { - return {} - } this._connectionHolder = connectionHolder || EMPTY_CONNECTION_HOLDER } + /** + * Returns a promise for the field keys. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @public + * @returns {Promise} - Field keys, in the order they will appear in records. + } + */ + keys () { + return new Promise((resolve, reject) => { + this._streamObserverPromise.then(observer => + observer.subscribe({ + onKeys: keys => resolve(keys), + onError: err => reject(err) + }) + ) + }) + } + + /** + * Returns a promise for the result summary. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @public + * @returns {Promise} - Result summary. + * + */ + summary () { + return new Promise((resolve, reject) => { + this._streamObserverPromise.then(o => + o.subscribe({ + onCompleted: metadata => resolve(metadata), + onError: err => reject(err) + }) + ) + }) + } + /** * Create and return new Promise + * + * @private * @return {Promise} new Promise. - * @access private */ - _createPromise () { - if (this._p) { - return - } - let self = this - this._p = new Promise((resolve, reject) => { - let records = [] - let observer = { - onNext: record => { - records.push(record) - }, - onCompleted: summary => { - resolve({ records: records, summary: summary }) - }, - onError: error => { - reject(error) + _getOrCreatePromise () { + if (!this._p) { + this._p = new Promise((resolve, reject) => { + let records = [] + let observer = { + onNext: record => { + records.push(record) + }, + onCompleted: summary => { + resolve({ records: records, summary: summary }) + }, + onError: error => { + reject(error) + } } - } - self.subscribe(observer) - }) + this.subscribe(observer) + }) + } + + return this._p } /** * Waits for all results and calls the passed in function with the results. - * Cannot be combined with the {@link Result#subscribe} function. + * + * *Should not be combined with {@link Result#subscribe} function.* * * @param {function(result: {records:Array, summary: ResultSummary})} onFulfilled - function to be called * when finished. @@ -100,19 +132,19 @@ class Result { * @return {Promise} promise. */ then (onFulfilled, onRejected) { - this._createPromise() - return this._p.then(onFulfilled, onRejected) + return this._getOrCreatePromise().then(onFulfilled, onRejected) } /** * Catch errors when using promises. - * Cannot be used with the subscribe function. + * + * *Should not be combined with {@link Result#subscribe} function.* + * * @param {function(error: Neo4jError)} onRejected - Function to be called upon errors. * @return {Promise} promise. */ catch (onRejected) { - this._createPromise() - return this._p.catch(onRejected) + return this._getOrCreatePromise().catch(onRejected) } /** @@ -120,28 +152,22 @@ class Result { * of handling the results, and allows you to handle arbitrarily large results. * * @param {Object} observer - Observer object + * @param {function(keys: string[])} observer.onKeys - handle stream head, the field keys. * @param {function(record: Record)} observer.onNext - handle records, one by one. * @param {function(summary: ResultSummary)} observer.onCompleted - handle stream tail, the result summary. * @param {function(error: {message:string, code:string})} observer.onError - handle errors. * @return */ subscribe (observer) { - const self = this - const onCompletedOriginal = observer.onCompleted || DEFAULT_ON_COMPLETED const onCompletedWrapper = metadata => { - const additionalMeta = self._metaSupplier() - for (let key in additionalMeta) { - if (additionalMeta.hasOwnProperty(key)) { - metadata[key] = additionalMeta[key] - } - } - const sum = new ResultSummary(this._statement, this._parameters, metadata) - // notify connection holder that the used connection is not needed any more because result has // been fully consumed; call the original onCompleted callback after that - self._connectionHolder.releaseConnection().then(() => { - onCompletedOriginal.call(observer, sum) + this._connectionHolder.releaseConnection().then(() => { + onCompletedOriginal.call( + observer, + new ResultSummary(this._statement, this._parameters, metadata) + ) }) } observer.onCompleted = onCompletedWrapper @@ -150,14 +176,24 @@ class Result { const onErrorWrapper = error => { // notify connection holder that the used connection is not needed any more because error happened // and result can't bee consumed any further; call the original onError callback after that - self._connectionHolder.releaseConnection().then(() => { + this._connectionHolder.releaseConnection().then(() => { replaceStacktrace(error, this._stack) onErrorOriginal.call(observer, error) }) } observer.onError = onErrorWrapper - this._streamObserver.subscribe(observer) + this._streamObserverPromise.then(o => o.subscribe(observer)) + } + + /** + * Signals the stream observer that the future records should be discarded on the server. + * + * @protected + * @since 4.0.0 + */ + _discard () { + this._streamObserverPromise.then(o => o.discard()) } } diff --git a/src/routing-driver.js b/src/routing-driver.js index a822637e8..d5f68f4d7 100644 --- a/src/routing-driver.js +++ b/src/routing-driver.js @@ -65,7 +65,7 @@ function createHostNameResolver (config) { /** * @private - * @returns {object} the given config. + * @returns {Object} the given config. */ function validateConfig (config) { const resolver = config.resolver diff --git a/src/session-rx.js b/src/session-rx.js new file mode 100644 index 000000000..b1279ad19 --- /dev/null +++ b/src/session-rx.js @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { defer, Observable, throwError } from 'rxjs' +import { flatMap, catchError, concat } from 'rxjs/operators' +import RxResult from './result-rx' +import Session from './session' +import RxTransaction from './transaction-rx' +import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import TxConfig from './internal/tx-config' +import RxRetryLogic from './internal/retry-logic-rx' + +/** + * A Reactive session, which provides the same functionality as {@link Session} but through a Reactive API. + */ +export default class RxSession { + /** + * Constructs a reactive session with given default session instance and provided driver configuration. + * + * @protected + * @param {Object} param - Object parameter + * @param {Session} param.session - The underlying session instance to relay requests + */ + constructor ({ session, config } = {}) { + this._session = session + this._retryLogic = _createRetryLogic(config) + } + + /** + * Creates a reactive result that will execute the statement with the provided parameters and the provided + * transaction configuration that applies to the underlying auto-commit transaction. + * + * @public + * @param {string} statement - Statement to be executed. + * @param {Object} parameters - Parameter values to use in statement execution. + * @param {TransactionConfig} transactionConfig - Configuration for the new auto-commit transaction. + * @returns {RxResult} - A reactive result + */ + run (statement, parameters, transactionConfig) { + return new RxResult( + new Observable(observer => { + try { + observer.next( + this._session.run(statement, parameters, transactionConfig) + ) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + ) + } + + /** + * Starts a new explicit transaction with the provided transaction configuration. + * + * @public + * @param {TransactionConfig} transactionConfig - Configuration for the new transaction. + * @returns {Observable} - A reactive stream that will generate at most **one** RxTransaction instance. + */ + beginTransaction (transactionConfig) { + return this._beginTransaction(this._session._mode, transactionConfig) + } + + /** + * Executes the provided unit of work in a {@link READ} reactive transaction which is created with the provided + * transaction configuration. + * @public + * @param {function(txc: RxTransaction): Observable} work - A unit of work to be executed. + * @param {TransactionConfig} transactionConfig - Configuration for the enclosing transaction created by the driver. + * @returns {Observable} - A reactive stream returned by the unit of work. + */ + readTransaction (work, transactionConfig) { + return this._runTransaction(ACCESS_MODE_READ, work, transactionConfig) + } + + /** + * Executes the provided unit of work in a {@link WRITE} reactive transaction which is created with the provided + * transaction configuration. + * @public + * @param {function(txc: RxTransaction): Observable} work - A unit of work to be executed. + * @param {TransactionConfig} transactionConfig - Configuration for the enclosing transaction created by the driver. + * @returns {Observable} - A reactive stream returned by the unit of work. + */ + writeTransaction (work, transactionConfig) { + return this._runTransaction(ACCESS_MODE_WRITE, work, transactionConfig) + } + + /** + * Closes this reactive session. + * + * @public + * @returns {Observable} - An empty reactive stream + */ + close () { + return new Observable(observer => { + this._session + .close() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } + + /** + * Returns the bookmark received following the last successfully completed statement, which is executed + * either in an {@link RxTransaction} obtained from this session instance or directly through one of + * the {@link RxSession#run} method of this session instance. + * + * If no bookmark was received or if this transaction was rolled back, the bookmark value will not be + * changed. + * + * @public + * @returns {string} + */ + lastBookmark () { + return this._session.lastBookmark() + } + + /** + * @private + */ + _beginTransaction (accessMode, transactionConfig) { + let txConfig = TxConfig.empty() + if (transactionConfig) { + txConfig = new TxConfig(transactionConfig) + } + + return new Observable(observer => { + try { + observer.next( + new RxTransaction( + this._session._beginTransaction(accessMode, txConfig) + ) + ) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + } + + /** + * @private + */ + _runTransaction (accessMode, work, transactionConfig) { + let txConfig = TxConfig.empty() + if (transactionConfig) { + txConfig = new TxConfig(transactionConfig) + } + + return this._retryLogic.retry( + this._beginTransaction(accessMode, transactionConfig).pipe( + flatMap(txc => + defer(() => { + try { + return work(txc) + } catch (err) { + return throwError(err) + } + }).pipe( + catchError(err => txc.rollback().pipe(concat(throwError(err)))), + concat(txc.commit()) + ) + ) + ) + ) + } +} + +function _createRetryLogic (config) { + const maxRetryTimeout = + config && config.maxTransactionRetryTime + ? config.maxTransactionRetryTime + : null + return new RxRetryLogic({ maxRetryTimeout }) +} diff --git a/src/session.js b/src/session.js index 94ef91982..32bd94e52 100644 --- a/src/session.js +++ b/src/session.js @@ -16,7 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import StreamObserver from './internal/stream-observer' +import { + ResultStreamObserver, + FailedObserver +} from './internal/stream-observers' import Result from './result' import Transaction from './transaction' import { newError } from './error' @@ -28,25 +31,6 @@ import TransactionExecutor from './internal/transaction-executor' import Bookmark from './internal/bookmark' import TxConfig from './internal/tx-config' -// Typedef for JSDoc. Declares TransactionConfig type and makes it possible to use in in method-level docs. -/** - * Configuration object containing settings for explicit and auto-commit transactions. - *

- * Configuration is supported for: - *

    - *
  • queries executed in auto-commit transactions using {@link Session#run}
  • - *
  • transactions started by transaction functions using {@link Session#readTransaction} and {@link Session#writeTransaction}
  • - *
  • explicit transactions using {@link Session#beginTransaction}
  • - *
- * @typedef {object} TransactionConfig - * @property {number} timeout - the transaction timeout in **milliseconds**. Transactions that execute longer than the configured timeout will - * be terminated by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default timeout - * configured in the database using `dbms.transaction.timeout` setting. Value should not represent a duration of zero or negative duration. - * @property {object} metadata - the transaction metadata. Specified metadata will be attached to the executing transaction and visible in the output of - * `dbms.listQueries` and `dbms.listTransactions` procedures. It will also get logged to the `query.log`. This functionality makes it easier to tag - * transactions and is equivalent to `dbms.setTXMetaData` procedure. - */ - /** * A Session instance is used for handling the connection and * sending statements through the connection. @@ -57,15 +41,26 @@ import TxConfig from './internal/tx-config' class Session { /** * @constructor - * @param {string} mode the default access mode for this session. - * @param {ConnectionProvider} connectionProvider - the connection provider to acquire connections from. - * @param {Bookmark} bookmark - the initial bookmark for this session. - * @param {string} database the database name - * @param {Object} [config={}] - this driver configuration. + * @protected + * @param {Object} args + * @param {string} args.mode the default access mode for this session. + * @param {ConnectionProvider} args.connectionProvider - the connection provider to acquire connections from. + * @param {Bookmark} args.bookmark - the initial bookmark for this session. + * @param {string} args.database the database name + * @param {Object} args.config={} - this driver configuration. + * @param {boolean} args.reactive - whether this session should create reactive streams */ - constructor ({ mode, connectionProvider, bookmark, database, config }) { + constructor ({ + mode, + connectionProvider, + bookmark, + database, + config, + reactive + }) { this._mode = mode this._database = database + this._reactive = reactive this._readConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_READ, database, @@ -80,12 +75,15 @@ class Session { this._hasTx = false this._lastBookmark = bookmark this._transactionExecutor = _createTransactionExecutor(config) + this._onComplete = this._onCompleteCallback.bind(this) } /** * Run Cypher statement * Could be called with a statement object i.e.: `{text: "MATCH ...", prameters: {param: 1}}` * or with the statement and parameters as separate arguments. + * + * @public * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement * @param {TransactionConfig} [transactionConfig] - configuration for the new auto-commit transaction. @@ -100,41 +98,40 @@ class Session { ? new TxConfig(transactionConfig) : TxConfig.empty() - return this._run(query, params, (connection, streamObserver) => - connection.protocol().run(query, params, streamObserver, { + return this._run(query, params, connection => + connection.protocol().run(query, params, { bookmark: this._lastBookmark, txConfig: autoCommitTxConfig, mode: this._mode, - database: this._database + database: this._database, + afterComplete: this._onComplete, + reactive: this._reactive }) ) } - _run (statement, parameters, statementRunner) { - const streamObserver = new SessionStreamObserver(this) + _run (statement, parameters, customRunner) { const connectionHolder = this._connectionHolderWithMode(this._mode) + + let observerPromise if (!this._hasTx) { connectionHolder.initializeConnection() - connectionHolder - .getConnection(streamObserver) - .then(connection => statementRunner(connection, streamObserver)) - .catch(error => streamObserver.onError(error)) + observerPromise = connectionHolder + .getConnection() + .then(connection => customRunner(connection)) + .catch(error => Promise.resolve(new FailedObserver({ error }))) } else { - streamObserver.onError( - newError( - 'Statements cannot be run directly on a ' + - 'session with an open transaction; either run from within the ' + - 'transaction or use a different session.' - ) + observerPromise = Promise.resolve( + new FailedObserver({ + error: newError( + 'Statements cannot be run directly on a ' + + 'session with an open transaction; either run from within the ' + + 'transaction or use a different session.' + ) + }) ) } - return new Result( - streamObserver, - statement, - parameters, - () => streamObserver.serverMetadata(), - connectionHolder - ) + return new Result(observerPromise, statement, parameters, connectionHolder) } /** @@ -173,11 +170,12 @@ class Session { connectionHolder.initializeConnection() this._hasTx = true - const tx = new Transaction( + const tx = new Transaction({ connectionHolder, - this._transactionClosed.bind(this), - this._updateBookmark.bind(this) - ) + onClose: this._transactionClosed.bind(this), + onBookmark: this._updateBookmark.bind(this), + reactive: this._reactive + }) tx._begin(this._lastBookmark, txConfig) return tx } @@ -252,20 +250,15 @@ class Session { /** * Close this session. - * @param {function()} callback - Function to be called after the session has been closed - * @return + * @return {Promise} */ - close (callback = () => null) { + async close () { if (this._open) { this._open = false this._transactionExecutor.close() - this._readConnectionHolder.close().then(() => { - this._writeConnectionHolder.close().then(() => { - callback() - }) - }) - } else { - callback() + + await this._readConnectionHolder.close() + await this._writeConnectionHolder.close() } } @@ -278,21 +271,9 @@ class Session { throw newError('Unknown access mode: ' + mode) } } -} - -/** - * @private - */ -class SessionStreamObserver extends StreamObserver { - constructor (session) { - super() - this._session = session - } - onCompleted (meta) { - super.onCompleted(meta) - const bookmark = new Bookmark(meta.bookmark) - this._session._updateBookmark(bookmark) + _onCompleteCallback (meta) { + this._updateBookmark(new Bookmark(meta.bookmark)) } } diff --git a/src/spatial-types.js b/src/spatial-types.js index af0654fc6..60ce04fd4 100644 --- a/src/spatial-types.js +++ b/src/spatial-types.js @@ -30,16 +30,35 @@ export class Point { * @param {Integer|number} srid the coordinate reference system identifier. * @param {number} x the `x` coordinate of the point. * @param {number} y the `y` coordinate of the point. - * @param {number} [z=undefined] the `y` coordinate of the point or `undefined` if point has 2 dimensions. + * @param {number} [z=undefined] the `z` coordinate of the point or `undefined` if point has 2 dimensions. */ constructor (srid, x, y, z) { + /** + * The coordinate reference system identifier. + * @type {Integer|Number} + */ this.srid = assertNumberOrInteger(srid, 'SRID') + /** + * The `x` coordinate of the point. + * @type {number} + */ this.x = assertNumber(x, 'X coordinate') + /** + * The `y` coordinate of the point. + * @type {number} + */ this.y = assertNumber(y, 'Y coordinate') + /** + * The `z` coordinate of the point or `undefined` if point is 2-dimensional. + * @type {number} + */ this.z = z === null || z === undefined ? z : assertNumber(z, 'Z coordinate') Object.freeze(this) } + /** + * @ignore + */ toString () { return this.z || this.z === 0 ? `Point{srid=${formatAsFloat(this.srid)}, x=${formatAsFloat( @@ -63,7 +82,7 @@ Object.defineProperty(Point.prototype, POINT_IDENTIFIER_PROPERTY, { /** * Test if given object is an instance of {@link Point} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Point}, `false` otherwise. */ export function isPoint (obj) { diff --git a/src/temporal-types.js b/src/temporal-types.js index 467dbd9c9..f9d68a3a4 100644 --- a/src/temporal-types.js +++ b/src/temporal-types.js @@ -51,15 +51,34 @@ export class Duration { * @param {Integer|number} nanoseconds the number of nanoseconds for the new duration. */ constructor (months, days, seconds, nanoseconds) { + /** + * The number of months. + * @type {Integer|number} + */ this.months = assertNumberOrInteger(months, 'Months') + /** + * The number of days. + * @type {Integer|number} + */ this.days = assertNumberOrInteger(days, 'Days') assertNumberOrInteger(seconds, 'Seconds') assertNumberOrInteger(nanoseconds, 'Nanoseconds') + /** + * The number of seconds. + * @type {Integer|number} + */ this.seconds = util.normalizeSecondsForDuration(seconds, nanoseconds) + /** + * The number of nanoseconds. + * @type {Integer|number} + */ this.nanoseconds = util.normalizeNanosecondsForDuration(nanoseconds) Object.freeze(this) } + /** + * @ignore + */ toString () { return util.durationToIsoString( this.months, @@ -78,7 +97,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Duration} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Duration}, `false` otherwise. */ export function isDuration (obj) { @@ -98,9 +117,25 @@ export class LocalTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor (hour, minute, second, nanosecond) { + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) Object.freeze(this) } @@ -123,6 +158,9 @@ export class LocalTime { ) } + /** + * @ignore + */ toString () { return util.timeToIsoString( this.hour, @@ -141,7 +179,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link LocalTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link LocalTime}, `false` otherwise. */ export function isLocalTime (obj) { @@ -163,10 +201,30 @@ export class Time { * This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC. */ constructor (hour, minute, second, nanosecond, timeZoneOffsetSeconds) { + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) + /** + * The time zone offset in seconds. + * @type {Integer|number} + */ this.timeZoneOffsetSeconds = assertNumberOrInteger( timeZoneOffsetSeconds, 'Time zone offset in seconds' @@ -193,6 +251,9 @@ export class Time { ) } + /** + * @ignore + */ toString () { return ( util.timeToIsoString( @@ -213,7 +274,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Time} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Time}, `false` otherwise. */ export function isTime (obj) { @@ -232,8 +293,20 @@ export class Date { * @param {Integer|number} day the day for the new local date. */ constructor (year, month, day) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) Object.freeze(this) } @@ -254,6 +327,9 @@ export class Date { ) } + /** + * @ignore + */ toString () { return util.dateToIsoString(this.year, this.month, this.day) } @@ -267,7 +343,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Date} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Date}, `false` otherwise. */ export function isDate (obj) { @@ -290,12 +366,40 @@ export class LocalDateTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor (year, month, day, hour, minute, second, nanosecond) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) Object.freeze(this) } @@ -321,6 +425,9 @@ export class LocalDateTime { ) } + /** + * @ignore + */ toString () { return localDateTimeToString( this.year, @@ -342,7 +449,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link LocalDateTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link LocalDateTime}, `false` otherwise. */ export function isLocalDateTime (obj) { @@ -379,19 +486,61 @@ export class DateTime { timeZoneOffsetSeconds, timeZoneId ) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) const [offset, id] = verifyTimeZoneArguments( timeZoneOffsetSeconds, timeZoneId ) + /** + * The time zone offset in seconds. + * + * *Either this or {@link timeZoneId} is defined.* + * + * @type {Integer|number} + */ this.timeZoneOffsetSeconds = offset + /** + * The time zone id. + * + * *Either this or {@link timeZoneOffsetSeconds} is defined.* + * + * @type {string} + */ this.timeZoneId = id Object.freeze(this) @@ -419,6 +568,9 @@ export class DateTime { ) } + /** + * @ignore + */ toString () { const localDateTimeStr = localDateTimeToString( this.year, @@ -444,7 +596,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link DateTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link DateTime}, `false` otherwise. */ export function isDateTime (obj) { diff --git a/src/transaction-rx.js b/src/transaction-rx.js new file mode 100644 index 000000000..4d636de3a --- /dev/null +++ b/src/transaction-rx.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { Observable } from 'rxjs' +import RxResult from './result-rx' +import Transaction from './transaction' + +/** + * A reactive transaction, which provides the same functionality as {@link Transaction} but through a Reactive API. + */ +export default class RxTransaction { + /** + * @constructor + * @protected + * @param {Transaction} txc - The underlying transaction instance to relay requests + */ + constructor (txc) { + this._txc = txc + } + + /** + * Creates a reactive result that will execute the statement in this transaction, with the provided parameters. + * + * @public + * @param {string} statement - Statement to be executed. + * @param {Object} parameters - Parameter values to use in statement execution. + * @returns {RxResult} - A reactive result + */ + + run (statement, parameters) { + return new RxResult( + new Observable(observer => { + try { + observer.next(this._txc.run(statement, parameters)) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + ) + } + + /** + * Commits the transaction. + * + * @public + * @returns {Observable} - An empty observable + */ + commit () { + return new Observable(observer => { + this._txc + .commit() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } + + /** + * Rollbacks the transaction. + * + * @public + * @returns {Observable} - An empty observable + */ + rollback () { + return new Observable(observer => { + this._txc + .rollback() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } +} diff --git a/src/transaction.js b/src/transaction.js index 68f6724db..32ba928d1 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -16,13 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import StreamObserver from './internal/stream-observer' import Result from './result' import { validateStatementAndParameters } from './internal/util' -import { EMPTY_CONNECTION_HOLDER } from './internal/connection-holder' +import ConnectionHolder, { + EMPTY_CONNECTION_HOLDER +} from './internal/connection-holder' import Bookmark from './internal/bookmark' import TxConfig from './internal/tx-config' +import { + ResultStreamObserver, + FailedObserver, + CompletedObserver +} from './internal/stream-observers' + /** * Represents a transaction in the Neo4j database. * @@ -34,28 +41,32 @@ class Transaction { * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. * @param {function()} onClose - Function to be called when transaction is committed or rolled back. * @param {function(bookmark: Bookmark)} onBookmark callback invoked when new bookmark is produced. + * @param {boolean} reactive whether this transaction generates reactive streams */ - constructor (connectionHolder, onClose, onBookmark) { + constructor ({ connectionHolder, onClose, onBookmark, reactive }) { this._connectionHolder = connectionHolder + this._reactive = reactive this._state = _states.ACTIVE this._onClose = onClose this._onBookmark = onBookmark + this._onError = this._onErrorCallback.bind(this) + this._onComplete = this._onCompleteCallback.bind(this) } _begin (bookmark, txConfig) { - const streamObserver = new _TransactionStreamObserver(this) - this._connectionHolder - .getConnection(streamObserver) + .getConnection() .then(conn => - conn.protocol().beginTransaction(streamObserver, { + conn.protocol().beginTransaction({ bookmark: bookmark, txConfig: txConfig, mode: this._connectionHolder.mode(), - database: this._connectionHolder.database() + database: this._connectionHolder.database(), + beforeError: this._onError, + afterComplete: this._onComplete }) ) - .catch(error => streamObserver.onError(error)) + .catch(error => this._onError(error)) } /** @@ -72,12 +83,12 @@ class Transaction { parameters ) - return this._state.run( - this._connectionHolder, - new _TransactionStreamObserver(this), - query, - params - ) + return this._state.run(query, params, { + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete, + reactive: this._reactive + }) } /** @@ -88,10 +99,11 @@ class Transaction { * @returns {Result} New Result */ commit () { - let committed = this._state.commit( - this._connectionHolder, - new _TransactionStreamObserver(this) - ) + let committed = this._state.commit({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete + }) this._state = committed.state // clean up this._onClose() @@ -106,10 +118,11 @@ class Transaction { * @returns {Result} New Result */ rollback () { - let committed = this._state.rollback( - this._connectionHolder, - new _TransactionStreamObserver(this) - ) + let committed = this._state.rollback({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete + }) this._state = committed.state // clean up this._onClose() @@ -124,7 +137,7 @@ class Transaction { return this._state === _states.ACTIVE } - _onError () { + _onErrorCallback (err) { // error will be "acknowledged" by sending a RESET message // database will then forget about this transaction and cleanup all corresponding resources // it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it @@ -134,232 +147,243 @@ class Transaction { // release connection back to the pool return this._connectionHolder.releaseConnection() } -} -/** Internal stream observer used for transactional results */ -class _TransactionStreamObserver extends StreamObserver { - constructor (tx) { - super() - this._tx = tx - } - - onError (error) { - if (!this._hasFailed) { - this._tx._onError().then(() => { - super.onError(error) - }) - } - } - - onCompleted (meta) { - super.onCompleted(meta) - const bookmark = new Bookmark(meta.bookmark) - this._tx._onBookmark(bookmark) + _onCompleteCallback (meta) { + this._onBookmark(new Bookmark(meta.bookmark)) } } -/** internal state machine of the transaction */ let _states = { // The transaction is running with no explicit success or failure marked ACTIVE: { - commit: (connectionHolder, observer) => { + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: finishTransaction(true, connectionHolder, observer), + result: finishTransaction(true, connectionHolder, onError, onComplete), state: _states.SUCCEEDED } }, - rollback: (connectionHolder, observer) => { + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: finishTransaction(false, connectionHolder, observer), + result: finishTransaction(false, connectionHolder, onError, onComplete), state: _states.ROLLED_BACK } }, - run: (connectionHolder, observer, statement, parameters) => { + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { // RUN in explicit transaction can't contain bookmarks and transaction configuration - const bookmark = Bookmark.empty() - const txConfig = TxConfig.empty() - - connectionHolder - .getConnection(observer) + const observerPromise = connectionHolder + .getConnection() .then(conn => - conn.protocol().run(statement, parameters, observer, { - bookmark: bookmark, - txConfig: txConfig, + conn.protocol().run(statement, parameters, { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), mode: connectionHolder.mode(), - database: connectionHolder.database() + database: connectionHolder.database(), + beforeError: onError, + afterComplete: onComplete, + reactive: reactive }) ) - .catch(error => observer.onError(error)) + .catch(error => new FailedObserver({ error, onError })) - return _newRunResult(observer, statement, parameters, () => - observer.serverMetadata() - ) + return newCompletedResult(observerPromise, statement, parameters) } }, // An error has occurred, transaction can no longer be used and no more messages will // be sent for this transaction. FAILED: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit statements in this transaction, because previous statements in the ' + - 'transaction has failed and the transaction has been rolled back. Please start a new' + - ' transaction to run another statement.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit statements in this transaction, because previous statements in the ' + + 'transaction has failed and the transaction has been rolled back. Please start a new ' + + 'transaction to run another statement.', + onError + }), + 'COMMIT', + {} + ), state: _states.FAILED } }, - rollback: (connectionHolder, observer) => { - observer.markCompleted() + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult(new CompletedObserver(), 'ROLLBACK', {}), state: _states.FAILED } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because previous statements in the ' + - 'transaction has failed and the transaction has already been rolled back.' - }) - return _newDummyResult(observer, statement, parameters) + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because previous statements in the ' + + 'transaction has failed and the transaction has already been rolled back.', + onError + }), + statement, + parameters + ) } }, // This transaction has successfully committed SUCCEEDED: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit statements in this transaction, because commit has already been successfully called on the transaction and transaction has been closed. Please start a new' + - ' transaction to run another statement.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit statements in this transaction, because commit has already been ' + + 'successfully called on the transaction and transaction has been closed. Please ' + + 'start a new transaction to run another statement.', + onError + }), + 'COMMIT', + {} + ), state: _states.SUCCEEDED } }, - rollback: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot rollback transaction, because transaction has already been successfully closed.' - }) + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot rollback transaction, because transaction has already been successfully closed.', + onError + }), + 'ROLLBACK', + {} + ), state: _states.SUCCEEDED } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because transaction has already been successfully closed.' - }) - return _newDummyResult(observer, statement, parameters) + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because transaction has already been successfully closed.', + onError + }), + statement, + parameters + ) } }, // This transaction has been rolled back ROLLED_BACK: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit this transaction, because it has already been rolled back.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit this transaction, because it has already been rolled back.', + onError + }), + 'COMMIT', + {} + ), state: _states.ROLLED_BACK } }, - rollback: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot rollback transaction, because transaction has already been rolled back.' - }) + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot rollback transaction, because transaction has already been rolled back.' + }), + 'ROLLBACK', + {} + ), state: _states.ROLLED_BACK } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because transaction has already been rolled back.' - }) - return _newDummyResult(observer, statement, parameters) + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because transaction has already been rolled back.', + onError + }), + statement, + parameters + ) } } } -function finishTransaction (commit, connectionHolder, observer) { - connectionHolder - .getConnection(observer) +/** + * + * @param {boolean} commit + * @param {ConnectionHolder} connectionHolder + * @param {function(err:Error): any} onError + * @param {function(metadata:object): any} onComplete + */ +function finishTransaction (commit, connectionHolder, onError, onComplete) { + const observerPromise = connectionHolder + .getConnection() .then(connection => { if (commit) { - return connection.protocol().commitTransaction(observer) + return connection.protocol().commitTransaction({ + beforeError: onError, + afterComplete: onComplete + }) } else { - return connection.protocol().rollbackTransaction(observer) + return connection.protocol().rollbackTransaction({ + beforeError: onError, + afterComplete: onComplete + }) } }) - .catch(error => observer.onError(error)) + .catch(error => new FailedObserver({ error, onError })) // for commit & rollback we need result that uses real connection holder and notifies it when // connection is not needed and can be safely released to the pool return new Result( - observer, + observerPromise, commit ? 'COMMIT' : 'ROLLBACK', {}, - emptyMetadataSupplier, connectionHolder ) } /** * Creates a {@link Result} with empty connection holder. - * Should be used as a result for running cypher statements. They can result in metadata but should not - * influence real connection holder to release connections because single transaction can have - * {@link Transaction#run} called multiple times. - * @param {StreamObserver} observer - an observer for the created result. - * @param {string} statement - the cypher statement that produced the result. - * @param {object} parameters - the parameters for cypher statement that produced the result. - * @param {function} metadataSupplier - the function that returns a metadata object. - * @return {Result} new result. - * @private - */ -function _newRunResult (observer, statement, parameters, metadataSupplier) { - return new Result( - observer, - statement, - parameters, - metadataSupplier, - EMPTY_CONNECTION_HOLDER - ) -} - -/** - * Creates a {@link Result} without metadata supplier and with empty connection holder. * For cases when result represents an intermediate or failed action, does not require any metadata and does not * need to influence real connection holder to release connections. - * @param {StreamObserver} observer - an observer for the created result. + * @param {ResultStreamObserver} observer - an observer for the created result. * @param {string} statement - the cypher statement that produced the result. - * @param {object} parameters - the parameters for cypher statement that produced the result. + * @param {Object} parameters - the parameters for cypher statement that produced the result. * @return {Result} new result. * @private */ -function _newDummyResult (observer, statement, parameters) { +function newCompletedResult (observerPromise, statement, parameters) { return new Result( - observer, + Promise.resolve(observerPromise), statement, parameters, - emptyMetadataSupplier, EMPTY_CONNECTION_HOLDER ) } -function emptyMetadataSupplier () { - return {} -} - export default Transaction diff --git a/test/examples.test.js b/test/examples.test.js index 370484c21..607bad217 100644 --- a/test/examples.test.js +++ b/test/examples.test.js @@ -19,6 +19,7 @@ import neo4j from '../src' import sharedNeo4j from './internal/shared-neo4j' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' /** * The tests below are examples that get pulled into the Driver Manual using the tags inside the tests. @@ -27,13 +28,17 @@ import sharedNeo4j from './internal/shared-neo4j' * DO NOT add tests to this file that are not for that exact purpose. * DO NOT modify these tests without ensuring they remain consistent with the equivalent examples in other drivers */ + describe('#integration examples', () => { + const originalConsole = console + let driverGlobal - let console + let version let originalTimeout - let testResultPromise - let resolveTestResultPromise + let consoleOverride + let consoleOverridePromise + let consoleOverridePromiseResolve const user = sharedNeo4j.username const password = sharedNeo4j.password @@ -46,20 +51,19 @@ describe('#integration examples', () => { driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken) }) - beforeEach(done => { - testResultPromise = new Promise((resolve, reject) => { - resolveTestResultPromise = resolve + beforeEach(async () => { + consoleOverridePromise = new Promise((resolve, reject) => { + consoleOverridePromiseResolve = resolve }) - - // Override console.log, to assert on stdout output - console = { log: resolveTestResultPromise } + consoleOverride = { log: msg => consoleOverridePromiseResolve(msg) } const session = driverGlobal.session() - session.run('MATCH (n) DETACH DELETE n').then(() => { - session.close(() => { - done() - }) - }) + try { + const result = await session.run('MATCH (n) DETACH DELETE n') + version = ServerVersion.fromString(result.summary.server.version) + } finally { + await session.close() + } }) afterAll(() => { @@ -67,35 +71,37 @@ describe('#integration examples', () => { driverGlobal.close() }) - it('autocommit transaction example', done => { + it('autocommit transaction example', async () => { const driver = driverGlobal // tag::autocommit-transaction[] - function addPerson (name) { + async function addPerson (name) { const session = driver.session() - return session - .run('CREATE (a:Person {name: $name})', { name: name }) - .then(result => { - session.close() - return result + try { + return await session.run('CREATE (a:Person {name: $name})', { + name: name }) + } finally { + await session.close() + } } // end::autocommit-transaction[] - addPerson('Alice').then(() => { - const session = driver.session() - session - .run('MATCH (a:Person {name: $name}) RETURN count(a) AS result', { + await addPerson('Alice') + + const session = driver.session() + try { + const result = await session.run( + 'MATCH (a:Person {name: $name}) RETURN count(a) AS result', + { name: 'Alice' - }) - .then(result => { - session.close(() => { - expect(result.records[0].get('result').toInt()).toEqual(1) - done() - }) - }) - }) + } + ) + expect(result.records[0].get('result').toInt()).toEqual(1) + } finally { + await session.close() + } }) it('basic auth example', done => { @@ -152,6 +158,10 @@ describe('#integration examples', () => { }) it('config trust example', done => { + if (version.compareTo(VERSION_4_0_0) >= 0) { + pending('address within security work') + } + // tag::config-trust[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { encrypted: 'ENCRYPTION_ON', @@ -238,270 +248,253 @@ describe('#integration examples', () => { driver.close() }) - it('cypher error example', done => { + it('cypher error example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Bob' // tag::cypher-error[] const session = driver.session() - - const readTxPromise = session.readTransaction(tx => - tx.run('SELECT * FROM Employees WHERE name = $name', { name: personName }) - ) - - readTxPromise.catch(error => { - session.close() + try { + await session.readTransaction(tx => + tx.run('SELECT * FROM Employees WHERE name = $name', { + name: personName + }) + ) + } catch (error) { console.log(error.message) - }) + } finally { + await session.close() + } // end::cypher-error[] - testResultPromise.then(loggedMsg => { - expect(removeLineBreaks(loggedMsg)).toBe( - removeLineBreaks( - "Invalid input 'L': expected 't/T' (line 1, column 3 (offset: 2))\n" + - '"SELECT * FROM Employees WHERE name = $name"\n' + - ' ^' - ) + expect(removeLineBreaks(await consoleLoggedMsg)).toBe( + removeLineBreaks( + "Invalid input 'L': expected 't/T' (line 1, column 3 (offset: 2))\n" + + '"SELECT * FROM Employees WHERE name = $name"\n' + + ' ^' ) - done() - }) + ) }) - it('driver lifecycle example', done => { + it('driver lifecycle example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise + // tag::driver-lifecycle[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) - driver - .verifyConnectivity() - .then(() => { - console.log('Driver created') - }) - .catch(error => { - console.log(`connectivity verification failed. ${error}`) - }) + try { + await driver.verifyConnectivity() + console.log('Driver created') + } catch (error) { + console.log(`connectivity verification failed. ${error}`) + } const session = driver.session() - session - .run('CREATE (i:Item)') - .then(() => { - session.close() + try { + await session.run('CREATE (i:Item)') + } catch (error) { + console.log(`unable to execute statement. ${error}`) + } finally { + await session.close() + } - // ... on application exit: - driver.close() - }) - .catch(error => { - console.log(`unable to execute statement. ${error}`) - }) + // ... on application exit: + driver.close() // end::driver-lifecycle[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual('Driver created') - done() - }) + expect(await consoleLoggedMsg).toEqual('Driver created') }) - it('hello world example', done => { + it('hello world example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise // tag::hello-world[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) const session = driver.session() - const resultPromise = session.writeTransaction(tx => - tx.run( - 'CREATE (a:Greeting) SET a.message = $message RETURN a.message + ", from node " + id(a)', - { message: 'hello, world' } + try { + const result = await session.writeTransaction(tx => + tx.run( + 'CREATE (a:Greeting) SET a.message = $message RETURN a.message + ", from node " + id(a)', + { message: 'hello, world' } + ) ) - ) - - resultPromise.then(result => { - session.close() const singleRecord = result.records[0] const greeting = singleRecord.get(0) console.log(greeting) + } finally { + await session.close() + } - // on application exit: - driver.close() - }) + // on application exit: + driver.close() // end::hello-world[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg.indexOf('hello, world, from node') === 0).toBeTruthy() - done() - }) + expect(await consoleLoggedMsg).toContain('hello, world, from node') }) const require = () => { return neo4j } - it('language guide page example', done => { + it('language guide page example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise // tag::language-guide-page[] const neo4j = require('neo4j-driver') const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) const session = driver.session() - const personName = 'Alice' - const resultPromise = session.run( - 'CREATE (a:Person {name: $name}) RETURN a', - { name: personName } - ) - resultPromise.then(result => { - session.close() + try { + const result = await session.run( + 'CREATE (a:Person {name: $name}) RETURN a', + { name: personName } + ) const singleRecord = result.records[0] const node = singleRecord.get(0) console.log(node.properties.name) + } finally { + await session.close() + } - // on application exit: - driver.close() - }) + // on application exit: + driver.close() // end::language-guide-page[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual(personName) - done() - }) + expect(await consoleLoggedMsg).toEqual(personName) }) - it('read write transaction example', done => { + it('read write transaction example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::read-write-transaction[] const session = driver.session() - const writeTxPromise = session.writeTransaction(tx => - tx.run('CREATE (a:Person {name: $name})', { name: personName }) - ) + try { + await session.writeTransaction(tx => + tx.run('CREATE (a:Person {name: $name})', { name: personName }) + ) - writeTxPromise.then(() => { - const readTxPromise = session.readTransaction(tx => + const result = await session.readTransaction(tx => tx.run('MATCH (a:Person {name: $name}) RETURN id(a)', { name: personName }) ) - readTxPromise.then(result => { - session.close() - - const singleRecord = result.records[0] - const createdNodeId = singleRecord.get(0) + const singleRecord = result.records[0] + const createdNodeId = singleRecord.get(0) - console.log('Matched created node with id: ' + createdNodeId) - }) - }) + console.log('Matched created node with id: ' + createdNodeId) + } finally { + await session.close() + } // end::read-write-transaction[] - testResultPromise.then(loggedMsg => { - expect( - loggedMsg.indexOf('Matched created node with id') === 0 - ).toBeTruthy() - done() - }) + expect(await consoleLoggedMsg).toContain('Matched created node with id') }) - it('result consume example', done => { + it('result consume example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const names = { nameA: 'Alice', nameB: 'Bob' } const tmpSession = driver.session() + try { + await tmpSession.run( + 'CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', + names + ) + // tag::result-consume[] + const session = driver.session() + const result = session.run( + 'MATCH (a:Person) RETURN a.name ORDER BY a.name' + ) + const collectedNames = [] - tmpSession - .run('CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', names) - .then(() => { - tmpSession.close(() => { - // tag::result-consume[] - const session = driver.session() - const result = session.run( - 'MATCH (a:Person) RETURN a.name ORDER BY a.name' - ) - const collectedNames = [] - - result.subscribe({ - onNext: record => { - const name = record.get(0) - collectedNames.push(name) - }, - onCompleted: () => { - session.close() - - console.log('Names: ' + collectedNames.join(', ')) - }, - onError: error => { - console.log(error) - } - }) - // end::result-consume[] - }) + result.subscribe({ + onNext: record => { + const name = record.get(0) + collectedNames.push(name) + }, + onCompleted: () => { + session.close() + + console.log('Names: ' + collectedNames.join(', ')) + }, + onError: error => { + console.log(error) + } }) + // end::result-consume[] + } finally { + await tmpSession.close() + } - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual('Names: Alice, Bob') - done() - }) + expect(await consoleLoggedMsg).toEqual('Names: Alice, Bob') }) - it('result retain example', done => { + it('result retain example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const companyName = 'Acme' const personNames = { nameA: 'Alice', nameB: 'Bob' } const tmpSession = driver.session() - tmpSession - .run( + try { + await tmpSession.run( 'CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', personNames ) - .then(() => { - tmpSession.close(() => { - // tag::result-retain[] - const session = driver.session() - const readTxPromise = session.readTransaction(tx => - tx.run('MATCH (a:Person) RETURN a.name AS name') + // tag::result-retain[] + const session = driver.session() + try { + const result = await session.readTransaction(tx => + tx.run('MATCH (a:Person) RETURN a.name AS name') + ) + + const nameRecords = result.records + for (let i = 0; i < nameRecords.length; i++) { + const name = nameRecords[i].get('name') + + await session.writeTransaction(tx => + tx.run( + 'MATCH (emp:Person {name: $person_name}) ' + + 'MERGE (com:Company {name: $company_name}) ' + + 'MERGE (emp)-[:WORKS_FOR]->(com)', + { person_name: name, company_name: companyName } + ) ) + } - const addEmployeesPromise = readTxPromise.then(result => { - const nameRecords = result.records - - let writeTxsPromise = Promise.resolve() - for (let i = 0; i < nameRecords.length; i++) { - const name = nameRecords[i].get('name') - - writeTxsPromise = writeTxsPromise.then(() => - session.writeTransaction(tx => - tx.run( - 'MATCH (emp:Person {name: $person_name}) ' + - 'MERGE (com:Company {name: $company_name}) ' + - 'MERGE (emp)-[:WORKS_FOR]->(com)', - { person_name: name, company_name: companyName } - ) - ) - ) - } - - return writeTxsPromise.then(() => nameRecords.length) - }) - - addEmployeesPromise.then(employeesCreated => { - session.close() - console.log('Created ' + employeesCreated + ' employees') - }) - // end::result-retain[] - }) - }) + console.log(`Created ${nameRecords.length} employees`) + } finally { + await session.close() + } + // end::result-retain[] + } finally { + await tmpSession.close() + } - testResultPromise.then(loggedMsg => { - driver.close() - expect(loggedMsg).toEqual('Created 2 employees') - done() - }) + expect(await consoleLoggedMsg).toEqual('Created 2 employees') }) it('service unavailable example', done => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const uri = 'bolt://localhost:7688' // wrong port const password = 'wrongPassword' @@ -522,7 +515,7 @@ describe('#integration examples', () => { }) // end::service-unavailable[] - testResultPromise.then(loggedMsg => { + consoleLoggedMsg.then(loggedMsg => { driver.close() expect(loggedMsg).toBe( 'Unable to create node: ' + neo4j.error.SERVICE_UNAVAILABLE @@ -531,51 +524,48 @@ describe('#integration examples', () => { }) }) - it('session example', done => { + it('session example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::session[] const session = driver.session() - session - .run('CREATE (a:Person {name: $name})', { name: personName }) - .then(() => { - session.close(() => { - console.log('Person created, session closed') - }) - }) + try { + await session.run('CREATE (a:Person {name: $name})', { name: personName }) + console.log('Person created, session closed') + } finally { + await session.close() + } // end::session[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toBe('Person created, session closed') - done() - }) + expect(await consoleLoggedMsg).toBe('Person created, session closed') }) - it('transaction function example', done => { + it('transaction function example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::transaction-function[] const session = driver.session() - const writeTxPromise = session.writeTransaction(tx => - tx.run('CREATE (a:Person {name: $name})', { name: personName }) - ) - - writeTxPromise.then(result => { - session.close() + try { + const result = await session.writeTransaction(tx => + tx.run('CREATE (a:Person {name: $name})', { name: personName }) + ) if (result) { console.log('Person created') } - }) + } finally { + await session.close() + } // end::transaction-function[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toBe('Person created') - done() - }) + expect(await consoleLoggedMsg).toBe('Person created') }) it('pass bookmarks example', done => { diff --git a/test/internal/bolt-protocol-v1.test.js b/test/internal/bolt-protocol-v1.test.js index 8d1e9c73f..0e88d3bf2 100644 --- a/test/internal/bolt-protocol-v1.test.js +++ b/test/internal/bolt-protocol-v1.test.js @@ -23,6 +23,7 @@ import Bookmark from '../../src/internal/bookmark' import TxConfig from '../../src/internal/tx-config' import { WRITE } from '../../src/driver' import utils from './test-utils' +import { LoginObserver } from '../../src/internal/stream-observers' describe('#unit BoltProtocolV1', () => { beforeEach(() => { @@ -56,11 +57,22 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) + const onError = error => {} + const onComplete = () => {} const clientName = 'js-driver/1.2.3' const authToken = { username: 'neo4j', password: 'secret' } - const observer = {} - protocol.initialize(clientName, authToken, observer) + const observer = protocol.initialize({ + userAgent: clientName, + authToken, + onError, + onComplete + }) + + expect(observer).toBeTruthy() + expect(observer instanceof LoginObserver).toBeTruthy() + expect(observer._afterError).toBe(onError) + expect(observer._afterComplete).toBe(onComplete) recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage( @@ -76,9 +88,7 @@ describe('#unit BoltProtocolV1', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), mode: WRITE @@ -98,9 +108,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.reset(observer) + const observer = protocol.reset() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.reset()) @@ -113,9 +121,8 @@ describe('#unit BoltProtocolV1', () => { const protocol = new BoltProtocolV1(recorder, null, false) const bookmark = new Bookmark('neo4j:bookmark:v1:tx42') - const observer = {} - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark: bookmark, txConfig: TxConfig.empty(), mode: WRITE @@ -135,9 +142,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.commitTransaction(observer) + const observer = protocol.commitTransaction() recorder.verifyMessageCount(2) @@ -151,9 +156,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.rollbackTransaction(observer) + const observer = protocol.rollbackTransaction() recorder.verifyMessageCount(2) @@ -164,14 +167,14 @@ describe('#unit BoltProtocolV1', () => { }) describe('Bolt V3', () => { + /** + * @param {function(protocol: BoltProtocolV1)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( 'Driver is connected to the database that does not support transaction configuration. ' + 'Please upgrade to neo4j 3.5.0 or later in order to use this functionality' ) @@ -179,9 +182,7 @@ describe('#unit BoltProtocolV1', () => { describe('beginTransaction', () => { function verifyBeginTransaction (txConfig) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { txConfig }) - ) + verifyError(protocol => protocol.beginTransaction({ txConfig })) } it('should throw error when txConfig.timeout is set', () => { @@ -202,7 +203,7 @@ describe('#unit BoltProtocolV1', () => { describe('run', () => { function verifyRun (txConfig) { verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { txConfig }) + protocol.run('statement', {}, { txConfig }) ) } @@ -221,14 +222,14 @@ describe('#unit BoltProtocolV1', () => { }) describe('Bolt V4', () => { + /** + * @param {function(protocol: BoltProtocolV1)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( 'Driver is connected to the database that does not support multiple databases. ' + 'Please upgrade to neo4j 4.0.0 or later in order to use this functionality' ) @@ -236,9 +237,7 @@ describe('#unit BoltProtocolV1', () => { describe('beginTransaction', () => { function verifyBeginTransaction (database) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { database }) - ) + verifyError(protocol => protocol.beginTransaction({ database })) } it('should throw error when database is set', () => { @@ -248,9 +247,7 @@ describe('#unit BoltProtocolV1', () => { describe('run', () => { function verifyRun (database) { - verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { database }) - ) + verifyError(protocol => protocol.run('statement', {}, { database })) } it('should throw error when database is set', () => { diff --git a/test/internal/bolt-protocol-v3.test.js b/test/internal/bolt-protocol-v3.test.js index d26e13610..2c4471f72 100644 --- a/test/internal/bolt-protocol-v3.test.js +++ b/test/internal/bolt-protocol-v3.test.js @@ -49,9 +49,8 @@ describe('#unit BoltProtocolV3', () => { const clientName = 'js-driver/1.2.3' const authToken = { username: 'neo4j', password: 'secret' } - const observer = {} - protocol.initialize(clientName, authToken, observer) + const observer = protocol.initialize({ userAgent: clientName, authToken }) recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage( @@ -75,9 +74,8 @@ describe('#unit BoltProtocolV3', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark, txConfig, mode: WRITE @@ -109,9 +107,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark, txConfig, mode: WRITE @@ -129,9 +125,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.commitTransaction(observer) + const observer = protocol.commitTransaction() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.commit()) @@ -143,9 +137,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.rollbackTransaction(observer) + const observer = protocol.rollbackTransaction() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.rollback()) @@ -154,14 +146,14 @@ describe('#unit BoltProtocolV3', () => { }) describe('Bolt V4', () => { + /** + * @param {function(protocol: BoltProtocolV3)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( 'Driver is connected to the database that does not support multiple databases. ' + 'Please upgrade to neo4j 4.0.0 or later in order to use this functionality' ) @@ -169,9 +161,7 @@ describe('#unit BoltProtocolV3', () => { describe('beginTransaction', () => { function verifyBeginTransaction (database) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { database }) - ) + verifyError(protocol => protocol.beginTransaction({ database })) } it('should throw error when database is set', () => { @@ -181,9 +171,7 @@ describe('#unit BoltProtocolV3', () => { describe('run', () => { function verifyRun (database) { - verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { database }) - ) + verifyError(protocol => protocol.run('statement', {}, { database })) } it('should throw error when database is set', () => { diff --git a/test/internal/bolt-protocol-v4.test.js b/test/internal/bolt-protocol-v4.test.js index 6868e59b9..a5a3bfa0c 100644 --- a/test/internal/bolt-protocol-v4.test.js +++ b/test/internal/bolt-protocol-v4.test.js @@ -44,9 +44,8 @@ describe('#unit BoltProtocolV4', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark, txConfig, database, @@ -81,9 +80,7 @@ describe('#unit BoltProtocolV4', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV4(recorder, null, false) - const observer = {} - - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark, txConfig, database, diff --git a/test/internal/connection-channel.test.js b/test/internal/connection-channel.test.js index 1eca98b16..26cee3c52 100644 --- a/test/internal/connection-channel.test.js +++ b/test/internal/connection-channel.test.js @@ -18,6 +18,7 @@ */ import DummyChannel from './dummy-channel' +import Connection from '../../src/internal/connection' import ChannelConnection from '../../src/internal/connection-channel' import { Packer } from '../../src/internal/packstream-v1' import { Chunker } from '../../src/internal/chunking' @@ -27,13 +28,13 @@ import sharedNeo4j from '../internal/shared-neo4j' import { ServerVersion, VERSION_3_5_0 } from '../../src/internal/server-version' import lolex from 'lolex' import Logger from '../../src/internal/logger' -import StreamObserver from '../../src/internal/stream-observer' import ConnectionErrorHandler from '../../src/internal/connection-error-handler' import testUtils from '../internal/test-utils' import Bookmark from '../../src/internal/bookmark' import TxConfig from '../../src/internal/tx-config' import { WRITE } from '../../src/driver' import ServerAddress from '../../src/internal/server-address' +import { ResultStreamObserver } from '../../src/internal/stream-observers' const ILLEGAL_MESSAGE = { signature: 42, fields: [] } const SUCCESS_MESSAGE = { signature: 0x70, fields: [{}] } @@ -41,6 +42,7 @@ const FAILURE_MESSAGE = { signature: 0x7f, fields: [newError('Hello')] } const RECORD_MESSAGE = { signature: 0x71, fields: [{ value: 'Hello' }] } describe('#integration ChannelConnection', () => { + /** @type {Connection} */ let connection afterEach(done => { @@ -69,9 +71,11 @@ describe('#integration ChannelConnection', () => { connection = createConnection('bolt://localhost') connection._negotiateProtocol().then(() => { - connection.protocol().initialize('mydriver/0.0.0', basicAuthToken(), { - onCompleted: msg => { - expect(msg).not.toBeNull() + connection.protocol().initialize({ + userAgent: 'mydriver/0.0.0', + authToken: basicAuthToken(), + onComplete: metadata => { + expect(metadata).not.toBeNull() done() }, onError: console.log @@ -92,15 +96,20 @@ describe('#integration ChannelConnection', () => { done() } } - const streamObserver = new StreamObserver() - streamObserver.subscribe(pullAllObserver) connection.connect('mydriver/0.0.0', basicAuthToken()).then(() => { - connection.protocol().run('RETURN 1.0', {}, streamObserver, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: WRITE - }) + connection + .protocol() + .run( + 'RETURN 1.0', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: WRITE + } + ) + .subscribe(pullAllObserver) }) }) @@ -218,7 +227,7 @@ describe('#integration ChannelConnection', () => { expect(connection.isOpen()).toBeFalsy() - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.subscribe({ onError: error => { expect(error).toEqual(initialError) @@ -239,7 +248,8 @@ describe('#integration ChannelConnection', () => { it('should not queue INIT observer when broken', done => { testQueueingOfObserversWithBrokenConnection( - connection => connection.protocol().initialize('Hello', {}, {}), + connection => + connection.protocol().initialize({ userAgent: 'Hello', authToken: {} }), done ) }) @@ -252,7 +262,6 @@ describe('#integration ChannelConnection', () => { .run( 'RETURN 1', {}, - {}, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty() } ), done @@ -321,7 +330,7 @@ describe('#integration ChannelConnection', () => { .then(() => done.fail('Should fail')) .catch(error => { expect(error.message).toEqual( - 'Received RECORD as a response for RESET: {"value":"Hello"}' + 'Received RECORD when resetting: received record is: {"value":"Hello"}' ) expect(connection._isBroken).toBeTruthy() expect(connection.isOpen()).toBeFalsy() diff --git a/test/internal/connection-holder.test.js b/test/internal/connection-holder.test.js index 7c04a7b9e..7cbcaec76 100644 --- a/test/internal/connection-holder.test.js +++ b/test/internal/connection-holder.test.js @@ -23,11 +23,11 @@ import ConnectionHolder, { import SingleConnectionProvider from '../../src/internal/connection-provider-single' import { READ, WRITE } from '../../src/driver' import FakeConnection from './fake-connection' -import StreamObserver from '../../src/internal/stream-observer' +import Connection from '../../src/internal/connection' describe('#unit EmptyConnectionHolder', () => { it('should return rejected promise instead of connection', done => { - EMPTY_CONNECTION_HOLDER.getConnection(new StreamObserver()).catch(() => { + EMPTY_CONNECTION_HOLDER.getConnection().catch(() => { done() }) }) @@ -60,7 +60,7 @@ describe('#unit ConnectionHolder', () => { expect(connectionProvider.acquireConnectionInvoked).toBe(1) }) - it('should return acquired during initialization connection', done => { + it('should return connection promise', done => { const connection = new FakeConnection() const connectionProvider = newSingleConnectionProvider(connection) const connectionHolder = new ConnectionHolder({ @@ -70,25 +70,24 @@ describe('#unit ConnectionHolder', () => { connectionHolder.initializeConnection() - connectionHolder.getConnection(new StreamObserver()).then(conn => { + connectionHolder.getConnection().then(conn => { expect(conn).toBe(connection) done() }) }) - it('should make stream observer aware about connection when initialization successful', done => { + it('should return connection promise with version', done => { const connection = new FakeConnection().withServerVersion('Neo4j/9.9.9') const connectionProvider = newSingleConnectionProvider(connection) const connectionHolder = new ConnectionHolder({ mode: READ, connectionProvider }) - const streamObserver = new StreamObserver() connectionHolder.initializeConnection() - connectionHolder.getConnection(streamObserver).then(conn => { - verifyConnection(streamObserver, 'Neo4j/9.9.9') + connectionHolder.getConnection().then(conn => { + verifyConnection(conn, 'Neo4j/9.9.9') done() }) }) @@ -101,11 +100,10 @@ describe('#unit ConnectionHolder', () => { mode: READ, connectionProvider }) - const streamObserver = new StreamObserver() connectionHolder.initializeConnection() - connectionHolder.getConnection(streamObserver).catch(error => { + connectionHolder.getConnection().catch(error => { expect(error.message).toEqual(errorMessage) done() }) @@ -310,11 +308,12 @@ function newSingleConnectionProvider (connection) { return new SingleConnectionProvider(Promise.resolve(connection)) } -function verifyConnection (streamObserver, expectedServerVersion) { - expect(streamObserver._conn).toBeDefined() - expect(streamObserver._conn).not.toBeNull() - - // server version is taken from connection, verify it as well - const metadata = streamObserver.serverMetadata() - expect(metadata.server.version).toEqual(expectedServerVersion) +/** + * @param {Connection} connection + * @param {*} expectedServerVersion + */ +function verifyConnection (connection, expectedServerVersion) { + expect(connection).toBeDefined() + expect(connection.server).toBeDefined() + expect(connection.server.version).toEqual(expectedServerVersion) } diff --git a/test/internal/node/direct.driver.boltkit.test.js b/test/internal/node/direct.driver.boltkit.test.js index 13e08d5ea..a1b4a730a 100644 --- a/test/internal/node/direct.driver.boltkit.test.js +++ b/test/internal/node/direct.driver.boltkit.test.js @@ -105,7 +105,7 @@ describe('#stub-direct direct driver with stub server', () => { tx.commit().then(() => { expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -156,7 +156,7 @@ describe('#stub-direct direct driver with stub server', () => { tx.commit().then(() => { expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -218,7 +218,7 @@ describe('#stub-direct direct driver with stub server', () => { 'neo4j:bookmark:v1:tx424242' ) - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -307,7 +307,7 @@ describe('#stub-direct direct driver with stub server', () => { const records = result.records expect(records.length).toEqual(1) expect(records[0].get(0).toNumber()).toEqual(42) - session.close(() => { + session.close().then(() => { expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) driver.close() server.exit(code => { @@ -362,7 +362,7 @@ describe('#stub-direct direct driver with stub server', () => { ) expect(error.message).toEqual('/ by zero') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -415,7 +415,7 @@ describe('#stub-direct direct driver with stub server', () => { .then(result => { const names = result.records.map(record => record.get(0)) expect(names).toEqual(['Foo', 'Bar']) - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -475,7 +475,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(connectionKey).toBeTruthy() const connection = openConnections(driver, connectionKey) - session.close(() => { + session.close().then(() => { // generate a fake fatal error connection._handleFatalError( newError('connection reset', SERVICE_UNAVAILABLE) @@ -539,7 +539,7 @@ describe('#stub-direct direct driver with stub server', () => { ) ) .then(() => - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { diff --git a/test/internal/node/routing.driver.boltkit.test.js b/test/internal/node/routing.driver.boltkit.test.js index 52e859a6c..0c22d8d6c 100644 --- a/test/internal/node/routing.driver.boltkit.test.js +++ b/test/internal/node/routing.driver.boltkit.test.js @@ -914,7 +914,7 @@ describe('#stub-routing routing driver with stub server', () => { // When const session1 = driver.session({ defaultAccessMode: WRITE }) session1.run("CREATE (n {name:'Bob'})").then(() => { - session1.close(() => { + session1.close().then(() => { const openConnectionsCount = numberOfOpenConnections(driver) const session2 = driver.session({ defaultAccessMode: WRITE }) session2.run('CREATE ()').then(() => { @@ -1134,7 +1134,7 @@ describe('#stub-routing routing driver with stub server', () => { session .run('MATCH (n) RETURN n.name') .then(() => { - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -1495,7 +1495,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(3) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenReader.exit(code2 => { @@ -1545,7 +1545,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(0) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenWriter.exit(code2 => { @@ -1602,7 +1602,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual(SESSION_EXPIRED) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenReader1.exit(code2 => { @@ -1659,7 +1659,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual(SESSION_EXPIRED) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenWriter1.exit(code2 => { @@ -1717,7 +1717,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(3) expect(invocations).toEqual(3) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { brokenReader1.exit(code2 => { @@ -1781,7 +1781,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(0) expect(invocations).toEqual(3) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { brokenWriter1.exit(code2 => { @@ -1827,7 +1827,7 @@ describe('#stub-routing routing driver with stub server', () => { const session = driver.session({ defaultAccessMode: READ }) session.run('MATCH (n) RETURN n.name').then(result => { expect(result.records.length).toEqual(3) - session.close(() => { + session.close().then(() => { // stop existing router and reader router1.exit(code1 => { tmpReader.exit(code2 => { @@ -1852,7 +1852,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[0].get('name')).toEqual('Bob') expect(records[1].get('name')).toEqual('Alice') - session.close(() => { + session.close().then(() => { driver.close() router2.exit(code => { expect(code).toEqual(0) @@ -1903,7 +1903,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[0].get('name')).toEqual('Bob') expect(records[1].get('name')).toEqual('Alice') - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { router2.exit(code2 => { @@ -1937,7 +1937,7 @@ describe('#stub-routing routing driver with stub server', () => { const names = result.records.map(record => record.get('name')) expect(names).toEqual(['Alice', 'Bob']) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code => { expect(code).toEqual(0) @@ -1979,7 +1979,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result2.records.length).toEqual(3) expect(result2.summary.server.address).toEqual('127.0.0.1:9004') - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { reader1.exit(code2 => { @@ -2017,7 +2017,7 @@ describe('#stub-routing routing driver with stub server', () => { const session = driver.session({ defaultAccessMode: READ }) session.run('MATCH (n) RETURN n.name').then(result => { - session.close(() => { + session.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2065,7 +2065,7 @@ describe('#stub-routing routing driver with stub server', () => { readSession .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) .then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2124,7 +2124,7 @@ describe('#stub-routing routing driver with stub server', () => { readSession .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) .then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2139,7 +2139,7 @@ describe('#stub-routing routing driver with stub server', () => { boltStub.run(() => { const writeSession = driver.session({ defaultAccessMode: WRITE }) writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close(() => { + writeSession.close().then(() => { expect(result.records).toEqual([]) driver.close() @@ -2193,7 +2193,7 @@ describe('#stub-routing routing driver with stub server', () => { const readSession = driver.session({ defaultAccessMode: READ }) readSession.run('MATCH (n) RETURN n.name').then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2206,7 +2206,7 @@ describe('#stub-routing routing driver with stub server', () => { const writeSession = driver.session({ defaultAccessMode: WRITE }) writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close(() => { + writeSession.close().then(() => { expect(result.records).toEqual([]) driver.close() @@ -2249,7 +2249,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized') expect(error.message).toEqual('Some server auth error message') - session.close(() => { + session.close().then(() => { driver.close() router.exit(code => { expect(code).toEqual(0) @@ -2434,7 +2434,7 @@ describe('#stub-routing routing driver with stub server', () => { assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { router2.exit(code2 => { @@ -2923,7 +2923,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(hasReaderInRoutingTable(driver, serverAddress)).toBeFalsy() expect(hasWriterInRoutingTable(driver, serverAddress)).toBeFalsy() - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { @@ -3258,7 +3258,7 @@ describe('#stub-routing routing driver with stub server', () => { 'Alice', 'Tina' ]) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { diff --git a/test/internal/node/tls.test.js b/test/internal/node/tls.test.js index c938b235f..ea77c75a0 100644 --- a/test/internal/node/tls.test.js +++ b/test/internal/node/tls.test.js @@ -20,104 +20,127 @@ import neo4j from '../../../src' import path from 'path' import sharedNeo4j from '../shared-neo4j' - -describe(' #integration trust-all-certificates', () => { - let driver - - afterEach(() => { - if (driver) { +import { + ServerVersion, + VERSION_4_0_0 +} from '../../../src/internal/server-version' + +describe('#integration trust', () => { + let serverVersion + + beforeAll(async () => { + const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + try { + serverVersion = await ServerVersion.fromDriver(driver) + } finally { driver.close() } }) - it('should work with default certificate', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: 'ENCRYPTION_ON', - trust: 'TRUST_ALL_CERTIFICATES' + beforeEach(() => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('address within security work') + } + }) + + describe('trust-all-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .then(result => { - expect(result.records[0].get(0).toNumber()).toBe(1) - done() + it('should work with default certificate', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: 'ENCRYPTION_ON', + trust: 'TRUST_ALL_CERTIFICATES' }) - }) -}) - -describe('#integration trust-custom-ca-signed-certificates', () => { - let driver - afterEach(() => { - if (driver) { - driver.close() - } + // When + driver + .session() + .run('RETURN 1') + .then(result => { + expect(result.records[0].get(0).toNumber()).toBe(1) + done() + }) + }) }) - it('should reject unknown certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', - trustedCertificates: ['test/resources/random.certificate'] + describe('trust-custom-ca-signed-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .catch(err => { - expect(err.message).toContain('Server certificate is not trusted') - done() + it('should reject unknown certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', + trustedCertificates: ['test/resources/random.certificate'] }) - }) - it('should accept known certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', - trustedCertificates: [neo4jCertPath()] + // When + driver + .session() + .run('RETURN 1') + .catch(err => { + expect(err.message).toContain('Server certificate is not trusted') + done() + }) }) - // When - driver - .session() - .run('RETURN 1') - .then(done) - }) -}) - -describe('#integration trust-system-ca-signed-certificates', () => { - let driver + it('should accept known certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', + trustedCertificates: [neo4jCertPath()] + }) - afterEach(() => { - if (driver) { - driver.close() - } + // When + driver + .session() + .run('RETURN 1') + .then(done) + }) }) - it('should reject unknown certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + describe('trust-system-ca-signed-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .catch(err => { - expect(err.message).toContain('Server certificate is not trusted') - done() + it('should reject unknown certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' }) + + // When + driver + .session() + .run('RETURN 1') + .catch(err => { + expect(err.message).toContain('Server certificate is not trusted') + done() + }) + }) }) -}) -function neo4jCertPath () { - return sharedNeo4j.neo4jCertPath(path.join('build', 'neo4j')) -} + function neo4jCertPath () { + return sharedNeo4j.neo4jCertPath(path.join('build', 'neo4j')) + } +}) diff --git a/test/internal/request-message.test.js b/test/internal/request-message.test.js index d46ee0410..47b565249 100644 --- a/test/internal/request-message.test.js +++ b/test/internal/request-message.test.js @@ -184,11 +184,11 @@ describe('#unit RequestMessage', () => { verify(RequestMessage.pull({ n: 501 }), 0x3f, { n: int(501) }, 'PULL') }) - it('should create PULL message with stmt_id and n', () => { + it('should create PULL message with qid and n', () => { verify( RequestMessage.pull({ stmtId: 27, n: 1023 }), 0x3f, - { n: int(1023), stmt_id: int(27) }, + { n: int(1023), qid: int(27) }, 'PULL' ) }) @@ -206,11 +206,11 @@ describe('#unit RequestMessage', () => { ) }) - it('should create DISCARD message with stmt_id and n', () => { + it('should create DISCARD message with qid and n', () => { verify( RequestMessage.discard({ stmtId: 27, n: 1023 }), 0x2f, - { n: int(1023), stmt_id: int(27) }, + { n: int(1023), qid: int(27) }, 'DISCARD' ) }) diff --git a/test/internal/retry-logic-rx.test.js b/test/internal/retry-logic-rx.test.js new file mode 100644 index 000000000..189aebd35 --- /dev/null +++ b/test/internal/retry-logic-rx.test.js @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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, SESSION_EXPIRED, SERVICE_UNAVAILABLE } from '../../src/error' +import RxRetryLogic from '../../src/internal/retry-logic-rx' +import { defer, throwError, of } from 'rxjs' +import { TestScheduler } from 'rxjs/testing' +import Logger from '../../src/internal/logger' + +describe('#unit-rx retrylogic', () => { + let scheduler + let loggerFunc + let logger + let clock + + beforeEach(() => { + scheduler = new TestScheduler(assertDeepEqualSkipFrame) + loggerFunc = jasmine.createSpy() + logger = new Logger('debug', loggerFunc) + + clock = jasmine.clock() + clock.install() + clock.mockDate(new Date()) + }) + + afterEach(() => clock.uninstall()) + + describe('should not retry on non-transient errors', () => { + let scheduler + + beforeEach(() => { + scheduler = new TestScheduler(assertDeepEqual) + }) + + it('a js error', () => { + verifyNoRetry(new Error('a random error')) + }) + + it('a neo4j error', () => { + verifyNoRetry(newError('a neo4j error')) + }) + + it('a transaction terminated error', () => { + verifyNoRetry( + newError( + 'transaction terminated', + 'Neo.TransientError.Transaction.Terminated' + ) + ) + }) + + it('a lock client stopped error', () => { + verifyNoRetry( + newError( + 'lock client stopped', + 'Neo.TransientError.Transaction.LockClientStopped' + ) + ) + }) + + function verifyNoRetry (error) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = helpers.cold('-a-b-c-#', { a: 1, b: 2, c: 3 }, error) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-b-c-#', { a: 1, b: 2, c: 3 }, error) + }) + } + }) + + describe('should retry on transient errors', () => { + it('a database unavailable error', () => { + verifyRetry( + newError( + 'database unavailable', + 'Neo.TransientError.Database.Unavailable' + ) + ) + }) + + it('a session expired error', () => { + verifyRetry(newError('session expired', SESSION_EXPIRED)) + }) + + it('a service unavailable error', () => { + verifyRetry(newError('service unavailable', SERVICE_UNAVAILABLE)) + }) + + function verifyRetry (error) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = newFailingObserver({ value: 1, errors: [error] }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 1 }) + }) + } + }) + + describe('should log retries', () => { + it('with 1 retry', () => { + verifyLogging(1) + }) + + it('with 2 retries', () => { + verifyLogging(2) + }) + + it('with 5 retries', () => { + verifyLogging(5) + }) + + function verifyLogging (errorCount) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 60000, logger }) + const observable = newFailingObserver({ + errors: sequenceOf( + newError('session expired', SESSION_EXPIRED), + errorCount + ), + value: 10 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 10 }) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(errorCount) + expect(loggerFunc.calls.allArgs()).toEqual( + sequenceOf( + [ + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ], + errorCount + ) + ) + } + }) + + it('should not retry on success', () => { + scheduler = new TestScheduler(assertDeepEqual) + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = helpers.cold('-a-|', { a: 5 }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 5 }) + }) + }) + + it('should retry at least twice', () => { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 2000, logger }) + const observable = newFailingObserver({ + delayBy: 2000, + errors: [newError('session expired', SESSION_EXPIRED)], + value: 10 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 10 }) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(1) + expect(loggerFunc).toHaveBeenCalledWith( + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ) + }) + + it('should fail with service unavailable', () => { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 2000, logger }) + const observable = newFailingObserver({ + delayBy: 1000, + errors: sequenceOf(newError('session expired', SESSION_EXPIRED), 3), + value: 15 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe( + '-#', + null, + newError( + 'Failed after retried for 3 times in 2000 ms. Make sure that your database is online and retry again.', + SERVICE_UNAVAILABLE + ) + ) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(2) + expect(loggerFunc.calls.allArgs()).toEqual( + sequenceOf( + [ + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ], + 2 + ) + ) + }) + + function newFailingObserver ({ delayBy = 0, value, errors = [] } = {}) { + let index = 0 + return defer(() => { + if (delayBy) { + clock.tick(delayBy) + } + if (index < errors.length) { + return throwError(errors[index++]) + } else { + return of(value) + } + }) + } + + function sequenceOf (obj, n) { + return Array.from({ length: n }, _ => obj) + } + + function assertDeepEqual (actual, expected) { + expect(actual).toEqual(expected) + } + + function assertDeepEqualSkipFrame (actual, expected) { + expect(actual.length).toBeDefined() + expect(expected.length).toBeDefined() + + expect(actual.map(m => m.notification)).toEqual( + expected.map(m => m.notification) + ) + } +}) diff --git a/test/internal/stream-observer.test.js b/test/internal/stream-observer.test.js index 432605fad..ee69bcfdc 100644 --- a/test/internal/stream-observer.test.js +++ b/test/internal/stream-observer.test.js @@ -17,19 +17,17 @@ * limitations under the License. */ -import StreamObserver from '../../src/internal/stream-observer' import FakeConnection from './fake-connection' +import { ResultStreamObserver } from '../../src/internal/stream-observers' const NO_OP = () => {} -describe('#unit StreamObserver', () => { +describe('#unit ResultStreamObserver', () => { it('remembers resolved connection', () => { - const streamObserver = newStreamObserver() const connection = new FakeConnection() + const streamObserver = newStreamObserver(connection) - streamObserver.resolveConnection(connection) - - expect(streamObserver._conn).toBe(connection) + expect(streamObserver._connection).toBe(connection) }) it('remembers subscriber', () => { @@ -38,7 +36,7 @@ describe('#unit StreamObserver', () => { streamObserver.subscribe(subscriber) - expect(streamObserver._observer).toBe(subscriber) + expect(streamObserver._observers).toContain(subscriber) }) it('passes received records to the subscriber', () => { @@ -154,7 +152,7 @@ describe('#unit StreamObserver', () => { it('invokes subscribed observer only once of error', () => { const errors = [] - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.subscribe({ onError: error => errors.push(error) }) @@ -169,7 +167,7 @@ describe('#unit StreamObserver', () => { }) it('should be able to handle a single response', done => { - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.prepareToHandleSingleResponse() streamObserver.subscribe({ @@ -183,7 +181,7 @@ describe('#unit StreamObserver', () => { }) it('should mark as completed', done => { - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.markCompleted() streamObserver.subscribe({ @@ -195,8 +193,10 @@ describe('#unit StreamObserver', () => { }) }) -function newStreamObserver () { - return new StreamObserver() +function newStreamObserver (connection) { + return new ResultStreamObserver({ + connection + }) } function newObserver (onNext = NO_OP, onError = NO_OP, onCompleted = NO_OP) { diff --git a/test/rx/navigation.test.js b/test/rx/navigation.test.js new file mode 100644 index 000000000..80898df53 --- /dev/null +++ b/test/rx/navigation.test.js @@ -0,0 +1,649 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import { Notification, Observable } from 'rxjs' +import { materialize, toArray, map } from 'rxjs/operators' +import RxTransaction from '../../src/transaction-rx' + +describe('#integration-rx navigation', () => { + describe('session', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + let originalTimeout + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return keys', () => shouldReturnKeys(serverVersion, session)) + + it('should return summary', () => + shouldReturnSummary(serverVersion, session)) + + it('should return keys and records', () => + shouldReturnKeysAndRecords(serverVersion, session)) + + it('should return records and summary', () => + shouldReturnRecordsAndSummary(serverVersion, session)) + + it('should return keys, records and summary', () => + shouldReturnKeysRecordsAndSummary(serverVersion, session)) + + it('should return keys and summary but no records', () => + shouldReturnKeysAndSummaryButRecords(serverVersion, session)) + + it('should return keys even after records are complete', () => + shouldReturnKeysEvenAfterRecordsAreComplete(serverVersion, session)) + + it('should return keys even after summary is complete', () => + shouldReturnKeysEvenAfterSummaryIsComplete(serverVersion, session)) + + it('should return keys multiple times', () => + shouldReturnKeysMultipleTimes(serverVersion, session)) + + it('should return summary multiple times', () => + shouldReturnSummaryMultipleTimes(serverVersion, session)) + + it('should return records only once', () => + shouldReturnRecordsOnlyOnce(serverVersion, session)) + + it('should return empty keys for statement without return', () => + shouldReturnEmptyKeysForStatementWithNoReturn(serverVersion, session)) + + it('should return no records for statement without return', () => + shouldReturnNoRecordsForStatementWithNoReturn(serverVersion, session)) + + it('should return summary for statement without return', () => + shouldReturnSummaryForStatementWithNoReturn(serverVersion, session)) + + it('should fail on keys when run fails', () => + shouldFailOnKeysWhenRunFails(serverVersion, session)) + + it('should fail on subsequent keys when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, session)) + + it('should fail on records when run fails', () => + shouldFailOnRecordsWhenRunFails(serverVersion, session)) + + it('should fail on summary when run fails', () => + shouldFailOnSummaryWhenRunFails(serverVersion, session)) + + it('should fail on subsequent summary when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, session)) + }) + + describe('transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {RxTransaction} */ + let txc + /** @type {ServerVersion} */ + let serverVersion + let originalTimeout + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + txc = await session.beginTransaction().toPromise() + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + if (txc) { + try { + await txc.commit().toPromise() + } catch (err) { + // ignore + } + } + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return keys', () => shouldReturnKeys(serverVersion, txc)) + + it('should return summary', () => shouldReturnSummary(serverVersion, txc)) + + it('should return keys and records', () => + shouldReturnKeysAndRecords(serverVersion, txc)) + + it('should return records and summary', () => + shouldReturnRecordsAndSummary(serverVersion, txc)) + + it('should return keys, records and summary', () => + shouldReturnKeysRecordsAndSummary(serverVersion, txc)) + + it('should return keys and summary but no records', () => + shouldReturnKeysAndSummaryButRecords(serverVersion, txc)) + + it('should return keys even after records are complete', () => + shouldReturnKeysEvenAfterRecordsAreComplete(serverVersion, txc)) + + it('should return keys even after summary is complete', () => + shouldReturnKeysEvenAfterSummaryIsComplete(serverVersion, txc)) + + it('should return keys multiple times', () => + shouldReturnKeysMultipleTimes(serverVersion, txc)) + + it('should return summary multiple times', () => + shouldReturnSummaryMultipleTimes(serverVersion, txc)) + + it('should return records only once', () => + shouldReturnRecordsOnlyOnce(serverVersion, txc)) + + it('should return empty keys for statement without return', () => + shouldReturnEmptyKeysForStatementWithNoReturn(serverVersion, txc)) + + it('should return no records for statement without return', () => + shouldReturnNoRecordsForStatementWithNoReturn(serverVersion, txc)) + + it('should return summary for statement without return', () => + shouldReturnSummaryForStatementWithNoReturn(serverVersion, txc)) + + it('should fail on keys when run fails', () => + shouldFailOnKeysWhenRunFails(serverVersion, txc)) + + it('should fail on subsequent keys when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, txc)) + + it('should fail on records when run fails', () => + shouldFailOnRecordsWhenRunFails(serverVersion, txc)) + + it('should fail on summary when run fails', () => + shouldFailOnSummaryWhenRunFails(serverVersion, txc)) + + it('should fail on subsequent summary when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, txc)) + }) + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeys (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await runnable + .run("RETURN 1 as f1, true as f2, 'string' as f3") + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([ + Notification.createNext(['f1', 'f2', 'f3']), + Notification.createComplete() + ]) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertSummary( + runnable.run("RETURN 1 as f1, true as f2, 'string' as f3") + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysAndRecords (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertRecords(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnRecordsAndSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysRecordsAndSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertRecords(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysAndSummaryButRecords (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertSummary(result) + + await collectAndAssertEmpty(result.records()) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysEvenAfterRecordsAreComplete ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysEvenAfterSummaryIsComplete (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertSummary(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysMultipleTimes (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryMultipleTimes (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnRecordsOnlyOnce (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertEmpty(result.records()) + await collectAndAssertEmpty(result.records()) + await collectAndAssertEmpty(result.records()) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnEmptyKeysForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const keys = await runnable + .run('CREATE ({id : $id})', { id: 5 }) + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(keys).toEqual([ + Notification.createNext([]), + Notification.createComplete() + ]) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNoRecordsForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertEmpty( + runnable.run('CREATE ({id : $id})', { id: 5 }).records() + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertSummary( + runnable.run('CREATE ({id : $id})', { id: 5 }), + 'w' + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnKeysWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError( + result.keys(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSubsequentKeysWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + const expectedError = jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + await collectAndAssertError(result.keys(), expectedError) + await collectAndAssertError(result.keys(), expectedError) + await collectAndAssertError(result.keys(), expectedError) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnRecordsWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError( + result.records(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSummaryWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError( + result.summary(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSubsequentSummaryWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + const expectedError = jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + + await collectAndAssertError(result.summary(), expectedError) + await collectAndAssertError(result.summary(), expectedError) + await collectAndAssertError(result.summary(), expectedError) + } + + async function collectAndAssertKeys (result) { + const keys = await result + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(keys).toEqual([ + Notification.createNext(['number', 'text']), + Notification.createComplete() + ]) + } + + async function collectAndAssertRecords (result) { + const records = await result + .records() + .pipe( + map(r => [r.get(0), r.get(1)]), + materialize(), + toArray() + ) + .toPromise() + expect(records).toEqual([ + Notification.createNext([neo4j.int(1), 't1']), + Notification.createNext([neo4j.int(2), 't2']), + Notification.createNext([neo4j.int(3), 't3']), + Notification.createNext([neo4j.int(4), 't4']), + Notification.createNext([neo4j.int(5), 't5']), + Notification.createComplete() + ]) + } + + async function collectAndAssertSummary (result, expectedStatementType = 'r') { + const summary = await result + .summary() + .pipe( + map(s => s.statementType), + materialize(), + toArray() + ) + .toPromise() + expect(summary).toEqual([ + Notification.createNext(expectedStatementType), + Notification.createComplete() + ]) + } + + async function collectAndAssertEmpty (stream) { + const result = await stream + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + /** + * + * @param {Observable} stream + * @param {function(err: Error): void} expectationFunc + */ + async function collectAndAssertError (stream, expectedError) { + const result = await stream + .pipe( + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([Notification.createError(expectedError)]) + } +}) diff --git a/test/rx/session.test.js b/test/rx/session.test.js new file mode 100644 index 000000000..10a2606a3 --- /dev/null +++ b/test/rx/session.test.js @@ -0,0 +1,301 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { Notification, throwError } from 'rxjs' +import { map, materialize, toArray, concat } from 'rxjs/operators' +import neo4j from '../../src' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import sharedNeo4j from '../internal/shared-neo4j' +import { newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../../src/error' + +describe('#integration rx-session', () => { + let originalTimeout + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000 + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + }) + + it('should be able to run a simple statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .run('UNWIND [1,2,3,4] AS n RETURN n') + .records() + .pipe( + map(r => r.get('n').toInt()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createNext(3), + Notification.createNext(4), + Notification.createComplete() + ]) + }) + + it('should be able to reuse session after failure', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result1 = await session + .run('INVALID STATEMENT') + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result1).toEqual([ + Notification.createError(jasmine.stringMatching(/Invalid input/)) + ]) + + const result2 = await session + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result2).toEqual([ + Notification.createNext(1), + Notification.createComplete() + ]) + }) + + it('should run transactions without retries', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithoutRetry) RETURN 5' + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(5), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(1) + expect(await countNodes('WithoutRetry')).toBe(1) + }) + + it('should run transaction with retries on reactive failures', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithReactiveFailure) RETURN 7', + reactiveFailures: [ + newError('service unavailable', SERVICE_UNAVAILABLE), + newError('session expired', SESSION_EXPIRED), + newError('transient error', 'Neo.TransientError.Transaction.NotStarted') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(7), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(4) + expect(await countNodes('WithReactiveFailure')).toBe(1) + }) + + it('should run transaction with retries on synchronous failures', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithSyncFailure) RETURN 9', + syncFailures: [ + newError('service unavailable', SERVICE_UNAVAILABLE), + newError('session expired', SESSION_EXPIRED), + newError('transient error', 'Neo.TransientError.Transaction.NotStarted') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(9), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(4) + expect(await countNodes('WithSyncFailure')).toBe(1) + }) + + it('should fail on transactions that cannot be retried', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txcWork = new ConfigurableTransactionWork({ + statement: 'UNWIND [10, 5, 0] AS x CREATE (:Hi) RETURN 10/x' + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createError(jasmine.stringMatching(/\/ by zero/)) + ]) + + expect(txcWork.invocations).toBe(1) + expect(await countNodes('Hi')).toBe(0) + }) + + it('should fail even after a transient error', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:Person) RETURN 1', + syncFailures: [ + newError( + 'a transient error', + 'Neo.TransientError.Transaction.NotStarted' + ) + ], + reactiveFailures: [ + newError('a database error', 'Neo.Database.Not.Started') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError(jasmine.stringMatching(/a database error/)) + ]) + + expect(txcWork.invocations).toBe(2) + expect(await countNodes('Person')).toBe(0) + }) + + async function countNodes (label) { + const session = driver.rxSession() + return await session + .run(`MATCH (n:${label}) RETURN count(n)`) + .records() + .pipe( + map(r => r.get(0).toInt()), + concat(session.close()) + ) + .toPromise() + } + class ConfigurableTransactionWork { + constructor ({ statement, syncFailures = [], reactiveFailures = [] } = {}) { + this._statement = statement + this._syncFailures = syncFailures + this._syncFailuresIndex = 0 + this._reactiveFailures = reactiveFailures + this._reactiveFailuresIndex = 0 + this._invocations = 0 + } + + get invocations () { + return this._invocations + } + + work (txc) { + this._invocations++ + + if (this._syncFailuresIndex < this._syncFailures.length) { + throw this._syncFailures[this._syncFailuresIndex++] + } + + if (this._reactiveFailuresIndex < this._reactiveFailures.length) { + return throwError(this._reactiveFailures[this._reactiveFailuresIndex++]) + } + + return txc + .run(this._statement) + .records() + .pipe(map(r => r.get(0).toInt())) + } + } +}) diff --git a/test/rx/summary.test.js b/test/rx/summary.test.js new file mode 100644 index 000000000..ad1c69a33 --- /dev/null +++ b/test/rx/summary.test.js @@ -0,0 +1,683 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import RxTransaction from '../../src/transaction-rx' +import sharedNeo4j from '../internal/shared-neo4j' + +describe('#integration-rx summary', () => { + describe('session', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + await dropConstraintsAndIndices(driver) + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return non-null summary', () => + shouldReturnNonNullSummary(serverVersion, session)) + + it('should return summary with statement text', () => + shouldReturnSummaryWithStatementText(serverVersion, session)) + + it('should return summary with statement text and parameters', () => + shouldReturnSummaryWithStatementTextAndParams(serverVersion, session)) + + it('should return summary with statement type', () => + shouldReturnSummaryWithCorrectStatementType(serverVersion, session)) + + it('should return summary with correct counters for create', () => + shouldReturnSummaryWithUpdateStatisticsForCreate(serverVersion, session)) + + it('should return summary with correct counters for delete', () => + shouldReturnSummaryWithUpdateStatisticsForDelete(serverVersion, session)) + + it('should return summary with correct counters for index create', () => + shouldReturnSummaryWithUpdateStatisticsForIndexCreate( + serverVersion, + session + )) + + it('should return summary with correct counters for index drop', () => + shouldReturnSummaryWithUpdateStatisticsForIndexDrop( + serverVersion, + driver, + session + )) + + it('should return summary with correct counters for constraint create', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintCreate( + serverVersion, + session + )) + + it('should return summary with correct counters for constraint drop', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintDrop( + serverVersion, + driver, + session + )) + + it('should not return plan or profile', () => + shouldNotReturnPlanAndProfile(serverVersion, session)) + + it('should return plan but no profile', () => + shouldReturnPlanButNoProfile(serverVersion, session)) + + it('should return plan and profile', () => + shouldReturnPlanAndProfile(serverVersion, session)) + + it('should not return notification', () => + shouldNotReturnNotification(serverVersion, session)) + + it('should return notification', () => + shouldReturnNotification(serverVersion, session)) + }) + + describe('transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {RxTransaction} */ + let txc + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + txc = await session.beginTransaction().toPromise() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + await dropConstraintsAndIndices(driver) + }) + + afterEach(async () => { + if (txc) { + try { + await txc.commit().toPromise() + } catch (err) { + // ignore + } + } + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return non-null summary', () => + shouldReturnNonNullSummary(serverVersion, txc)) + + it('should return summary with statement text', () => + shouldReturnSummaryWithStatementText(serverVersion, txc)) + + it('should return summary with statement text and parameters', () => + shouldReturnSummaryWithStatementTextAndParams(serverVersion, txc)) + + it('should return summary with statement type', () => + shouldReturnSummaryWithCorrectStatementType(serverVersion, txc)) + + it('should return summary with correct counters for create', () => + shouldReturnSummaryWithUpdateStatisticsForCreate(serverVersion, txc)) + + it('should return summary with correct counters for delete', () => + shouldReturnSummaryWithUpdateStatisticsForDelete(serverVersion, txc)) + + it('should return summary with correct counters for index create', () => + shouldReturnSummaryWithUpdateStatisticsForIndexCreate(serverVersion, txc)) + + it('should return summary with correct counters for index drop', () => + shouldReturnSummaryWithUpdateStatisticsForIndexDrop( + serverVersion, + driver, + txc + )) + + it('should return summary with correct counters for constraint create', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintCreate( + serverVersion, + txc + )) + + it('should return summary with correct counters for constraint drop', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintDrop( + serverVersion, + driver, + txc + )) + + it('should not return plan or profile', () => + shouldNotReturnPlanAndProfile(serverVersion, txc)) + + it('should return plan but no profile', () => + shouldReturnPlanButNoProfile(serverVersion, txc)) + + it('should return plan and profile', () => + shouldReturnPlanAndProfile(serverVersion, txc)) + + it('should not return notification', () => + shouldNotReturnNotification(serverVersion, txc)) + + it('should return notification', () => + shouldReturnNotification(serverVersion, txc)) + }) + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNonNullSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('UNWIND RANGE(1,10) AS n RETURN n') + .summary() + .toPromise() + + expect(summary).toBeDefined() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithStatementText (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementTextAndParameters( + runnable, + 'UNWIND RANGE(1, 10) AS n RETURN n' + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithStatementTextAndParams ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementTextAndParameters( + runnable, + 'UNWIND RANGE(1, $x) AS n RETURN n', + { x: 100 } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithCorrectStatementType ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementType(runnable, 'CREATE (n)', 'w') + await verifyStatementType(runnable, 'MATCH (n) RETURN n LIMIT 1', 'r') + await verifyStatementType(runnable, 'CREATE (n) RETURN n', 'rw') + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters( + runnable, + 'CREATE (n:Label1 {id: $id1})-[:KNOWS]->(m:Label2 {id: $id2}) RETURN n, m', + { id1: 10, id2: 20 }, + { + nodesCreated: 2, + nodesDeleted: 0, + relationshipsCreated: 1, + relationshipsDeleted: 0, + propertiesSet: 2, + labelsAdded: 2, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForDelete ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-deleted nodes + await shouldReturnSummaryWithUpdateStatisticsForCreate(version, runnable) + + await verifyCounters( + runnable, + 'MATCH (n:Label1)-[r:KNOWS]->(m:Label2) DELETE n, r', + null, + { + nodesCreated: 0, + nodesDeleted: 1, + relationshipsCreated: 0, + relationshipsDeleted: 1, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForIndexCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters(runnable, 'CREATE INDEX on :Label(prop)', null, { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 1, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForIndexDrop ( + version, + driver, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-dropped index + const session = driver.session() + try { + await session.run('CREATE INDEX on :Label(prop)') + } finally { + await session.close() + } + + await verifyCounters(runnable, 'DROP INDEX on :Label(prop)', null, { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 1, + constraintsAdded: 0, + constraintsRemoved: 0 + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForConstraintCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters( + runnable, + 'CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE', + null, + { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 1, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForConstraintDrop ( + version, + driver, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-dropped index + const session = driver.session() + try { + await session.run( + 'CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE' + ) + } finally { + await session.close() + } + + await verifyCounters( + runnable, + 'DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE', + null, + { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 1 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldNotReturnPlanAndProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeFalsy() + expect(summary.plan).toBeFalsy() + expect(summary.hasProfile()).toBeFalsy() + expect(summary.profile).toBeFalsy() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnPlanButNoProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('EXPLAIN CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeTruthy() + expect(summary.plan.operatorType).toBe('ProduceResults') + expect(summary.plan.identifiers).toEqual(['n']) + expect(summary.hasProfile()).toBeFalsy() + expect(summary.profile).toBeFalsy() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnPlanAndProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('PROFILE CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeTruthy() + expect(summary.plan.operatorType).toBe('ProduceResults') + expect(summary.plan.identifiers).toEqual(['n']) + expect(summary.hasProfile()).toBeTruthy() + expect(summary.profile.operatorType).toBe('ProduceResults') + expect(summary.profile.identifiers).toEqual(['n']) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldNotReturnNotification (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.notifications).toBeTruthy() + expect(summary.notifications.length).toBe(0) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNotification (version, runnable) { + pending('seems to be flaky') + + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('CYPHER runtime=interpreted EXPLAIN MATCH (n),(m) RETURN n,m') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.notifications).toBeTruthy() + expect(summary.notifications.length).toBeGreaterThan(0) + expect(summary.notifications[0].code).toBe( + 'Neo.ClientNotification.Statement.CartesianProductWarning' + ) + expect(summary.notifications[0].title).toBe( + 'This query builds a cartesian product between disconnected patterns.' + ) + expect(summary.notifications[0].description).toBe( + 'If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifier is: (m))' + ) + expect(summary.notifications[0].severity).toBe('WARNING') + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {*} parameters + */ + async function verifyStatementTextAndParameters ( + runnable, + statement, + parameters = null + ) { + const summary = await runnable + .run(statement, parameters) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.statement).toBeDefined() + expect(summary.statement.text).toBe(statement) + expect(summary.statement.parameters).toEqual(parameters || {}) + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {string} expectedStatementType + */ + async function verifyStatementType ( + runnable, + statement, + expectedStatementType + ) { + const summary = await runnable + .run(statement) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.statementType).toBe(expectedStatementType) + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {*} parameters + * @param {*} counters + */ + async function verifyCounters (runnable, statement, parameters, counters) { + const summary = await runnable + .run(statement, parameters) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect({ + nodesCreated: summary.counters.nodesCreated(), + nodesDeleted: summary.counters.nodesDeleted(), + relationshipsCreated: summary.counters.relationshipsCreated(), + relationshipsDeleted: summary.counters.relationshipsDeleted(), + propertiesSet: summary.counters.propertiesSet(), + labelsAdded: summary.counters.labelsAdded(), + labelsRemoved: summary.counters.labelsRemoved(), + indexesAdded: summary.counters.indexesAdded(), + indexesRemoved: summary.counters.indexesRemoved(), + constraintsAdded: summary.counters.constraintsAdded(), + constraintsRemoved: summary.counters.constraintsRemoved() + }).toEqual(counters) + } + + async function dropConstraintsAndIndices (driver) { + const session = driver.session() + try { + const constraints = await session.run( + "CALL db.constraints() yield description RETURN 'DROP ' + description" + ) + for (let i = 0; i < constraints.records.length; i++) { + await session.run(constraints.records[0].get(0)) + } + + const indices = await session.run( + "CALL db.indexes() yield description RETURN 'DROP ' + description" + ) + for (let i = 0; i < indices.records.length; i++) { + await session.run(indices.records[0].get(0)) + } + } finally { + await session.close() + } + } +}) diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js new file mode 100644 index 000000000..f8e6bb4ff --- /dev/null +++ b/test/rx/transaction.test.js @@ -0,0 +1,793 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { Notification, throwError } from 'rxjs' +import { + flatMap, + materialize, + toArray, + concat, + map, + bufferCount, + catchError +} from 'rxjs/operators' +import neo4j from '../../src' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import sharedNeo4j from '../internal/shared-neo4j' +import { newError } from '../../src/error' + +describe('#integration-rx transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should commit an empty transaction', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => txc.commit()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should rollback an empty transaction', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => txc.rollback()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should run statement and commit', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('CREATE (n:Node {id: 42}) RETURN n') + .records() + .pipe( + map(r => r.get('n').properties['id']), + concat(txc.commit()) + ) + ), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(42)), + Notification.createComplete() + ]) + + expect(await countNodes(42)).toBe(1) + }) + + it('should run statement and rollback', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('CREATE (n:Node {id: 42}) RETURN n') + .records() + .pipe( + map(r => r.get('n').properties['id']), + concat(txc.rollback()) + ) + ), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(42)), + Notification.createComplete() + ]) + + expect(await countNodes(42)).toBe(0) + }) + + it('should run multiple statements and commit', async () => { + await verifyCanRunMultipleStatements(true) + }) + + it('should run multiple statements and rollback', async () => { + await verifyCanRunMultipleStatements(false) + }) + + it('should run multiple statements without waiting and commit', async () => { + await verifyCanRunMultipleStatementsWithoutWaiting(true) + }) + + it('should run multiple statements without waiting and rollback', async () => { + await verifyCanRunMultipleStatementsWithoutWaiting(false) + }) + + it('should run multiple statements without streaming and commit', async () => { + await verifyCanRunMultipleStatementsWithoutStreaming(true) + }) + + it('should run multiple statements without streaming and rollback', async () => { + await verifyCanRunMultipleStatementsWithoutStreaming(false) + }) + + it('should fail to commit after a failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit statements in this transaction/ + ) + }) + ) + ]) + }) + + it('should succeed to rollback after a failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to commit after successful and failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 5) + await verifyCanReturnOne(txc) + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit statements in this transaction/ + ) + }) + ) + ]) + }) + + it('should succeed to rollback after successful and failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 5) + await verifyCanReturnOne(txc) + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to run another statement after a failed one', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .run('CREATE ()') + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot run statement, because previous statements in the transaction has failed/ + ) + }) + ) + ]) + }) + + it('should allow commit after commit', async () => { + pending('behaviour difference across drivers') + + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanCommit(txc) + + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should allow rollback after rollback', async () => { + pending('behaviour difference across drivers') + + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanRollback(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to rollback after commit', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanCommit(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot rollback transaction, because transaction has already been successfully closed/ + ) + }) + ) + ]) + }) + + it('should fail to commit after rollback', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanRollback(txc) + + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit this transaction, because it has already been rolled back/ + ) + }) + ) + ]) + }) + + it('should fail to run statement after committed transaction', async () => { + await verifyFailToRunStatementAfterTxcIsComplete(true) + }) + + it('should fail to run statement after rollbacked transaction', async () => { + await verifyFailToRunStatementAfterTxcIsComplete(false) + }) + + it('should update bookmark', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const bookmark0 = session.lastBookmark() + + const txc1 = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc1, 20) + await verifyCanCommit(txc1) + const bookmark1 = session.lastBookmark() + + const txc2 = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc2, 21) + await verifyCanCommit(txc2) + const bookmark2 = session.lastBookmark() + + expect(bookmark0).toBeFalsy() + expect(bookmark1).toBeTruthy() + expect(bookmark1).not.toEqual(bookmark0) + expect(bookmark2).toBeTruthy() + expect(bookmark2).not.toEqual(bookmark1) + }) + + it('should propagate failures from statements', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (:TestNode) RETURN 1 AS n') + const result2 = txc.run('CREATE (:TestNode) RETURN 2 AS n') + const result3 = txc.run('RETURN 10 / 0 AS n') + const result4 = txc.run('CREATE (:TestNode) RETURN 3 AS n') + + const result = await result1 + .records() + .pipe( + concat(result2.records()), + concat(result3.records()), + concat(result4.records()), + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createError(newError('/ by zero')) + ]) + + await verifyCanRollback(txc) + }) + + it('should not run until subscribed', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('RETURN 1') + const result2 = txc.run('RETURN 2') + const result3 = txc.run('RETURN 3') + const result4 = txc.run('RETURN 4') + + const result = await result4 + .records() + .pipe( + concat(result3.records()), + concat(result2.records()), + concat(result1.records()), + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(4), + Notification.createNext(3), + Notification.createNext(2), + Notification.createNext(1), + Notification.createComplete() + ]) + + await verifyCanCommit(txc) + }) + + it('should not propagate failure on commit if not executed', async () => { + await verifyNoFailureIfNotExecuted(true) + }) + + it('should not propagate failure on rollback if not executed', async () => { + await verifyNoFailureIfNotExecuted(false) + }) + + it('should not propagate run failure from summary', async () => { + pending('behaviour difference across drivers') + + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + const result = txc.run('RETURN Wrong') + + const messages = await result + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(messages).toEqual([ + Notification.createError( + jasmine.stringMatching(/Variable `Wrong` not defined/) + ) + ]) + + const summary = await result.summary().toPromise() + expect(summary).toBeTruthy() + }) + + it('should handle nested queries', async () => { + const size = 1024 + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const messages = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('UNWIND RANGE(1, $size) AS x RETURN x', { size }) + .records() + .pipe( + map(r => r.get(0)), + bufferCount(50), + flatMap(x => + txc + .run('UNWIND $x AS id CREATE (n:Node {id: id}) RETURN n.id', { + x + }) + .records() + ), + map(r => r.get(0)), + concat(txc.commit()), + catchError(err => txc.rollback().pipe(concat(throwError(err)))), + materialize(), + toArray() + ) + ) + ) + .toPromise() + + expect(messages.length).toBe(size + 1) + expect(messages[size]).toEqual(Notification.createComplete()) + }) + + async function verifyNoFailureIfNotExecuted (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + txc.run('RETURN ILLEGAL') + + await verifyCanCommitOrRollback(txc, commit) + } + + async function verifyFailToRunStatementAfterTxcIsComplete (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc, 15) + await verifyCanCommitOrRollback(txc, commit) + + const result = await txc + .run('CREATE ()') + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot run statement, because transaction/ + ) + }) + ) + ]) + } + + async function verifyCanRunMultipleStatements (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await txc + .run('CREATE (n:Node {id: 1})') + .summary() + .toPromise() + await txc + .run('CREATE (n:Node {id: 2})') + .summary() + .toPromise() + await txc + .run('CREATE (n:Node {id: 1})') + .summary() + .toPromise() + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanRunMultipleStatementsWithoutWaiting (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (n:Node {id: 1})') + const result2 = txc.run('CREATE (n:Node {id: 2})') + const result3 = txc.run('CREATE (n:Node {id: 1})') + + const results = await result1 + .records() + .pipe( + concat(result2.records()), + concat(result3.records()), + materialize(), + toArray() + ) + .toPromise() + expect(results).toEqual([Notification.createComplete()]) + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanRunMultipleStatementsWithoutStreaming (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (n:Node {id: 1})') + const result2 = txc.run('CREATE (n:Node {id: 2})') + const result3 = txc.run('CREATE (n:Node {id: 1})') + + const results = await result1 + .keys() + .pipe( + concat(result2.keys()), + concat(result3.keys()), + materialize(), + toArray() + ) + .toPromise() + expect(results).toEqual([ + Notification.createNext([]), + Notification.createNext([]), + Notification.createNext([]), + Notification.createComplete() + ]) + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanCommit (txc) { + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + async function verifyCanRollback (txc) { + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + async function verifyCanCommitOrRollback (txc, commit) { + if (commit) { + await verifyCanCommit(txc) + } else { + await verifyCanRollback(txc) + } + } + + async function verifyCanCreateNode (txc, id) { + const result = await txc + .run('CREATE (n:Node {id: $id}) RETURN n', { id: neo4j.int(id) }) + .records() + .pipe( + map(r => r.get('n').properties['id']), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(id)), + Notification.createComplete() + ]) + } + + async function verifyCanReturnOne (txc) { + const result = await txc + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0)), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(1)), + Notification.createComplete() + ]) + } + + async function verifyFailsWithWrongStatement (txc) { + const result = await txc + .run('RETURN') + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError( + jasmine.stringMatching(/Unexpected end of input/) + ) + ]) + } + + async function verifyCommittedOrRollbacked (commit) { + if (commit) { + expect(await countNodes(1)).toBe(2) + expect(await countNodes(2)).toBe(1) + } else { + expect(await countNodes(1)).toBe(0) + expect(await countNodes(2)).toBe(0) + } + } + + async function countNodes (id) { + const session = driver.rxSession() + return await session + .run('MATCH (n:Node {id: $id}) RETURN count(n)', { id: id }) + .records() + .pipe( + map(r => r.get(0).toInt()), + concat(session.close()) + ) + .toPromise() + } +}) diff --git a/test/session.test.js b/test/session.test.js index 9716311b5..b205db634 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -25,7 +25,7 @@ import SingleConnectionProvider from '../src/internal/connection-provider-single import FakeConnection from './internal/fake-connection' import sharedNeo4j from './internal/shared-neo4j' import _ from 'lodash' -import { ServerVersion } from '../src/internal/server-version' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' import { isString } from '../src/internal/util' import testUtils from './internal/test-utils' import { newError, PROTOCOL_ERROR, SESSION_EXPIRED } from '../src/error' @@ -55,20 +55,20 @@ describe('#integration session', () => { driver.close() }) - it('close should invoke callback ', done => { + it('close should return promise', done => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(done) + session.close().then(() => done()) }) - it('close should invoke callback even when already closed ', done => { + it('close should return promise even when already closed ', done => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(() => { - session.close(() => { - session.close(() => { + session.close().then(() => { + session.close().then(() => { + session.close().then(() => { done() }) }) @@ -79,13 +79,13 @@ describe('#integration session', () => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() done() }) @@ -104,7 +104,7 @@ describe('#integration session', () => { originalClose.call(transactionExecutor) } - session.close(() => { + session.close().then(() => { expect(closeCalledTimes).toEqual(1) done() }) @@ -116,7 +116,7 @@ describe('#integration session', () => { const tx = session.beginTransaction() tx.run('INVALID QUERY').catch(() => { tx.rollback().then(() => { - session.close(() => { + session.close().then(() => { driver.close() done() }) @@ -307,6 +307,10 @@ describe('#integration session', () => { }) it('should expose cypher notifications ', done => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('seems to be flaky') + } + // Given const statement = 'EXPLAIN MATCH (n), (m) RETURN n, m' // When & Then @@ -324,7 +328,7 @@ describe('#integration session', () => { }) }) - it('should fail when using the session when having an open transaction', done => { + it('should fail when using the session with an open transaction', done => { // When session.beginTransaction() @@ -783,7 +787,7 @@ describe('#integration session', () => { it('should interrupt query waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -817,7 +821,7 @@ describe('#integration session', () => { it('should interrupt transaction waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -850,7 +854,7 @@ describe('#integration session', () => { it('should interrupt transaction function waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -990,7 +994,7 @@ describe('#integration session', () => { expect(numberOfAcquiredConnectionsFromPool()).toEqual(1) }, onCompleted: () => { - session.close(() => { + session.close().then(() => { done() }) }, @@ -1010,8 +1014,8 @@ describe('#integration session', () => { expect(numberOfAcquiredConnectionsFromPool()).toEqual(2) }, onCompleted: () => { - otherSession.close(() => { - session.close(() => { + otherSession.close().then(() => { + session.close().then(() => { done() }) }) @@ -1060,7 +1064,7 @@ describe('#integration session', () => { .run('RETURN $array', { array: iterable }) .then(result => { done.fail( - 'Failre expected but query returned ' + + 'Failure expected but query returned ' + JSON.stringify(result.records[0].get(0)) ) }) @@ -1205,7 +1209,7 @@ describe('#integration session', () => { function withQueryInTmpSession (driver, callback) { const tmpSession = driver.session() return tmpSession.run('RETURN 1').then(() => { - tmpSession.close(callback) + tmpSession.close().then(() => callback()) }) } @@ -1263,7 +1267,7 @@ describe('#integration session', () => { tx.commit() .then(() => { const bookmark = session.lastBookmark() - session.close(() => { + session.close().then(() => { resolve(bookmark) }) }) diff --git a/test/stress.test.js b/test/stress.test.js index fca065cbf..7268dba3b 100644 --- a/test/stress.test.js +++ b/test/stress.test.js @@ -236,7 +236,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode) context.log(commandId, `Query completed successfully`) - session.close(() => { + return session.close().then(() => { const possibleError = verifyQueryResult(result) callback(possibleError) }) @@ -277,7 +277,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode, session.lastBookmark()) context.log(commandId, `Transaction function executed successfully`) - session.close(() => { + return session.close().then(() => { const possibleError = verifyQueryResult(result) callback(possibleError) }) @@ -327,7 +327,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode, session.lastBookmark()) context.log(commandId, `Transaction committed successfully`) - session.close(() => { + return session.close().then(() => { callback(commandError) }) }) diff --git a/test/summary.test.js b/test/summary.test.js index 8a0963947..e0427b419 100644 --- a/test/summary.test.js +++ b/test/summary.test.js @@ -19,15 +19,17 @@ import neo4j from '../src' import sharedNeo4j from './internal/shared-neo4j' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' describe('#integration result summary', () => { - let driver, session + let driver, session, serverVersion - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() - session.run('MATCH (n) DETACH DELETE n').then(done) + const result = await session.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) }) afterEach(() => { @@ -99,6 +101,10 @@ describe('#integration result summary', () => { }) it('should get notifications from summary', done => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('seems to be flaky') + } + session.run('EXPLAIN MATCH (n), (m) RETURN n, m').then(result => { let summary = result.summary expect(summary.notifications).toBeDefined() diff --git a/test/temporal-types.test.js b/test/temporal-types.test.js index 994d08467..0eddcb883 100644 --- a/test/temporal-types.test.js +++ b/test/temporal-types.test.js @@ -86,255 +86,245 @@ describe('#integration temporal-types', () => { } }) - beforeEach(done => { + beforeEach(async () => { session = driver.session() - session - .run('MATCH (n) DETACH DELETE n') - .then(() => { - done() - }) - .catch(error => { - done.fail(error) - }) + + try { + await session.run('MATCH (n) DETACH DELETE n') + } finally { + await session.close() + } }) - afterEach(() => { + afterEach(async () => { if (session) { - session.close() + await session.close() session = null } }) - it('should receive Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = duration(27, 17, 91, 999) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN duration({years: 2, months: 3, days: 17, seconds: 91, nanoseconds: 999})', - expectedValue, - done + expectedValue ) }) - it('should send and receive random Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomDuration(), done) + await testSendAndReceiveRandomTemporalValues(() => randomDuration()) }) - it('should send and receive Duration when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Duration when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.Duration(4, 15, 931, 99953), - done + await testSendReceiveTemporalValue( + new neo4j.types.Duration(4, 15, 931, 99953) ) }) - it('should send and receive array of Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomDuration(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomDuration()) }) - it('should receive LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = localTime(22, 59, 10, 999999) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN localtime({hour: 22, minute: 59, second: 10, nanosecond: 999999})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxLocalTime = localTime(23, 59, 59, MAX_NANO_OF_SECOND) - testSendReceiveTemporalValue(maxLocalTime, done) + await testSendReceiveTemporalValue(maxLocalTime) }) - it('should send and receive min LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minLocalTime = localTime(0, 0, 0, 0) - testSendReceiveTemporalValue(minLocalTime, done) + await testSendReceiveTemporalValue(minLocalTime) }) - it('should send and receive LocalTime when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive LocalTime when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.LocalTime(12, 32, 56, 12345), - done + await testSendReceiveTemporalValue( + new neo4j.types.LocalTime(12, 32, 56, 12345) ) }) - it('should send and receive random LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomLocalTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomLocalTime()) }) - it('should send and receive array of LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomLocalTime(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomLocalTime()) }) - it('should receive Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = time(11, 42, 59, 9999, -30600) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN time({hour: 11, minute: 42, second: 59, nanosecond: 9999, timezone:"-08:30"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxTime = time(23, 59, 59, MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET) - testSendReceiveTemporalValue(maxTime, done) + await testSendReceiveTemporalValue(maxTime) }) - it('should send and receive min Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minTime = time(0, 0, 0, 0, MIN_TIME_ZONE_OFFSET) - testSendReceiveTemporalValue(minTime, done) + await testSendReceiveTemporalValue(minTime) }) - it('should send and receive Time when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Time when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.Time(22, 19, 32, 18381, MAX_TIME_ZONE_OFFSET), - done + await testSendReceiveTemporalValue( + new neo4j.types.Time(22, 19, 32, 18381, MAX_TIME_ZONE_OFFSET) ) }) - it('should send and receive random Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomTime()) }) - it('should send and receive array of Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomTime(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomTime()) }) - it('should receive Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = date(1995, 7, 28) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN date({year: 1995, month: 7, day: 28})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxDate = date(MAX_YEAR, 12, 31) - testSendReceiveTemporalValue(maxDate, done) + await testSendReceiveTemporalValue(maxDate) }) - it('should send and receive min Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minDate = date(MIN_YEAR, 1, 1) - testSendReceiveTemporalValue(minDate, done) + await testSendReceiveTemporalValue(minDate) }) - it('should send and receive Date when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Date when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue(new neo4j.types.Date(1923, 8, 14), done) + await testSendReceiveTemporalValue(new neo4j.types.Date(1923, 8, 14)) }) - it('should send and receive random Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomDate(), done) + await testSendAndReceiveRandomTemporalValues(() => randomDate()) }) - it('should send and receive array of Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomDate(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomDate()) }) - it('should receive LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = localDateTime(1869, 9, 23, 18, 29, 59, 12349) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN localdatetime({year: 1869, month: 9, day: 23, hour: 18, minute: 29, second: 59, nanosecond: 12349})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -347,51 +337,49 @@ describe('#integration temporal-types', () => { 59, MAX_NANO_OF_SECOND ) - testSendReceiveTemporalValue(maxLocalDateTime, done) + await testSendReceiveTemporalValue(maxLocalDateTime) }) - it('should send and receive min LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minLocalDateTime = localDateTime(MIN_YEAR, 1, 1, 0, 0, 0, 0) - testSendReceiveTemporalValue(minLocalDateTime, done) + await testSendReceiveTemporalValue(minLocalDateTime) }) - it('should send and receive LocalDateTime when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive LocalDateTime when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.LocalDateTime(2045, 9, 1, 11, 25, 25, 911), - done + await testSendReceiveTemporalValue( + new neo4j.types.LocalDateTime(2045, 9, 1, 11, 25, 25, 911) ) }) - it('should send and receive random LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomLocalDateTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomLocalDateTime()) }) - it('should send and receive random LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomLocalDateTime(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomLocalDateTime() ) }) - it('should receive DateTime with time zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive DateTime with time zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -405,15 +393,14 @@ describe('#integration temporal-types', () => { 999, 18000 ) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "+05:00"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -427,11 +414,11 @@ describe('#integration temporal-types', () => { MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET ) - testSendReceiveTemporalValue(maxDateTime, done) + await testSendReceiveTemporalValue(maxDateTime) }) - it('should send and receive min DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -445,16 +432,16 @@ describe('#integration temporal-types', () => { 0, MAX_TIME_ZONE_OFFSET ) - testSendReceiveTemporalValue(minDateTime, done) + await testSendReceiveTemporalValue(minDateTime) }) - it('should send and receive DateTime with zone offset when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive DateTime with zone offset when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( + await testSendReceiveTemporalValue( new neo4j.types.DateTime( 2022, 2, @@ -465,35 +452,32 @@ describe('#integration temporal-types', () => { 12399, MAX_TIME_ZONE_OFFSET, null - ), - done + ) ) }) - it('should send and receive random DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues( - () => randomDateTimeWithZoneOffset(), - done + await testSendAndReceiveRandomTemporalValues(() => + randomDateTimeWithZoneOffset() ) }) - it('should send and receive array of DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomDateTimeWithZoneOffset(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomDateTimeWithZoneOffset() ) }) - it('should receive DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -507,15 +491,14 @@ describe('#integration temporal-types', () => { 999, 'Europe/Stockholm' ) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "Europe/Stockholm"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -529,11 +512,11 @@ describe('#integration temporal-types', () => { MAX_NANO_OF_SECOND, MAX_ZONE_ID ) - testSendReceiveTemporalValue(maxDateTime, done) + await testSendReceiveTemporalValue(maxDateTime) }) - it('should send and receive min DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -547,16 +530,16 @@ describe('#integration temporal-types', () => { 0, MIN_ZONE_ID ) - testSendReceiveTemporalValue(minDateTime, done) + await testSendReceiveTemporalValue(minDateTime) }) - it('should send and receive DateTime with zone id when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive DateTime with zone id when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( + await testSendReceiveTemporalValue( new neo4j.types.DateTime( 2011, 11, @@ -567,30 +550,27 @@ describe('#integration temporal-types', () => { 192378, null, 'Europe/Stockholm' - ), - done + ) ) }) - it('should send and receive random DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues( - () => randomDateTimeWithZoneId(), - done + await testSendAndReceiveRandomTemporalValues(() => + randomDateTimeWithZoneId() ) }) - it('should send and receive array of DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomDateTimeWithZoneId(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomDateTimeWithZoneId() ) }) @@ -748,125 +728,122 @@ describe('#integration temporal-types', () => { expect(zonedDateTime.nanosecond).toEqual(neo4j.int(9346458)) }) - it('should format duration to string', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should format duration to string', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testDurationToString( - [ - { duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S' }, - - { duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S' }, - { duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S' }, - { duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S' }, - { duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S' }, - - { - duration: duration(0, 0, 0, 5), - expectedString: 'P0M0DT0.000000005S' - }, - { - duration: duration(0, 0, 0, -5), - expectedString: 'P0M0DT-0.000000005S' - }, - { - duration: duration(0, 0, 0, 999999999), - expectedString: 'P0M0DT0.999999999S' - }, - { - duration: duration(0, 0, 0, -999999999), - expectedString: 'P0M0DT-0.999999999S' - }, - - { - duration: duration(0, 0, 1, 5), - expectedString: 'P0M0DT1.000000005S' - }, - { - duration: duration(0, 0, -1, -5), - expectedString: 'P0M0DT-1.000000005S' - }, - { - duration: duration(0, 0, 1, -5), - expectedString: 'P0M0DT0.999999995S' - }, - { - duration: duration(0, 0, -1, 5), - expectedString: 'P0M0DT-0.999999995S' - }, - { - duration: duration(0, 0, 1, 999999999), - expectedString: 'P0M0DT1.999999999S' - }, - { - duration: duration(0, 0, -1, -999999999), - expectedString: 'P0M0DT-1.999999999S' - }, - { - duration: duration(0, 0, 1, -999999999), - expectedString: 'P0M0DT0.000000001S' - }, - { - duration: duration(0, 0, -1, 999999999), - expectedString: 'P0M0DT-0.000000001S' - }, - - { - duration: duration(0, 0, 28, 9), - expectedString: 'P0M0DT28.000000009S' - }, - { - duration: duration(0, 0, -28, 9), - expectedString: 'P0M0DT-27.999999991S' - }, - { - duration: duration(0, 0, 28, -9), - expectedString: 'P0M0DT27.999999991S' - }, - { - duration: duration(0, 0, -28, -9), - expectedString: 'P0M0DT-28.000000009S' - }, - - { - duration: duration(0, 0, -78036, -143000000), - expectedString: 'P0M0DT-78036.143000000S' - }, - - { duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S' }, - { - duration: duration(0, 0, 0, -1000000000), - expectedString: 'P0M0DT-1S' - }, - { - duration: duration(0, 0, 0, 1000000007), - expectedString: 'P0M0DT1.000000007S' - }, - { - duration: duration(0, 0, 0, -1000000007), - expectedString: 'P0M0DT-1.000000007S' - }, - - { - duration: duration(0, 0, 40, 2123456789), - expectedString: 'P0M0DT42.123456789S' - }, - { - duration: duration(0, 0, -40, 2123456789), - expectedString: 'P0M0DT-37.876543211S' - }, - { - duration: duration(0, 0, 40, -2123456789), - expectedString: 'P0M0DT37.876543211S' - }, - { - duration: duration(0, 0, -40, -2123456789), - expectedString: 'P0M0DT-42.123456789S' - } - ], - done - ) + await testDurationToString([ + { duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S' }, + + { duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S' }, + { duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S' }, + { duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S' }, + { duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S' }, + + { + duration: duration(0, 0, 0, 5), + expectedString: 'P0M0DT0.000000005S' + }, + { + duration: duration(0, 0, 0, -5), + expectedString: 'P0M0DT-0.000000005S' + }, + { + duration: duration(0, 0, 0, 999999999), + expectedString: 'P0M0DT0.999999999S' + }, + { + duration: duration(0, 0, 0, -999999999), + expectedString: 'P0M0DT-0.999999999S' + }, + + { + duration: duration(0, 0, 1, 5), + expectedString: 'P0M0DT1.000000005S' + }, + { + duration: duration(0, 0, -1, -5), + expectedString: 'P0M0DT-1.000000005S' + }, + { + duration: duration(0, 0, 1, -5), + expectedString: 'P0M0DT0.999999995S' + }, + { + duration: duration(0, 0, -1, 5), + expectedString: 'P0M0DT-0.999999995S' + }, + { + duration: duration(0, 0, 1, 999999999), + expectedString: 'P0M0DT1.999999999S' + }, + { + duration: duration(0, 0, -1, -999999999), + expectedString: 'P0M0DT-1.999999999S' + }, + { + duration: duration(0, 0, 1, -999999999), + expectedString: 'P0M0DT0.000000001S' + }, + { + duration: duration(0, 0, -1, 999999999), + expectedString: 'P0M0DT-0.000000001S' + }, + + { + duration: duration(0, 0, 28, 9), + expectedString: 'P0M0DT28.000000009S' + }, + { + duration: duration(0, 0, -28, 9), + expectedString: 'P0M0DT-27.999999991S' + }, + { + duration: duration(0, 0, 28, -9), + expectedString: 'P0M0DT27.999999991S' + }, + { + duration: duration(0, 0, -28, -9), + expectedString: 'P0M0DT-28.000000009S' + }, + + { + duration: duration(0, 0, -78036, -143000000), + expectedString: 'P0M0DT-78036.143000000S' + }, + + { duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S' }, + { + duration: duration(0, 0, 0, -1000000000), + expectedString: 'P0M0DT-1S' + }, + { + duration: duration(0, 0, 0, 1000000007), + expectedString: 'P0M0DT1.000000007S' + }, + { + duration: duration(0, 0, 0, -1000000007), + expectedString: 'P0M0DT-1.000000007S' + }, + + { + duration: duration(0, 0, 40, 2123456789), + expectedString: 'P0M0DT42.123456789S' + }, + { + duration: duration(0, 0, -40, 2123456789), + expectedString: 'P0M0DT-37.876543211S' + }, + { + duration: duration(0, 0, 40, -2123456789), + expectedString: 'P0M0DT37.876543211S' + }, + { + duration: duration(0, 0, -40, -2123456789), + expectedString: 'P0M0DT-42.123456789S' + } + ]) }) it('should normalize created duration', () => { @@ -1284,8 +1261,8 @@ describe('#integration temporal-types', () => { ) }) - it('should send and receive neo4j Date created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j Date created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1294,11 +1271,11 @@ describe('#integration temporal-types', () => { const standardDate = new Date(2000, 0, 1) const neo4jDate = neo4j.types.Date.fromStandardDate(standardDate) - testSendReceiveTemporalValue(neo4jDate, done) + await testSendReceiveTemporalValue(neo4jDate) }) - it('should send and receive neo4j LocalDateTime created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j LocalDateTime created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1309,11 +1286,11 @@ describe('#integration temporal-types', () => { const neo4jLocalDateTime = neo4j.types.LocalDateTime.fromStandardDate( standardDate ) - testSendReceiveTemporalValue(neo4jLocalDateTime, done) + await testSendReceiveTemporalValue(neo4jLocalDateTime) }) - it('should send and receive neo4j DateTime created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j DateTime created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1322,7 +1299,7 @@ describe('#integration temporal-types', () => { const standardDate = new Date(1756, 0, 29, 23, 15, 59, 12) const neo4jDateTime = neo4j.types.DateTime.fromStandardDate(standardDate) - testSendReceiveTemporalValue(neo4jDateTime, done) + await testSendReceiveTemporalValue(neo4jDateTime) }) it('should fail to create LocalTime with out of range values', () => { @@ -1440,91 +1417,84 @@ describe('#integration temporal-types', () => { verifyTimeZoneOffset(neo4jDateTime5, -1 * 150 * 60, '-02:30') }) - function testSendAndReceiveRandomTemporalValues (valueGenerator, done) { + function testSendAndReceiveRandomTemporalValues (valueGenerator) { const asyncFunction = (index, callback) => { - const next = () => callback() - next.fail = error => callback(error) - testSendReceiveTemporalValue(valueGenerator(), next) - } - - const doneFunction = error => { - if (error) { - done.fail(error) - } else { - done() - } + testSendReceiveTemporalValue(valueGenerator()) + .then(() => callback()) + .catch(error => callback(error)) } - timesSeries(RANDOM_VALUES_TO_TEST, asyncFunction, doneFunction) + return new Promise((resolve, reject) => { + timesSeries(RANDOM_VALUES_TO_TEST, asyncFunction, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result) + } + }) + }) } - function testSendAndReceiveArrayOfRandomTemporalValues (valueGenerator, done) { + async function testSendAndReceiveArrayOfRandomTemporalValues (valueGenerator) { const arrayLength = _.random( MIN_TEMPORAL_ARRAY_LENGTH, MAX_TEMPORAL_ARRAY_LENGTH ) const values = _.range(arrayLength).map(() => valueGenerator()) - testSendReceiveTemporalValue(values, done) + + await testSendReceiveTemporalValue(values) } - function testReceiveTemporalValue (query, expectedValue, done) { - session - .run(query) - .then(result => { - const records = result.records - expect(records.length).toEqual(1) + async function testReceiveTemporalValue (query, expectedValue) { + try { + const result = await session.run(query) - const value = records[0].get(0) - expect(value).toEqual(expectedValue) + const records = result.records + expect(records.length).toEqual(1) - session.close() - done() - }) - .catch(error => { - done.fail(error) - }) + const value = records[0].get(0) + expect(value).toEqual(expectedValue) + } finally { + await session.close() + } } - function testSendReceiveTemporalValue (value, done) { - session - .run('CREATE (n:Node {value: $value}) RETURN n.value', { value: value }) - .then(result => { - const records = result.records - expect(records.length).toEqual(1) + async function testSendReceiveTemporalValue (value) { + const result = await session.run( + 'CREATE (n:Node {value: $value}) RETURN n.value', + { value: value } + ) - const receivedValue = records[0].get(0) - expect(receivedValue).toEqual(value) + const records = result.records + expect(records.length).toEqual(1) - session.close() - done() - }) - .catch(error => { - done.fail(error) - }) + const receivedValue = records[0].get(0) + expect(receivedValue).toEqual(value) + + await session.close() } - function testDurationToString (values, done) { + async function testDurationToString (values) { const durations = values.map(value => value.duration) const expectedDurationStrings = values.map(value => value.expectedString) - session - .run('UNWIND $durations AS d RETURN d', { durations: durations }) - .then(result => { - const receivedDurationStrings = result.records - .map(record => record.get(0)) - .map(duration => duration.toString()) - - expect(expectedDurationStrings).toEqual(receivedDurationStrings) - done() - }) - .catch(error => { - done.fail(error) + try { + const result = await session.run('UNWIND $durations AS d RETURN d', { + durations: durations }) + + const receivedDurationStrings = result.records + .map(record => record.get(0)) + .map(duration => duration.toString()) + + expect(expectedDurationStrings).toEqual(receivedDurationStrings) + } finally { + await session.close() + } } - function neo4jDoesNotSupportTemporalTypes (done) { + function neo4jDoesNotSupportTemporalTypes () { if (serverVersion.compareTo(VERSION_3_4_0) < 0) { - done() return true } return false diff --git a/test/transaction.test.js b/test/transaction.test.js index 49d4e4649..7c8ea5042 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -382,10 +382,7 @@ describe('#integration transaction', () => { const tx2 = session2.beginTransaction() tx2.run('CREATE ()').catch(error => { const message = error.message - const expectedPrefix = - message.indexOf('Database not up to the requested version') === - 0 - expect(expectedPrefix).toBeTruthy() + expect(message).toContain('not up to the requested version') done() }) }) diff --git a/test/types/driver.test.ts b/test/types/driver.test.ts index cf1e98d55..aa577282d 100644 --- a/test/types/driver.test.ts +++ b/test/types/driver.test.ts @@ -30,6 +30,9 @@ import { Parameters } from '../../types/statement-runner' import Session from '../../types/session' import { Neo4jError } from '../../types/error' import { ServerInfo } from '../../types/result-summary' +import RxSession from '../../types/session-rx' +import { concat, map, catchError } from 'rxjs/operators' +import { throwError } from 'rxjs' const dummy: any = null @@ -88,12 +91,14 @@ const session7: Session = driver.session({ bookmarks: 'bookmark2' }) -session1.run('RETURN 1').then(result => { - session1.close() - result.records.forEach(record => { - console.log(record) +session1 + .run('RETURN 1') + .then(result => { + result.records.forEach(record => { + console.log(record) + }) }) -}) + .then(() => session1.close()) const close: void = driver.close() @@ -101,3 +106,39 @@ driver.verifyConnectivity().then((serverInfo: ServerInfo) => { console.log(serverInfo.version) console.log(serverInfo.address) }) + +const rxSession1: RxSession = driver.rxSession() +const rxSession2: RxSession = driver.rxSession({ defaultAccessMode: READ }) +const rxSession3: RxSession = driver.rxSession({ defaultAccessMode: 'READ' }) +const rxSession4: RxSession = driver.rxSession({ defaultAccessMode: WRITE }) +const rxSession5: RxSession = driver.rxSession({ defaultAccessMode: 'WRITE' }) +const rxSession6: RxSession = driver.rxSession({ + defaultAccessMode: READ, + bookmarks: 'bookmark1' +}) +const rxSession7: RxSession = driver.rxSession({ + defaultAccessMode: READ, + bookmarks: ['bookmark1', 'bookmark2'] +}) +const rxSession8: RxSession = driver.rxSession({ + defaultAccessMode: WRITE, + bookmarks: 'bookmark1' +}) +const rxSession9: RxSession = driver.rxSession({ + defaultAccessMode: WRITE, + bookmarks: ['bookmark1', 'bookmark2'] +}) + +rxSession1 + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0)), + concat(rxSession1.close()), + catchError(err => rxSession1.close().pipe(concat(throwError(err)))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) diff --git a/test/types/result-rx.test.ts b/test/types/result-rx.test.ts new file mode 100644 index 000000000..8dc7f53f7 --- /dev/null +++ b/test/types/result-rx.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 RxResult from '../../types/result-rx' +import Record from '../../types/record' +import ResultSummary from '../../types/result-summary' +import { Observer } from 'rxjs' + +const dummy: any = null + +const res: RxResult = dummy + +res.keys().subscribe({ + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +}) + +res.records().subscribe({ + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +}) + +res.summary().subscribe({ + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +}) diff --git a/test/types/result.test.ts b/test/types/result.test.ts index 0f4a46005..4ba47f443 100644 --- a/test/types/result.test.ts +++ b/test/types/result.test.ts @@ -50,3 +50,10 @@ res.subscribe({ onError: (error: Error) => console.log(error), onCompleted: (summary: ResultSummary) => console.log(summary) }) + +res.subscribe({ + onKeys: (keys: string[]) => console.log(keys), + onNext: (record: Record) => console.log(record), + onError: (error: Error) => console.log(error), + onCompleted: (summary: ResultSummary) => console.log(summary) +}) diff --git a/test/types/session-rx.test.ts b/test/types/session-rx.test.ts new file mode 100644 index 000000000..be77b64a1 --- /dev/null +++ b/test/types/session-rx.test.ts @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 RxSession from '../../types/session-rx' +import { TransactionConfig } from '../../types/session' +import RxTransaction from '../../types/transaction-rx' +import Record from '../../types/record' +import RxResult from '../../types/result-rx' +import ResultSummary from '../../types/result-summary' +import Integer from '../../types/integer' +import { Observable, of, Observer, throwError } from 'rxjs' +import { concat, finalize, catchError } from 'rxjs/operators' + +const dummy: any = null +const intValue: Integer = Integer.fromInt(42) + +const keysObserver: Observer = { + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +} + +const recordsObserver: Observer = { + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +} + +const summaryObserver: Observer = { + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +} + +const rxSession: RxSession = dummy + +const txConfig1: TransactionConfig = {} +const txConfig2: TransactionConfig = { timeout: 5000 } +const txConfig3: TransactionConfig = { timeout: intValue } +const txConfig4: TransactionConfig = { metadata: {} } +const txConfig5: TransactionConfig = { + metadata: { + key1: 'value1', + key2: 5, + key3: { a: 'a', b: 'b' }, + key4: [1, 2, 3] + } +} +const txConfig6: TransactionConfig = { + timeout: 2000, + metadata: { key1: 'value1', key2: 2 } +} +const txConfig7: TransactionConfig = { + timeout: intValue, + metadata: { key1: 'value1', key2: 2 } +} + +const tx1: Observable = rxSession.beginTransaction() +const bookmark: null | string = rxSession.lastBookmark() + +const observable1: Observable = rxSession.readTransaction( + (tx: RxTransaction) => { + return of(10) + } +) + +const observable2: Observable = rxSession.readTransaction( + (tx: RxTransaction) => { + return of('42') + } +) + +const observable3: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => { + return of(10) + } +) + +const observable4: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => { + return of('42') + } +) + +const close1: Observable = rxSession.close() +const close2: Observable = rxSession + .close() + .pipe(finalize(() => 'session closed')) + +const result1: RxResult = rxSession.run('RETURN 1') +result1.keys().subscribe(keysObserver) +result1.records().subscribe(recordsObserver) +result1 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const result2: RxResult = rxSession.run('RETURN $value', { value: '42' }) +result2.keys().subscribe(keysObserver) +result2.records().subscribe(recordsObserver) +result2 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const result3: RxResult = rxSession.run( + 'RETURN $value', + { value: '42' }, + txConfig1 +) +result3.keys().subscribe(keysObserver) +result3.records().subscribe(recordsObserver) +result3 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const tx2: Observable = rxSession.beginTransaction(txConfig2) +const observable5: Observable = rxSession.readTransaction( + (tx: RxTransaction) => of(''), + txConfig3 +) +const observable6: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => of(42), + txConfig4 +) diff --git a/test/types/session.test.ts b/test/types/session.test.ts index 79d9dea98..162246e91 100644 --- a/test/types/session.test.ts +++ b/test/types/session.test.ts @@ -73,8 +73,8 @@ const promise4: Promise = session.writeTransaction( } ) -const close1: void = session.close() -const close2: void = session.close(() => { +const close1: Promise = session.close() +const close2: Promise = session.close().then(() => { console.log('Session closed') }) diff --git a/test/types/transaction-rx.test.ts b/test/types/transaction-rx.test.ts new file mode 100644 index 000000000..bacf7a830 --- /dev/null +++ b/test/types/transaction-rx.test.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 RxTransaction from '../../types/transaction-rx' +import Record from '../../types/record' +import RxResult from '../../types/result-rx' +import ResultSummary from '../../types/result-summary' +import { Observable, of, Observer, throwError } from 'rxjs' +import { concat, finalize, catchError } from 'rxjs/operators' + +const dummy: any = null + +const stringObserver: Observer = { + next: value => console.log(value), + complete: () => console.log('complete'), + error: error => console.log(`error: ${error}`) +} + +const keysObserver: Observer = { + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +} + +const recordsObserver: Observer = { + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +} + +const summaryObserver: Observer = { + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +} + +const tx: RxTransaction = dummy + +const result1: RxResult = tx.run('RETURN 1') +result1.keys().subscribe(keysObserver) +result1.records().subscribe(recordsObserver) +result1.summary().subscribe(summaryObserver) + +const result2: RxResult = tx.run('RETURN $value', { value: '42' }) +result2.keys().subscribe(keysObserver) +result2.records().subscribe(recordsObserver) +result2.summary().subscribe(summaryObserver) + +tx.commit() + .pipe(concat(of('committed'))) + .subscribe(stringObserver) + +tx.rollback() + .pipe(concat(of('rolled back'))) + .subscribe(stringObserver) diff --git a/tsconfig.json b/tsconfig.json index 4881be175..b1ed827a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "module": "es6", "target": "es6", "noImplicitAny": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "moduleResolution": "node", + "types": [] } } diff --git a/types/driver.d.ts b/types/driver.d.ts index d087753be..2701fb16d 100644 --- a/types/driver.d.ts +++ b/types/driver.d.ts @@ -18,6 +18,7 @@ */ import Session from './session' +import RxSession from './session-rx' import { Parameters } from './statement-runner' import { Neo4jError } from './error' import { ServerInfo } from './result-summary' @@ -72,6 +73,16 @@ declare interface Driver { database?: string }): Session + rxSession({ + defaultAccessMode, + bookmarks, + database + }?: { + defaultAccessMode?: SessionMode + bookmarks?: string | string[] + database?: string + }): RxSession + close(): void verifyConnectivity(): Promise diff --git a/types/index.d.ts b/types/index.d.ts index 5062de5ab..568e149bc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -46,7 +46,7 @@ import { SERVICE_UNAVAILABLE, SESSION_EXPIRED } from './error' -import Result, { Observer, StatementResult } from './result' +import Result, { ResultObserver, StatementResult } from './result' import ResultSummary, { Notification, NotificationPosition, @@ -174,7 +174,7 @@ declare const forExport: { Record: Record Result: Result StatementResult: StatementResult - Observer: Observer + ResultObserver: ResultObserver ResultSummary: ResultSummary Plan: Plan ProfiledPlan: ProfiledPlan @@ -227,7 +227,7 @@ export { Record, Result, StatementResult, - Observer, + ResultObserver, ResultSummary, Plan, ProfiledPlan, diff --git a/types/result-rx.d.ts b/types/result-rx.d.ts new file mode 100644 index 000000000..7ca0baa18 --- /dev/null +++ b/types/result-rx.d.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { Observable } from 'rxjs' +import ResultSummary from './result-summary' +import Record from './record' + +declare interface RxResult { + keys(): Observable + + records(): Observable + + summary(): Observable +} + +export default RxResult diff --git a/types/result.d.ts b/types/result.d.ts index 8a2d2bc9a..022f38a18 100644 --- a/types/result.d.ts +++ b/types/result.d.ts @@ -25,15 +25,16 @@ declare type StatementResult = { summary: ResultSummary } -declare type Observer = { +declare type ResultObserver = { + onKeys?(keys: string[]): void onNext?(record: Record): void onCompleted?(summary: ResultSummary): void onError?(error: Error): void } declare interface Result extends Promise { - subscribe(observer: Observer): void + subscribe(observer: ResultObserver): void } -export { StatementResult, Observer } +export { StatementResult, ResultObserver } export default Result diff --git a/types/session-rx.d.ts b/types/session-rx.d.ts new file mode 100644 index 000000000..ec6d4ee6a --- /dev/null +++ b/types/session-rx.d.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 RxResult from './result-rx' +import RxTransaction from './transaction-rx' +import { TransactionConfig } from './session' +import { Parameters } from './statement-runner' +import { Observable } from 'rxjs' + +declare type RxTransactionWork = (tx: RxTransaction) => Observable + +declare interface RxSession { + run( + statement: string, + parameters?: Parameters, + config?: TransactionConfig + ): RxResult + + beginTransaction(config?: TransactionConfig): Observable + + lastBookmark(): string | null + + readTransaction( + work: RxTransactionWork, + config?: TransactionConfig + ): Observable + + writeTransaction( + work: RxTransactionWork, + config?: TransactionConfig + ): Observable + + close(): Observable +} + +export default RxSession diff --git a/types/session.d.ts b/types/session.d.ts index 457329869..14b3d442e 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -50,7 +50,7 @@ declare interface Session extends StatementRunner { config?: TransactionConfig ): Promise - close(callback?: () => void): void + close(): Promise } export { TransactionConfig } diff --git a/types/transaction-rx.d.ts b/types/transaction-rx.d.ts new file mode 100644 index 000000000..a7719224a --- /dev/null +++ b/types/transaction-rx.d.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 { Observable } from 'rxjs' +import { Parameters } from './statement-runner' +import RxResult from './result-rx' + +declare interface RxTransaction { + run(statement: string, parameters?: Parameters): RxResult + + commit(): Observable + + rollback(): Observable +} + +export default RxTransaction