From 293a05e6adc533824444d6b5184ba415d2402f33 Mon Sep 17 00:00:00 2001 From: Jacob Hansson Date: Thu, 10 Dec 2015 16:11:03 +0100 Subject: [PATCH 1/2] Basic test for transactions --- src/v1/internal/connector.js | 1 + src/v1/result.js | 4 +-- src/v1/session.js | 9 ++++-- src/v1/transaction.js | 36 ++++++++++++++++++++++++ test/v1/transaction.test.js | 53 ++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/v1/transaction.js create mode 100644 test/v1/transaction.test.js diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index a4e061938..b4190e628 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -219,6 +219,7 @@ class Connection { } _handleMessage( msg ) { + switch( msg.signature ) { case RECORD: this._currentObserver.onNext( msg.fields[0] ); diff --git a/src/v1/result.js b/src/v1/result.js index 46e51e62e..156b4131d 100644 --- a/src/v1/result.js +++ b/src/v1/result.js @@ -113,6 +113,4 @@ class Result { } } -export default { - Result -} +export default Result diff --git a/src/v1/session.js b/src/v1/session.js index c5d4fbc7c..47479aa35 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -18,7 +18,8 @@ */ import StreamObserver from './internal/stream-observer'; -import {Result} from './result'; +import Result from './result'; +import Transaction from './transaction'; /** * A Session instance is used for handling the connection and @@ -40,7 +41,7 @@ class Session { /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} - * or with the statement and parameters as separate arguments. + * or with the statem ent and parameters as separate arguments. * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement * @return {Result} - New Result @@ -57,6 +58,10 @@ class Session { return new Result( streamObserver, statement, parameters ); } + beginTransaction() { + return new Transaction(this); + } + /** * Close connection * @param {function()} cb - Function to be called on connection close diff --git a/src/v1/transaction.js b/src/v1/transaction.js new file mode 100644 index 000000000..5746e2f66 --- /dev/null +++ b/src/v1/transaction.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2002-2015 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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. + */ + +class Transaction { + constructor( session ) { + this._session = session; + this._session.run("BEGIN"); + } + + run(statement, parameters) { + return this._session.run(statement, parameters) + } + + commit() { + return this._session.run("COMMIT"); + } + +} + +export default Transaction; diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js new file mode 100644 index 000000000..91e4ce8a8 --- /dev/null +++ b/test/v1/transaction.test.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2002-2015 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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. + */ + +var neo4j = require("../../lib/v1"); +var StatementType = require("../../lib/v1/result-summary").statementType; + +fdescribe('transaction', function() { + + var driver, session; + + beforeEach(function(done) { + driver = neo4j.driver("bolt://localhost"); + session = driver.session(); + + session.run("MATCH (n) DETACH DELETE n").then(done); + }); + + it('should handle simple transaction', function(done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.run("CREATE (:TXNode2)"); + tx.commit() + .then(function() { + session.run("MATCH (t1:TXNode1), (t2:TXNode2) RETURN count(t1), count(t2)").then(function(records) { + + expect( records.length ).toBe( 1 ); + expect( records[0]['count(t1)'].toInt() ) + .toBe( 1 ); + expect( records[0]['count(t2)'].toInt() ) + .toBe( 1 ); + done(); + }); + }); + }); + +}); From cdedd344750237154e118b8d32a4ca6179c7d76d Mon Sep 17 00:00:00 2001 From: Pontus Melke Date: Mon, 14 Dec 2015 13:44:08 +0100 Subject: [PATCH 2/2] Transaction support in js driver. --- README.md | 41 +++++-- src/v1/internal/connector.js | 17 ++- src/v1/internal/stream-observer.js | 6 +- src/v1/result.js | 6 +- src/v1/session.js | 34 ++++-- src/v1/transaction.js | 182 ++++++++++++++++++++++++++++- test/v1/session.test.js | 27 +++++ test/v1/transaction.test.js | 182 +++++++++++++++++++++++++++-- 8 files changed, 455 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index bd700cb00..5799100f6 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ session .subscribe({ onNext: function(record) { console.log(record); - }, + }, onCompleted: function() { // Completed! session.close(); - }, + }, onError: function(error) { console.log(error); } @@ -72,6 +72,31 @@ session .catch(function(error) { console.log(error); }); + + //run statement in a transaction + var tx = session.beginTransaction(); + tx.run("CREATE (alice {name : {nameParam} })", { nameParam:'Alice'}"); + tx.run("MATCH (alice {name : {nameParam} }) RETURN alice.age", { nameParam:'Alice'}"); + //decide if the transaction should be committed or rolled back + var success = ... + ... + if (success) { + tx.commit() + .subscribe({ + onCompleted: function() { + // Completed! + session.close(); + }, + onError: function(error) { + console.log(error); + } + }); + } else { + //transaction is rolled black nothing is created in the database + tx.rollback(); + } + + ``` ## Building @@ -89,25 +114,25 @@ See files under `examples/` on how to use. This runs the test suite against a fresh download of Neo4j. Or `npm test` 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 +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. +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`. ## A note on 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)`. +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 includes an explicit Integer types. Any time the driver recieves an Integer value from Neo4j, it will be represented with the Integer type by the driver. ### Write integers -Number written directly e.g. `session.run("CREATE (n:Node {age: {age}})", {age: 22})` will be of type `Float` in Neo4j. +Number 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 diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index b4190e628..938025d80 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -190,7 +190,7 @@ class Connection { // this to the dechunker self._ch.onmessage = (buf) => { self._dechunker.write(buf); - } + }; if( buf.hasRemaining() ) { self._dechunker.write(buf.readSlice( buf.remaining() )); @@ -219,7 +219,7 @@ class Connection { } _handleMessage( msg ) { - + switch( msg.signature ) { case RECORD: this._currentObserver.onNext( msg.fields[0] ); @@ -234,6 +234,7 @@ class Connection { case FAILURE: try { this._currentObserver.onError( msg ); + this._errorMsg = msg; } finally { this._currentObserver = this._pendingObservers.shift(); // Things are now broken. Pending observers will get FAILURE messages routed until @@ -257,6 +258,18 @@ class Connection { } } break; + case IGNORED: + try { + if (this._errorMsg) + this._currentObserver.onError(this._errorMsg); + else + this._currentObserver.onError(msg); + } finally { + this._currentObserver = this._pendingObservers.shift(); + } + break; + default: + console.log("UNKNOWN MESSAGE: ", msg); } } diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index 0147a3e3e..34d4c771c 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -16,8 +16,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/** + +/** * 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. @@ -106,7 +106,7 @@ class StreamObserver { if( this._queuedRecords.length > 0 ) { for (var i = 0; i < _queuedRecords.length; i++) { observer.onNext( _queuedRecords[i] ); - }; + } } if( this._tail ) { observer.onCompleted( this._tail ); diff --git a/src/v1/result.js b/src/v1/result.js index 156b4131d..162bb9067 100644 --- a/src/v1/result.js +++ b/src/v1/result.js @@ -38,7 +38,7 @@ class Result { this._p = null; this._statement = statement; this._parameters = parameters; - this.summary = {} + this.summary = {}; } /** @@ -56,7 +56,7 @@ class Result { onNext: (record) => { records.push(record); }, onCompleted: () => { resolve(records); }, onError: (error) => { reject(error); } - } + }; self.subscribe(observer); }); } @@ -99,7 +99,7 @@ class Result { let onCompletedWrapper = (metadata) => { this.summary = new ResultSummary(this._statement, this._parameters, metadata); onCompletedOriginal.call(observer); - } + }; observer.onCompleted = onCompletedWrapper; this._streamObserver.subscribe(observer); } diff --git a/src/v1/session.js b/src/v1/session.js index 47479aa35..ba8adb7f9 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -16,7 +16,7 @@ * 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 Transaction from './transaction'; @@ -36,12 +36,13 @@ class Session { constructor( conn, onClose ) { this._conn = conn; this._onClose = onClose; + this._hasTx = false; } /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} - * or with the statem ent and parameters as separate arguments. + * or with the statement and parameters as separate arguments. * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement * @return {Result} - New Result @@ -52,16 +53,34 @@ class Session { statement = statement.text; } let streamObserver = new StreamObserver(); - this._conn.run( statement, parameters || {}, streamObserver ); - this._conn.pullAll( streamObserver ); - this._conn.sync(); + if (!this._hasTx) { + this._conn.run(statement, parameters || {}, streamObserver); + this._conn.pullAll(streamObserver); + this._conn.sync(); + } else { + streamObserver.onError({error: "Please close the currently open transaction object before running " + + "more statements/transactions in the current session." }); + } return new Result( streamObserver, statement, parameters ); } + /** + * Begin a new transaction in this session. A session can have at most one transaction running at a time, if you + * want to run multiple concurrent transactions, you should use multiple concurrent sessions. + * + * While a transaction is open the session cannot be used to run statements. + * + * @returns {Transaction} - New Transaction + */ beginTransaction() { - return new Transaction(this); + if (this._hasTx) { + throw new Error("Cannot have multiple transactions open for the session. Use multiple sessions or close the transaction before opening a new one.") + } + + this._hasTx = true; + return new Transaction(this._conn, () => {this._hasTx = false}); } - + /** * Close connection * @param {function()} cb - Function to be called on connection close @@ -72,4 +91,5 @@ class Session { this._conn.close(cb); } } + export default Session; diff --git a/src/v1/transaction.js b/src/v1/transaction.js index 5746e2f66..7f2d574ac 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -16,21 +16,191 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import StreamObserver from './internal/stream-observer'; +import Result from './result'; +/** + * Represents a transaction in the Neo4j database. + * + * @access public + */ class Transaction { - constructor( session ) { - this._session = session; - this._session.run("BEGIN"); + /** + * @constructor + * @param {Connection} conn - A connection to use + * @param {function()} onClose - Function to be called when transaction is committed or rolled back. + */ + constructor(conn, onClose) { + this._conn = conn; + let streamObserver = new _TransactionStreamObserver(this); + this._conn.run("BEGIN", {}, streamObserver); + this._conn.discardAll(streamObserver); + this._state = _states.ACTIVE; + this._onClose = onClose; } + /** + * Run Cypher statement + * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} + * or with the statem ent and parameters as separate arguments. + * @param {mixed} statement - Cypher statement to execute + * @param {Object} parameters - Map with parameters to use in statement + * @return {Result} - New Result + */ run(statement, parameters) { - return this._session.run(statement, parameters) + if(typeof statement === 'object' && statement.text) { + parameters = statement.parameters || {}; + statement = statement.text; + } + return this._state.run(this._conn, new _TransactionStreamObserver(this), statement, parameters); } + /** + * Commits the transaction and returns the result. + * + * After committing the transaction can no longer be used. + * + * @returns {Result} - New Result + */ commit() { - return this._session.run("COMMIT"); + let committed = this._state.commit(this._conn, new _TransactionStreamObserver(this)); + this._state = committed.state; + //clean up + this._onClose(); + return committed.result; + + } + + /** + * Rollbacks the transaction. + * + * After rolling back, the transaction can no longer be used. + * + * @returns {Result} - New Result + */ + rollback() { + let committed = this._state.rollback(this._conn, new _TransactionStreamObserver(this)); + this._state = committed.state; + //clean up + this._onClose(); + return committed.result; + } + + _onError() { + this._onClose(); + this._state = _states.FAILED; + } +} + +/** Internal stream observer used for transactional results*/ +class _TransactionStreamObserver extends StreamObserver { + constructor(tx) { + super(); + this._tx = tx; + //this is to to avoid multiple calls to onError caused by IGNORED + this._hasFailed = false; + } + + onError(error) { + if (!this._hasFailed) { + this._tx._onError(); + super.onError(error); + this._hasFailed = true; + } + } +} + +/** internal state machine of the transaction*/ +let _states = { + //The transaction is running with no explicit success or failure marked + ACTIVE: { + commit: (conn, observer) => { + return {result: _runDiscardAll("COMMIT", conn, observer), + state: _states.SUCCEEDED} + }, + rollback: (conn, observer) => { + return {result: _runDiscardAll("ROLLBACK", conn, observer), state: _states.ROLLED_BACK}; + }, + run: (conn, observer, statement, parameters) => { + conn.run( statement, parameters || {}, observer ); + conn.pullAll( observer ); + conn.sync(); + return new Result( observer, statement, parameters ); + } + }, + + //An error has occurred, transaction can no longer be used and no more messages will + // be sent for this transaction. + FAILED: { + commit: (conn, 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." + }); + return {result: new Result(observer, "COMMIT", {}), state: _states.FAILED}; + }, + rollback: (conn, observer) => { + observer.onError({error: + "Cannot rollback transaction, because previous statements in the " + + "transaction has failed and the transaction has already been rolled back."}); + return {result: new Result(observer, "ROLLBACK", {}), state: _states.FAILED}; + }, + run: (conn, 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 new Result(observer, statement, parameters); + } + }, + + //This transaction has successfully committed + SUCCEEDED: { + commit: (conn, 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." + }); + return {result: new Result(observer, "COMMIT", {}), state: _states.SUCCEEDED}; + }, + rollback: (conn, observer) => { + observer.onError({error: + "Cannot rollback transaction, because transaction has already been successfully closed."}); + return {result: new Result(observer, "ROLLBACK", {}), state: _states.SUCCEEDED}; + }, + run: (conn, observer, statement, parameters) => { + observer.onError({error: + "Cannot run statement, because transaction has already been successfully closed."}); + return new Result(observer, statement, parameters); + } + }, + + //This transaction has been rolled back + ROLLED_BACK: { + commit: (conn, observer) => { + observer.onError({ + error: "Cannot commit this transaction, because it has already been rolled back." + }); + return {result: new Result(observer, "COMMIT", {}), state: _states.ROLLED_BACK}; + }, + rollback: (conn, observer) => { + observer.onError({error: + "Cannot rollback transaction, because transaction has already been rolled back."}); + return {result: new Result(observer, "ROLLBACK", {}), state: _states.ROLLED_BACK}; + }, + run: (conn, observer, statement, parameters) => { + observer.onError({error: + "Cannot run statement, because transaction has already been rolled back."}); + return new Result(observer, statement, parameters); + } } - +}; + +function _runDiscardAll(msg, conn, observer) { + conn.run(msg, {}, observer ); + conn.discardAll(observer); + conn.sync(); + return new Result(observer, msg, {}); } export default Transaction; diff --git a/test/v1/session.test.js b/test/v1/session.test.js index 60e7e84ab..91d81e284 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -205,4 +205,31 @@ describe('session', function() { done(); }); }); + + it('should fail when using the session when having an open transaction', function (done) { + // Given + var session = neo4j.driver("bolt://localhost").session(); + + // When + session.beginTransaction(); + + //Then + session.run("RETURN 42") + .catch(function (error) { + expect(error.error).toBe("Please close the currently open transaction object before running " + + "more statements/transactions in the current session." ) + done(); + }) + }); + + it('should fail when opening multiple transactions', function () { + // Given + var session = neo4j.driver("bolt://localhost").session(); + + // When + session.beginTransaction(); + + // Then + expect(session.beginTransaction).toThrow(); + }); }); diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index 91e4ce8a8..a503905d9 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -18,9 +18,8 @@ */ var neo4j = require("../../lib/v1"); -var StatementType = require("../../lib/v1/result-summary").statementType; -fdescribe('transaction', function() { +describe('transaction', function() { var driver, session; @@ -31,23 +30,184 @@ fdescribe('transaction', function() { session.run("MATCH (n) DETACH DELETE n").then(done); }); - it('should handle simple transaction', function(done) { + it('should handle simple transaction', function(done) { // When var tx = session.beginTransaction(); tx.run("CREATE (:TXNode1)"); tx.run("CREATE (:TXNode2)"); tx.commit() - .then(function() { - session.run("MATCH (t1:TXNode1), (t2:TXNode2) RETURN count(t1), count(t2)").then(function(records) { - - expect( records.length ).toBe( 1 ); - expect( records[0]['count(t1)'].toInt() ) - .toBe( 1 ); - expect( records[0]['count(t2)'].toInt() ) - .toBe( 1 ); + .then(function () { + session.run("MATCH (t1:TXNode1), (t2:TXNode2) RETURN count(t1), count(t2)") + .then(function (records) { + expect(records.length).toBe(1); + expect(records[0]['count(t1)'].toInt()) + .toBe(1); + expect(records[0]['count(t2)'].toInt()) + .toBe(1); done(); }); }); }); + it('should handle interactive session', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("RETURN 'foo' AS res").then(function (records) { + tx.run("CREATE ({name: {param}})", {param: records[0]['res']}); + tx.commit() + .then(function () { + session.run("MATCH (a {name:'foo'}) RETURN count(a)") + .then(function (records) { + expect(records.length).toBe(1); + expect(records[0]['count(a)'].toInt()).toBe(1); + done(); + }); + }); + }); + }); + + it('should handle failures with subscribe', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("THIS IS NOT CYPHER") + .catch(function (error) { + expect(error.fields.length).toBe(1); + driver.close(); + done(); + }); + }); + + it('should handle failures with catch', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("THIS IS NOT CYPHER") + .subscribe({ + onError: function (error) { + expect(error.fields.length).toBe(1); + driver.close(); + done(); + } + }); + }); + + it('should handle failures on commit', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.run("THIS IS NOT CYPHER"); + tx.run("CREATE (:TXNode2)"); + + tx.commit() + .catch(function (error) { + expect(error.fields.length).toBe(1); + driver.close(); + done(); + }); + }); + + it('should fail when committing on a failed query', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.run("THIS IS NOT CYPHER") + .catch(function () { + tx.commit() + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); + }); + + it('should handle when committing when another statement fails', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)") + .then(function () { + tx.commit() + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); + tx.run("THIS IS NOT CYPHER"); + }); + + it('should handle rollbacks', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.run("CREATE (:TXNode2)"); + tx.rollback() + .then(function () { + session.run("MATCH (t1:TXNode1), (t2:TXNode2) RETURN count(t1), count(t2)").then(function (records) { + + expect(records.length).toBe(1); + expect(records[0]['count(t1)'].toInt()) + .toBe(0); + expect(records[0]['count(t2)'].toInt()) + .toBe(0); + done(); + }); + }); + }); + + it('should fail when committing on a rolled back query', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.rollback() + + tx.commit() + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); + + it('should fail when running on a rolled back transaction', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.rollback(); + + tx.run("RETURN 42") + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); + + it('should fail when running when a previous statement failed', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("THIS IS NOT CYPHER") + .catch(function () { + tx.run("RETURN 42") + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); + tx.rollback(); + }); + + it('should fail when trying to roll back a rolled back transaction', function (done) { + // When + var tx = session.beginTransaction(); + tx.run("CREATE (:TXNode1)"); + tx.rollback(); + + tx.rollback() + .catch(function (error) { + expect(error.error).toBeDefined(); + driver.close(); + done(); + }); + }); });