diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..26a660ba5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "stage-3"] +} diff --git a/gulpfile.js b/gulpfile.babel.js similarity index 92% rename from gulpfile.js rename to gulpfile.babel.js index be6530db4..9f4c20b16 100644 --- a/gulpfile.js +++ b/gulpfile.babel.js @@ -16,6 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +require("babel-polyfill"); var browserify = require('browserify'); var source = require('vinyl-source-stream'); @@ -52,7 +53,8 @@ gulp.task('browser', function(cb){ }); /** Build all-in-one files for use in the browser */ -gulp.task('build-browser', function () { +gulp.task('bu' + + 'ild-browser', function () { var browserOutput = 'lib/browser'; // Our app bundler var appBundler = browserify({ @@ -61,7 +63,7 @@ gulp.task('build-browser', function () { standalone: 'neo4j', packageCache: {} }).transform(babelify.configure({ - ignore: /external/ + presets: ['es2015', 'stage-3'], ignore: /external/ })).bundle(); // Un-minified browser package @@ -98,7 +100,7 @@ gulp.task('build-browser-test', function(){ cache: {}, debug: true }).transform(babelify.configure({ - ignore: /external/ + presets: ['es2015', 'stage-3'], ignore: /external/ })) .bundle(function(err, res){ cb(); @@ -115,7 +117,7 @@ gulp.task('build-browser-test', function(){ var buildNode = function(options) { return gulp.src(options.src) - .pipe(babel({ignore: ['src/external/**/*.js']})) + .pipe(babel({presets: ['es2015', 'stage-3'], ignore: ['src/external/**/*.js']})) .pipe(gulp.dest(options.dest)) }; @@ -152,6 +154,17 @@ gulp.task('test-nodejs', ['nodejs'], function () { })); }); +gulp.task('test-boltkit', ['nodejs'], function () { + return gulp.src('test/**/*.boltkit.it.js') + .pipe(jasmine({ + // reporter: new reporters.JUnitXmlReporter({ + // savePath: "build/nodejs-test-reports", + // consolidateAll: false + // }), + includeStackTrace: true + })); +}); + gulp.task('test-browser', function (cb) { runSequence('all', 'run-browser-test', cb) }); diff --git a/package.json b/package.json index 35ad5e3a8..42046b4f2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "scripts": { "test": "gulp test", + "boltkit": "gulp test-boltkit", "build": "gulp all", "start-neo4j": "gulp start-neo4j", "stop-neo4j": "gulp stop-neo4j", @@ -18,14 +19,17 @@ }, "main": "lib/index.js", "devDependencies": { - "babel": "^5.8.23", - "babelify": "^6.3.0", - "browserify": "^11.0.0", + "babel-core": "^6.17.0", + "babel-polyfill": "^6.16.0", + "babel-preset-es2015": "^6.16.0", + "babel-preset-stage-3": "^6.17.0", + "babelify": "^7.3.0", + "browserify": "^13.1.0", "esdoc": "^0.4.0", "esdoc-importpath-plugin": "0.0.1", "glob": "^5.0.14", "gulp": "^3.9.1", - "gulp-babel": "^5.2.1", + "gulp-babel": "^6.1.2", "gulp-batch": "^1.0.5", "gulp-concat": "^2.6.0", "gulp-cucumber": "0.0.14", diff --git a/runTests.sh b/runTests.sh index 6ec1f977b..41879af35 100755 --- a/runTests.sh +++ b/runTests.sh @@ -16,4 +16,4 @@ else fi sleep 2 -npm test +npm test \ No newline at end of file diff --git a/src/index.js b/src/index.js index ed5f3c016..118d4c1e6 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,5 @@ import * as v1 from './v1/index'; -export default { - v1: v1 -} +export { v1 } +export default v1 diff --git a/src/v1/driver.js b/src/v1/driver.js index f8e612ba8..bca4985ab 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -18,11 +18,14 @@ */ import Session from './session'; -import {Pool} from './internal/pool'; +import Pool from './internal/pool'; +import Integer from './integer'; import {connect} from "./internal/connector"; import StreamObserver from './internal/stream-observer'; -import {VERSION} from '../version'; +import {newError, SERVICE_UNAVAILABLE} from "./error"; +import "babel-polyfill"; +let READ = 'READ', WRITE = 'WRITE'; /** * A driver maintains one or more {@link Session sessions} with a remote * Neo4j instance. Through the {@link Session sessions} you can send statements @@ -53,7 +56,7 @@ class Driver { this._pool = new Pool( this._createConnection.bind(this), this._destroyConnection.bind(this), - this._validateConnection.bind(this), + Driver._validateConnection.bind(this), config.connectionPoolSize ); } @@ -63,13 +66,13 @@ class Driver { * @return {Connection} new connector-api session instance, a low level session API. * @access private */ - _createConnection( release ) { + _createConnection(url, release) { let sessionId = this._sessionIdGenerator++; let streamObserver = new _ConnectionStreamObserver(this); - let conn = connect(this._url, this._config); + let conn = connect(url, this._config); conn.initialize(this._userAgent, this._token, streamObserver); conn._id = sessionId; - conn._release = () => release(conn); + conn._release = () => release(this._url, conn); this._openSessions[sessionId] = conn; return conn; @@ -80,7 +83,7 @@ class Driver { * @return {boolean} true if the connection is open * @access private **/ - _validateConnection( conn ) { + static _validateConnection(conn) { return conn.isOpen(); } @@ -89,7 +92,7 @@ class Driver { * @return {Session} new session. * @access private */ - _destroyConnection( conn ) { + _destroyConnection(conn) { delete this._openSessions[conn._id]; conn.close(); } @@ -105,11 +108,19 @@ class Driver { * it is returned to the pool, the session will be reset to a clean state and * made available for others to use. * + * @param {String} mode of session - optional * @return {Session} new session. */ - session() { - let conn = this._pool.acquire(); - return new Session( conn, (cb) => { + session(mode) { + let connectionPromise = this._acquireConnection(mode); + connectionPromise.catch((err) => { + if (this.onError && err.code === SERVICE_UNAVAILABLE) { + this.onError(err); + } else { + //we don't need to tell the driver about this error + } + }); + return this._createSession(connectionPromise, (cb) => { // This gets called on Session#close(), and is where we return // the pooled 'connection' instance. @@ -119,17 +130,32 @@ class Driver { // Queue up a 'reset', to ensure the next user gets a clean // session to work with. - conn.reset(); - conn.sync(); - // Return connection to the pool - conn._release(); + connectionPromise.then( (conn) => { + conn.reset(); + conn.sync(); + + // Return connection to the pool + conn._release(); + }).catch( () => {/*ignore errors here*/}); // Call user callback - if(cb) { cb(); } + if (cb) { + cb(); + } }); } + //Extension point + _acquireConnection(mode) { + return Promise.resolve(this._pool.acquire(this._url)); + } + + //Extension point + _createSession(connectionPromise, cb) { + return new Session(connectionPromise, cb); + } + /** * Close all open sessions and other associated resources. You should * make sure to use this when you are done with this driver instance. @@ -140,6 +166,7 @@ class Driver { if (this._openSessions.hasOwnProperty(sessionId)) { this._openSessions[sessionId].close(); } + this._pool.purgeAll(); } } } @@ -151,83 +178,26 @@ class _ConnectionStreamObserver extends StreamObserver { this._driver = driver; this._hasFailed = false; } + onError(error) { if (!this._hasFailed) { super.onError(error); - if(this._driver.onError) { + if (this._driver.onError) { this._driver.onError(error); } this._hasFailed = true; } } + onCompleted(message) { - if(this._driver.onCompleted) { - this._driver.onCompleted(message); + if (this._driver.onCompleted) { + this._driver.onCompleted(message); } } } -let USER_AGENT = "neo4j-javascript/" + VERSION; -/** - * Construct a new Neo4j Driver. This is your main entry point for this - * library. - * - * ## Configuration - * - * This function optionally takes a configuration argument. Available configuration - * options are as follows: - * - * { - * // Encryption level: one of ENCRYPTION_ON, ENCRYPTION_OFF or ENCRYPTION_NON_LOCAL. - * // ENCRYPTION_NON_LOCAL is on by default in modern NodeJS installs, - * // but off by default in the Web Bundle and old (<=1.0.0) NodeJS installs - * // due to technical limitations on those platforms. - * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF|ENCRYPTION_NON_LOCAL - * - * // Trust strategy to use if encryption is enabled. There is no mode to disable - * // trust other than disabling encryption altogether. The reason for - * // this is that if you don't know who you are talking to, it is easy for an - * // attacker to hijack your encrypted connection, rendering encryption pointless. - * // - * // TRUST_ON_FIRST_USE is the default for modern NodeJS deployments, and works - * // similarly to how `ssl` works - the first time we connect to a new host, - * // we remember the certificate they use. If the certificate ever changes, we - * // assume it is an attempt to hijack the connection and require manual intervention. - * // This means that by default, connections "just work" while still giving you - * // good encrypted protection. - * // - * // TRUST_CUSTOM_CA_SIGNED_CERTIFICATES is the classic approach to trust verification - - * // whenever we establish an encrypted connection, we ensure the host is using - * // an encryption certificate that is in, or is signed by, a certificate listed - * // as trusted. In the web bundle, this list of trusted certificates is maintained - * // by the web browser. In NodeJS, you configure the list with the next config option. - * // - * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES meand that you trust whatever certificates - * // are in the default certificate chain of th - * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, - * - * // List of one or more paths to trusted encryption certificates. This only - * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". - * // The certificate files should be in regular X.509 PEM format. - * // For instance, ['./trusted.pem'] - * trustedCertificates: [], - * - * // Path to a file where the driver saves hosts it has seen in the past, this is - * // very similar to the ssl tool's known_hosts file. Each time we connect to a - * // new host, a hash of their certificate is stored along with the domain name and - * // port, and this is then used to verify the host certificate does not change. - * // This setting has no effect unless TRUST_ON_FIRST_USE is enabled. - * knownHosts:"~/.neo4j/known_hosts", - * } - * - * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. - * @param {Object} config Configuration object. See the configuration section above for details. - * @returns {Driver} - */ -function driver(url, authToken, config={}) { - return new Driver(url, USER_AGENT, authToken, config); -} -export {Driver, driver} +export {Driver, READ, WRITE} + +export default Driver diff --git a/src/v1/error.js b/src/v1/error.js index 4db0e7147..f2c1b17dc 100644 --- a/src/v1/error.js +++ b/src/v1/error.js @@ -20,8 +20,10 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. +let SERVICE_UNAVAILABLE = 'ServiceUnavailable'; +let SESSION_EXPIRED = 'SessionExpired'; function newError(message, code="N/A") { - // TODO: Idea is that we can check the cod here and throw sub-classes + // TODO: Idea is that we can check the code here and throw sub-classes // of Neo4jError as appropriate return new Neo4jError(message, code); } @@ -36,5 +38,7 @@ class Neo4jError extends Error { export { newError, - Neo4jError + Neo4jError, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED } diff --git a/src/v1/graph-types.js b/src/v1/graph-types.js index 3c7523a23..5acd9f71a 100644 --- a/src/v1/graph-types.js +++ b/src/v1/graph-types.js @@ -171,7 +171,7 @@ class Path { } } -export default { +export { Node, Relationship, UnboundRelationship, diff --git a/src/v1/index.js b/src/v1/index.js index bf02c05f3..5099263c9 100644 --- a/src/v1/index.js +++ b/src/v1/index.js @@ -18,43 +18,146 @@ */ import {int, isInt} from './integer'; -import {driver} from './driver'; import {Node, Relationship, UnboundRelationship, PathSegment, Path} from './graph-types' -import {Neo4jError} from './error'; +import {Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from './error'; import Result from './result'; import ResultSummary from './result-summary'; -import {Record} from './record'; +import Record from './record'; +import {Driver, READ, WRITE} from './driver'; +import RoutingDriver from './routing-driver'; +import VERSION from '../version'; +import {parseScheme, parseUrl} from "./internal/connector"; -export default { + +const auth ={ + basic: (username, password, realm = undefined) => { + if (realm) { + return {scheme: "basic", principal: username, credentials: password, realm: realm}; + } else { + return {scheme: "basic", principal: username, credentials: password}; + } + }, + custom: (principal, credentials, realm, scheme, parameters = undefined ) => { + if (parameters) { + return {scheme: scheme, principal: principal, credentials: credentials, realm: realm, + parameters: parameters} + } else { + return {scheme: scheme, principal: principal, credentials: credentials, realm: realm} + } + } +}; +let USER_AGENT = "neo4j-javascript/" + VERSION; + +/** + * Construct a new Neo4j Driver. This is your main entry point for this + * library. + * + * ## Configuration + * + * This function optionally takes a configuration argument. Available configuration + * options are as follows: + * + * { + * // Encryption level: one of ENCRYPTION_ON, ENCRYPTION_OFF or ENCRYPTION_NON_LOCAL. + * // ENCRYPTION_NON_LOCAL is on by default in modern NodeJS installs, + * // but off by default in the Web Bundle and old (<=1.0.0) NodeJS installs + * // due to technical limitations on those platforms. + * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF|ENCRYPTION_NON_LOCAL + * + * // Trust strategy to use if encryption is enabled. There is no mode to disable + * // trust other than disabling encryption altogether. The reason for + * // this is that if you don't know who you are talking to, it is easy for an + * // attacker to hijack your encrypted connection, rendering encryption pointless. + * // + * // TRUST_ON_FIRST_USE is the default for modern NodeJS deployments, and works + * // similarly to how `ssl` works - the first time we connect to a new host, + * // we remember the certificate they use. If the certificate ever changes, we + * // assume it is an attempt to hijack the connection and require manual intervention. + * // This means that by default, connections "just work" while still giving you + * // good encrypted protection. + * // + * // TRUST_CUSTOM_CA_SIGNED_CERTIFICATES is the classic approach to trust verification - + * // whenever we establish an encrypted connection, we ensure the host is using + * // an encryption certificate that is in, or is signed by, a certificate listed + * // as trusted. In the web bundle, this list of trusted certificates is maintained + * // by the web browser. In NodeJS, you configure the list with the next config option. + * // + * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES meand that you trust whatever certificates + * // are in the default certificate chain of th + * trust: "TRUST_ON_FIRST_USE" | "TRUST_SIGNED_CERTIFICATES" | TRUST_CUSTOM_CA_SIGNED_CERTIFICATES | + * TRUST_SYSTEM_CA_SIGNED_CERTIFICATES, + * + * // List of one or more paths to trusted encryption certificates. This only + * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". + * // The certificate files should be in regular X.509 PEM format. + * // For instance, ['./trusted.pem'] + * trustedCertificates: [], + * + * // Path to a file where the driver saves hosts it has seen in the past, this is + * // very similar to the ssl tool's known_hosts file. Each time we connect to a + * // new host, a hash of their certificate is stored along with the domain name and + * // port, and this is then used to verify the host certificate does not change. + * // This setting has no effect unless TRUST_ON_FIRST_USE is enabled. + * knownHosts:"~/.neo4j/known_hosts", + * } + * + * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" + * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Object} config Configuration object. See the configuration section above for details. + * @returns {Driver} + */ +function driver(url, authToken, config = {}) { + let scheme = parseScheme(url); + if (scheme === "bolt+routing://") { + return new RoutingDriver(parseUrl(url), USER_AGENT, authToken, config); + } else if (scheme === "bolt://") { + return new Driver(parseUrl(url), USER_AGENT, authToken, config); + } else { + throw new Error("Unknown scheme: " + scheme); + + } +} + + +const types ={ + Node, + Relationship, + UnboundRelationship, + PathSegment, + Path, + Result, + ResultSummary, + Record + }; + +const session = { + READ, + WRITE +}; +const error = { + SERVICE_UNAVAILABLE, + SESSION_EXPIRED +}; + +const forExport = { driver, int, isInt, Neo4jError, - auth: { - basic: (username, password, realm = undefined) => { - if (realm) { - return {scheme: "basic", principal: username, credentials: password, realm: realm}; - } else { - return {scheme: "basic", principal: username, credentials: password}; - } - }, - custom: (principal, credentials, realm, scheme, parameters = undefined ) => { - if (parameters) { - return {scheme: scheme, principal: principal, credentials: credentials, realm: realm, - parameters: parameters} - } else { - return {scheme: scheme, principal: principal, credentials: credentials, realm: realm} - } - } - }, - types: { - Node, - Relationship, - UnboundRelationship, - PathSegment, - Path, - Result, - ResultSummary, - Record - } + auth, + types, + session, + error +}; + +export { + driver, + int, + isInt, + Neo4jError, + auth, + types, + session, + error } +export default forExport diff --git a/src/v1/integer.js b/src/v1/integer.js index 70b7250a9..3a20d9e52 100644 --- a/src/v1/integer.js +++ b/src/v1/integer.js @@ -817,8 +817,9 @@ let int = Integer.fromValue; */ let isInt = Integer.isInteger; -export default { - Integer, +export { int, - isInt + isInt, } + +export default Integer diff --git a/src/v1/internal/buf.js b/src/v1/internal/buf.js index 95d93bf6e..d8800e125 100644 --- a/src/v1/internal/buf.js +++ b/src/v1/internal/buf.js @@ -579,7 +579,7 @@ function alloc (size) { return new _DefaultBuffer(size); } -export default { +export { BaseBuffer, HeapBuffer, SliceBuffer, diff --git a/src/v1/internal/ch-dummy.js b/src/v1/internal/ch-dummy.js index 6fb55c711..f08fe6a81 100644 --- a/src/v1/internal/ch-dummy.js +++ b/src/v1/internal/ch-dummy.js @@ -23,7 +23,7 @@ const observer = { updateInstance: (instance) => { observer.instance = instance } -} +}; class DummyChannel { constructor(opts) { @@ -48,7 +48,6 @@ class DummyChannel { } } -export default { - channel: DummyChannel, - observer: observer -} +const channel = DummyChannel; + +export { channel, observer } diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index 00b464501..69d0f5c99 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -24,7 +24,7 @@ import path from 'path'; import {EOL} from 'os'; import {NodeBuffer} from './buf'; import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF} from './util'; -import {newError} from './../error'; +import {newError, SESSION_EXPIRED} from './../error'; let _CONNECTION_IDGEN = 0; @@ -293,15 +293,18 @@ class NodeChannel { } _handleConnectionError( err ) { - this._error = err; + let msg = err.message || 'Failed to connect to server'; + this._error = newError(msg, SESSION_EXPIRED); if( this.onerror ) { - this.onerror(err); + this.onerror(this._error); } } _handleConnectionTerminated() { - this._open = false; - this._conn = undefined; + this._error = newError('Connection was closed by server', SESSION_EXPIRED); + if( this.onerror ) { + this.onerror(this._error); + } } isEncrypted() { diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index 277a70fc0..684ba48db 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import {debug} from "./log"; +import debug from "./log"; import {HeapBuffer} from "./buf"; import {newError} from './../error'; import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_ON, ENCRYPTION_OFF} from './util'; diff --git a/src/v1/internal/chunking.js b/src/v1/internal/chunking.js index 9d58b07c7..85141026d 100644 --- a/src/v1/internal/chunking.js +++ b/src/v1/internal/chunking.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import buf from './buf'; +import {alloc, BaseBuffer, CombinedBuffer} from './buf'; let _CHUNK_HEADER_SIZE = 2, @@ -28,12 +28,12 @@ let * Looks like a writable buffer, chunks output transparently into a channel below. * @access private */ -class Chunker extends buf.BaseBuffer { +class Chunker extends BaseBuffer { constructor(channel, bufferSize) { super(0); this._bufferSize = bufferSize || _DEFAULT_BUFFER_SIZE; this._ch = channel; - this._buffer = buf.alloc(this._bufferSize); + this._buffer = alloc(this._bufferSize); this._currentChunkStart = 0; this._chunkOpen = false; } @@ -80,7 +80,7 @@ class Chunker extends buf.BaseBuffer { this._ch.write(out.getSlice(0, out.position)); // Alloc a new output buffer. We assume we're using NodeJS's buffer pooling under the hood here! - this._buffer = buf.alloc(this._bufferSize); + this._buffer = alloc(this._bufferSize); this._chunkOpen = false; } return this; @@ -180,7 +180,7 @@ class Dechunker { if (this._currentMessage.length == 1) { message = this._currentMessage[0]; } else { - message = new buf.CombinedBuffer( this._currentMessage ); + message = new CombinedBuffer( this._currentMessage ); } this._currentMessage = []; this.onmessage(message); @@ -198,8 +198,7 @@ class Dechunker { } } - -export default { - Chunker: Chunker, - Dechunker: Dechunker +export { + Chunker, + Dechunker } \ No newline at end of file diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index 80db8ab26..fb9d1bc94 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -19,11 +19,11 @@ import WebSocketChannel from "./ch-websocket"; import NodeChannel from "./ch-node"; -import chunking from "./chunking"; +import {Dechunker, Chunker} from "./chunking"; import hasFeature from "./features"; -import packstream from "./packstream"; +import {Packer,Unpacker} from "./packstream"; import {alloc, CombinedBuffer} from "./buf"; -import GraphType from '../graph-types'; +import {Node, Relationship, UnboundRelationship, Path, PathSegment} from '../graph-types'; import {int, isInt} from '../integer'; import {newError} from './../error'; import {ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF, shouldEncrypt} from './util'; @@ -63,15 +63,28 @@ MAGIC_PREAMBLE = 0x6060B017, DEBUG = false; let URLREGEX = new RegExp([ - "[^/]+//", // scheme + "([^/]+//)?", // scheme "(([^:/?#]*)", // hostname "(?::([0-9]+))?)", // port (optional) ".*"].join("")); // everything else -function host( url ) { +function parseScheme( url ) { + let scheme = url.match(URLREGEX)[1] || ''; + return scheme.toLowerCase(); +} + +function parseUrl(url) { return url.match( URLREGEX )[2]; } +function parseHost( url ) { + return url.match( URLREGEX )[3]; +} + +function parsePort( url ) { + return url.match( URLREGEX )[4]; +} + /** * Very rudimentary log handling, should probably be replaced by something proper at some point. * @param actor the part that sent the message, 'S' for server and 'C' for client @@ -86,9 +99,6 @@ function log(actor, msg) { } } -function port( url ) { - return url.match( URLREGEX )[3]; -} function NO_OP(){} @@ -101,14 +111,14 @@ let NO_OP_OBSERVER = { /** Maps from packstream structures to Neo4j domain objects */ let _mappers = { node : ( unpacker, buf ) => { - return new GraphType.Node( + return new Node( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Labels unpacker.unpack(buf) // Properties ); }, rel : ( unpacker, buf ) => { - return new GraphType.Relationship( + return new Relationship( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Start Node Identity unpacker.unpack(buf), // End Node Identity @@ -117,7 +127,7 @@ let _mappers = { ); }, unboundRel : ( unpacker, buf ) => { - return new GraphType.UnboundRelationship( + return new UnboundRelationship( unpacker.unpack(buf), // Identity unpacker.unpack(buf), // Type unpacker.unpack(buf) // Properties @@ -136,7 +146,7 @@ let _mappers = { rel; if (relIndex > 0) { rel = rels[relIndex - 1]; - if( rel instanceof GraphType.UnboundRelationship ) { + if( rel instanceof UnboundRelationship ) { // To avoid duplication, relationships in a path do not contain // information about their start and end nodes, that's instead // inferred from the path sequence. This is us inferring (and, @@ -145,16 +155,16 @@ let _mappers = { } } else { rel = rels[-relIndex - 1]; - if( rel instanceof GraphType.UnboundRelationship ) { + if( rel instanceof UnboundRelationship ) { // See above rels[-relIndex - 1] = rel = rel.bind(nextNode.identity, prevNode.identity); } } // Done hydrating one path segment. - segments.push( new GraphType.PathSegment( prevNode, rel, nextNode ) ); + segments.push( new PathSegment( prevNode, rel, nextNode ) ); prevNode = nextNode; } - return new GraphType.Path(nodes[0], nodes[nodes.length - 1], segments ); + return new Path(nodes[0], nodes[nodes.length - 1], segments ); } }; @@ -177,20 +187,22 @@ class Connection { * @constructor * @param channel - channel with a 'write' function and a 'onmessage' * callback property + * @param url - url to connect to */ - constructor (channel) { + constructor (channel, url) { /** * An ordered queue of observers, each exchange response (zero or more * RECORD messages followed by a SUCCESS message) we recieve will be routed * to the next pending observer. */ + this.url = url; this._pendingObservers = []; this._currentObserver = undefined; this._ch = channel; - this._dechunker = new chunking.Dechunker(); - this._chunker = new chunking.Chunker( channel ); - this._packer = new packstream.Packer( this._chunker ); - this._unpacker = new packstream.Unpacker(); + this._dechunker = new Dechunker(); + this._chunker = new Chunker( channel ); + this._packer = new Packer( this._chunker ); + this._unpacker = new Unpacker(); this._isHandlingFailure = false; // Set to true on fatal errors, to get this out of session pool. @@ -456,18 +468,20 @@ class Connection { function connect( url, config = {}) { let Ch = config.channel || Channel; return new Connection( new Ch({ - host: host(url), - port: port(url) || 7687, + host: parseHost(url), + port: parsePort(url) || 7687, // Default to using ENCRYPTION_NON_LOCAL if trust-on-first-use is available - encrypted : shouldEncrypt(config.encrypted, (hasFeature("trust_on_first_use") ? ENCRYPTION_NON_LOCAL : ENCRYPTION_OFF), host(url)), + encrypted : shouldEncrypt(config.encrypted, (hasFeature("trust_on_first_use") ? ENCRYPTION_NON_LOCAL : ENCRYPTION_OFF), parseHost(url)), // Default to using TRUST_ON_FIRST_USE if it is available trust : config.trust || (hasFeature("trust_on_first_use") ? "TRUST_ON_FIRST_USE" : "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES"), trustedCertificates : config.trustedCertificates || [], knownHosts : config.knownHosts - })); + }), url); } -export default { +export { connect, + parseScheme, + parseUrl, Connection } diff --git a/src/v1/internal/log.js b/src/v1/internal/log.js index ce0d77007..4af6413dd 100644 --- a/src/v1/internal/log.js +++ b/src/v1/internal/log.js @@ -16,7 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export default { - "debug": (val) => { console.log(val); } + +function debug(val) { + console.log(val); } + +export default debug \ No newline at end of file diff --git a/src/v1/internal/packstream.js b/src/v1/internal/packstream.js index b8f36ed17..9d925efe4 100644 --- a/src/v1/internal/packstream.js +++ b/src/v1/internal/packstream.js @@ -17,10 +17,11 @@ * limitations under the License. */ -import {debug} from "./log"; +import debug from "./log"; import {alloc} from "./buf"; import utf8 from "./utf8"; -import {Integer, int} from "../integer"; +import Integer from "../integer"; +import {int} from "../integer"; import {newError} from './../error'; let MAX_CHUNK_SIZE = 16383, @@ -375,8 +376,8 @@ class Unpacker { } } -export default { +export { Packer, Unpacker, Structure -}; +} diff --git a/src/v1/internal/pool.js b/src/v1/internal/pool.js index 37d898900..116eeb39b 100644 --- a/src/v1/internal/pool.js +++ b/src/v1/internal/pool.js @@ -35,34 +35,65 @@ class Pool { this._destroy = destroy; this._validate = validate; this._maxIdle = maxIdle; - this._pool = []; + this._pools = {}; this._release = this._release.bind(this); } - acquire() { + acquire(key) { let resource; - while( this._pool.length ) { - resource = this._pool.pop(); + let pool = this._pools[key]; + if (!pool) { + pool = []; + this._pools[key] = pool; + } + while (pool.length) { + resource = pool.pop(); - if( this._validate(resource) ) { + if (this._validate(resource)) { return resource; } else { this._destroy(resource); } } - return this._create(this._release); + return this._create(key, this._release); + } + + purge(key) { + let resource; + let pool = this._pools[key] || []; + while (pool.length) { + resource = pool.pop(); + this._destroy(resource) + } + delete this._pools[key] + } + + purgeAll() { + for (let key in this._pools.keys) { + if (this._pools.hasOwnPropertykey) { + this.purge(key); + } + } + } + + has(key) { + return (key in this._pools); } - _release(resource) { - if( this._pool.length >= this._maxIdle || !this._validate(resource) ) { + _release(key, resource) { + let pool = this._pools[key]; + if (!pool) { + //key has been purged, don't put it back + return; + } + if( pool.length >= this._maxIdle || !this._validate(resource) ) { this._destroy(resource); } else { - this._pool.push(resource); + pool.push(resource); } } } -export default { - Pool -} +export default Pool + diff --git a/src/v1/internal/round-robin-array.js b/src/v1/internal/round-robin-array.js new file mode 100644 index 000000000..8ece12e9f --- /dev/null +++ b/src/v1/internal/round-robin-array.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +/** + * An array that lets you hop through the elements endlessly. + */ +class RoundRobinArray { + constructor(items) { + this._items = items || []; + this._index = 0; + } + + next() { + let elem = this._items[this._index]; + if (this._items.length === 0) { + this._index = 0; + } else { + this._index = (this._index + 1) % (this._items.length); + } + return elem; + } + + push(elem) { + this._items.push(elem); + } + + pushAll(elems) { + Array.prototype.push.apply(this._items, elems); + } + + empty() { + return this._items.length === 0; + } + + clear() { + this._items = []; + this._index = 0; + } + + size() { + return this._items.length; + } + + toArray() { + return this._items; + } + + remove(item) { + let index = this._items.indexOf(item); + while (index != -1) { + this._items.splice(index, 1); + if (index < this._index) { + this._index -= 1; + } + //make sure we are in range + if (this._items.length === 0) { + this._index = 0; + } else { + this._index %= this._items.length; + } + index = this._items.indexOf(item, index); + } + } +} + +export default RoundRobinArray \ No newline at end of file diff --git a/src/v1/internal/stream-observer.js b/src/v1/internal/stream-observer.js index ba06c822d..c98bbd61c 100644 --- a/src/v1/internal/stream-observer.js +++ b/src/v1/internal/stream-observer.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import {Record} from "../record"; +import Record from "../record"; /** * Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses @@ -32,14 +32,16 @@ import {Record} from "../record"; class StreamObserver { /** * @constructor + * @param errorTransformer optional callback to be used for adding additional logic on error */ - constructor() { + constructor(errorTransformer = (err) => {return err}) { this._fieldKeys = null; this._fieldLookup = null; this._queuedRecords = []; this._tail = null; this._error = null; this._hasFailed = false; + this._errorTransformer = errorTransformer; } /** @@ -81,6 +83,10 @@ class StreamObserver { } } + resolveConnection(conn) { + this._conn = conn; + } + /** * Will be called on errors. * If user-provided observer is present, pass the error @@ -88,18 +94,19 @@ class StreamObserver { * @param {Object} error - An error object */ onError(error) { + let transformedError = this._errorTransformer(error, this._conn); if(this._hasFailed) { return; } this._hasFailed = true; if( this._observer ) { if( this._observer.onError ) { - this._observer.onError( error ); + this._observer.onError( transformedError ); } else { - console.log( error ); + console.log( transformedError ); } } else { - this._error = error; + this._error = transformedError; } } diff --git a/src/v1/internal/utf8.js b/src/v1/internal/utf8.js index 8230dd874..5913b7ab8 100644 --- a/src/v1/internal/utf8.js +++ b/src/v1/internal/utf8.js @@ -20,7 +20,7 @@ // This module defines a cross-platform UTF-8 encoder and decoder that works // with the Buffer API defined in buf.js -import buf from "./buf"; +import {alloc, NodeBuffer, HeapBuffer, CombinedBuffer} from "./buf"; import {StringDecoder} from 'string_decoder'; import {newError} from './../error'; let platformObj = {}; @@ -34,16 +34,16 @@ try { platformObj = { "encode": function (str) { - return new buf.NodeBuffer(new node.Buffer(str, "UTF-8")); + return new NodeBuffer(new node.Buffer(str, "UTF-8")); }, "decode": function (buffer, length) { - if (buffer instanceof buf.NodeBuffer) { + if (buffer instanceof NodeBuffer) { let start = buffer.position, end = start + length; buffer.position = Math.min(end, buffer.length); return buffer._buffer.toString('utf8', start, end); } - else if (buffer instanceof buf.CombinedBuffer) { + else if (buffer instanceof CombinedBuffer) { let out = streamDecodeCombinedBuffer(buffer, length, (partBuffer) => { return decoder.write(partBuffer._buffer); @@ -69,16 +69,16 @@ try { platformObj = { "encode": function (str) { - return new buf.HeapBuffer(encoder.encode(str).buffer); + return new HeapBuffer(encoder.encode(str).buffer); }, "decode": function (buffer, length) { - if (buffer instanceof buf.HeapBuffer) { + if (buffer instanceof HeapBuffer) { return decoder.decode(buffer.readView(Math.min(length, buffer.length - buffer.position))); } else { // Decoding combined buffer is complicated. For simplicity, for now, // we simply copy the combined buffer into a regular buffer and decode that. - var tmpBuf = buf.alloc(length); + var tmpBuf = alloc(length); for (var i = 0; i < length; i++) { tmpBuf.writeUInt8(buffer.readUInt8()); } diff --git a/src/v1/record.js b/src/v1/record.js index 249e918f6..d96221c43 100644 --- a/src/v1/record.js +++ b/src/v1/record.js @@ -132,4 +132,4 @@ class Record { } } -export {Record} +export default Record diff --git a/src/v1/result-summary.js b/src/v1/result-summary.js index d35e979ff..b02bd7af7 100644 --- a/src/v1/result-summary.js +++ b/src/v1/result-summary.js @@ -261,7 +261,8 @@ const statementType = { SCHEMA_WRITE: 's' }; -export default { - ResultSummary, +export { statementType } + +export default ResultSummary diff --git a/src/v1/result.js b/src/v1/result.js index 285328747..e94a90486 100644 --- a/src/v1/result.js +++ b/src/v1/result.js @@ -17,11 +17,10 @@ * limitations under the License. */ -import {ResultSummary} from './result-summary'; +import ResultSummary from './result-summary'; // Ensure Promise is available -import {polyfill as polyfillPromise} from '../external/es6-promise'; -polyfillPromise(); +import "babel-polyfill"; /** * A stream of {@link Record} representing the result of a statement. diff --git a/src/v1/routing-driver.js b/src/v1/routing-driver.js new file mode 100644 index 000000000..0cad5411b --- /dev/null +++ b/src/v1/routing-driver.js @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2002-2016 "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. + */ + +import Session from './session'; +import {Driver, READ, WRITE} from './driver'; +import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from "./error"; +import RoundRobinArray from './internal/round-robin-array'; +import {int} from './integer' +import Integer from './integer' +import "babel-polyfill"; + +/** + * A driver that supports routing in a core-edge cluster. + */ +class RoutingDriver extends Driver { + + constructor(url, userAgent = 'neo4j-javascript/0.0', token = {}, config = {}) { + super(url, userAgent, token, config); + this._clusterView = new ClusterView(new RoundRobinArray([url])); + } + + _createSession(connectionPromise, cb) { + return new RoutingSession(connectionPromise, cb, (err, conn) => { + let code = err.code; + let msg = err.message; + if (!code) { + try { + code = err.fields[0].code; + } catch (e) { + code = 'UNKNOWN'; + } + } + if (!msg) { + try { + msg = err.fields[0].message; + } catch (e) { + msg = 'Unknown failure occurred'; + } + } + //just to simplify later error handling + err.code = code; + err.message = msg; + + if (code === SERVICE_UNAVAILABLE || code === SESSION_EXPIRED) { + if (conn) { + this._forget(conn.url) + } else { + connectionPromise.then((conn) => { + this._forget(conn.url); + }).catch(() => {/*ignore*/}); + } + return err; + } else if (code === 'Neo.ClientError.Cluster.NotALeader') { + let url = 'UNKNOWN'; + if (conn) { + url = conn.url; + this._clusterView.writers.remove(conn.url); + } else { + connectionPromise.then((conn) => { + this._clusterView.writers.remove(conn.url); + }).catch(() => {/*ignore*/}); + } + return newError("No longer possible to write to server at " + url, SESSION_EXPIRED); + } else { + return err; + } + }); + } + + _updatedClusterView() { + if (!this._clusterView.needsUpdate()) { + return Promise.resolve(this._clusterView); + } else { + let call = () => { + let conn = this._pool.acquire(routers.next()); + let session = this._createSession(Promise.resolve(conn)); + return newClusterView(session).catch((err) => { + this._forget(conn); + return Promise.reject(err); + }); + }; + let routers = this._clusterView.routers; + //Build a promise chain that ends on the first successful call + //i.e. call().catch(call).catch(call).catch(call)... + //each call will try a different router + let acc = Promise.reject(); + for (let i = 0; i < routers.size(); i++) { + acc = acc.catch(call); + } + return acc; + } + } + + _diff(oldView, updatedView) { + let oldSet = oldView.all(); + let newSet = updatedView.all(); + newSet.forEach((item) => { + oldSet.delete(item); + }); + return oldSet; + } + + _acquireConnection(mode) { + let m = mode || WRITE; + //make sure we have enough servers + return this._updatedClusterView().then((view) => { + let toRemove = this._diff(this._clusterView, view); + let self = this; + toRemove.forEach((url) => { + self._pool.purge(url); + }); + //update our cached view + this._clusterView = view; + if (m === READ) { + let key = view.readers.next(); + if (!key) { + return Promise.reject(newError('No read servers available', SESSION_EXPIRED)); + } + return this._pool.acquire(key); + } else if (m === WRITE) { + let key = view.writers.next(); + if (!key) { + return Promise.reject(newError('No write servers available', SESSION_EXPIRED)); + } + return this._pool.acquire(key); + } else { + return Promise.reject(m + " is not a valid option"); + } + }).catch((err) => {return Promise.reject(err)}); + } + + _forget(url) { + this._pool.purge(url); + this._clusterView.remove(url); + } +} + +class ClusterView { + constructor(routers, readers, writers, expires) { + this.routers = routers || new RoundRobinArray(); + this.readers = readers || new RoundRobinArray(); + this.writers = writers || new RoundRobinArray(); + this._expires = expires || int(-1); + + } + + needsUpdate() { + return this._expires.lessThan(Date.now()) || + this.routers.size() <= 1 || + this.readers.empty() || + this.writers.empty(); + } + + all() { + let seen = new Set(this.routers.toArray()); + let writers = this.writers.toArray(); + let readers = this.readers.toArray(); + for (let i = 0; i < writers.length; i++) { + seen.add(writers[i]); + } + for (let i = 0; i < readers.length; i++) { + seen.add(readers[i]); + } + return seen; + } + + remove(item) { + this.routers.remove(item); + this.readers.remove(item); + this.writers.remove(item); + } +} + +class RoutingSession extends Session { + constructor(connectionPromise, onClose, onFailedConnection) { + super(connectionPromise, onClose); + this._onFailedConnection = onFailedConnection; + } + + _onRunFailure() { + return this._onFailedConnection; + } +} + +let GET_SERVERS = "CALL dbms.cluster.routing.getServers"; + +/** + * Calls `getServers` and retrieves a new promise of a ClusterView. + * @param session + * @returns {Promise.} + */ +function newClusterView(session) { + return session.run(GET_SERVERS) + .then((res) => { + session.close(); + if (res.records.length != 1) { + return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)); + } + let record = res.records[0]; + let now = int(Date.now()); + let expires = record.get('ttl').multiply(1000).add(now); + //if the server uses a really big expire time like Long.MAX_VALUE + //this may have overflowed + if (expires.lessThan(now)) { + expires = Integer.MAX_VALUE; + } + let servers = record.get('servers'); + let routers = new RoundRobinArray(); + let readers = new RoundRobinArray(); + let writers = new RoundRobinArray(); + for (let i = 0; i < servers.length; i++) { + let server = servers[i]; + + let role = server['role']; + let addresses = server['addresses']; + if (role === 'ROUTE') { + routers.pushAll(addresses); + } else if (role === 'WRITE') { + writers.pushAll(addresses); + } else if (role === 'READ') { + readers.pushAll(addresses); + } + } + if (routers.empty() || writers.empty()) { + return Promise.reject(newError("Invalid routing response from server", SERVICE_UNAVAILABLE)) + } + return new ClusterView(routers, readers, writers, expires); + }) + .catch((e) => { + if (e.code === 'Neo.ClientError.Procedure.ProcedureNotFound') { + return Promise.reject(newError("Server could not perform routing, make sure you are connecting to a causal cluster", SERVICE_UNAVAILABLE)); + } else { + return Promise.reject(newError("No servers could be found at this instant.", SERVICE_UNAVAILABLE)); + } + }); +} + +export default RoutingDriver diff --git a/src/v1/session.js b/src/v1/session.js index 546190226..85bda64b4 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -20,7 +20,8 @@ import StreamObserver from './internal/stream-observer'; import Result from './result'; import Transaction from './transaction'; -import {Integer, int} from "./integer"; +import Integer from "./integer"; +import {int} from "./integer"; import {newError} from "./error"; /** @@ -32,19 +33,15 @@ import {newError} from "./error"; class Session { /** * @constructor - * @param {Connection} conn - A connection to use + * @param {Promise.} connectionPromise - Promise of a connection to use * @param {function()} onClose - Function to be called on connection close */ - constructor( conn, onClose ) { - this._conn = conn; + constructor( connectionPromise, onClose ) { + this._connectionPromise = connectionPromise; this._onClose = onClose; this._hasTx = false; } - isEncrypted() { - return this._conn.isEncrypted(); - } - /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} @@ -58,11 +55,14 @@ class Session { parameters = statement.parameters || {}; statement = statement.text; } - let streamObserver = new _RunObserver(); + let streamObserver = new _RunObserver(this._onRunFailure()); if (!this._hasTx) { - this._conn.run(statement, parameters, streamObserver); - this._conn.pullAll(streamObserver); - this._conn.sync(); + this._connectionPromise.then((conn) => { + streamObserver.resolveConnection(conn); + conn.run(statement, parameters, streamObserver); + conn.pullAll(streamObserver); + conn.sync(); + }).catch((err) => streamObserver.onError(err)); } else { streamObserver.onError(newError("Statements cannot be run directly on a " + "session with an open transaction; either run from within the " @@ -79,15 +79,21 @@ class Session { * * @returns {Transaction} - New Transaction */ - beginTransaction() { + beginTransaction(bookmark) { if (this._hasTx) { - throw new newError("You cannot begin a transaction on a session with an " + throw newError("You cannot begin a transaction on a session with an " + "open transaction; either run from within the transaction or use a " + "different session.") } this._hasTx = true; - return new Transaction(this._conn, () => {this._hasTx = false}); + return new Transaction(this._connectionPromise, () => { + this._hasTx = false}, + this._onRunFailure(), bookmark, (bookmark) => {this._lastBookmark = bookmark}); + } + + lastBookmark() { + return this._lastBookmark; } /** @@ -106,12 +112,17 @@ class Session { cb(); } } + + //Can be overridden to add error callback on RUN + _onRunFailure() { + return (err) => {return err}; + } } /** Internal stream observer used for transactional results*/ class _RunObserver extends StreamObserver { - constructor() { - super(); + constructor(onError) { + super(onError); this._meta = {}; } diff --git a/src/v1/transaction.js b/src/v1/transaction.js index d4915ad05..cb92c0d6a 100644 --- a/src/v1/transaction.js +++ b/src/v1/transaction.js @@ -27,22 +27,34 @@ import Result from './result'; class Transaction { /** * @constructor - * @param {Connection} conn - A connection to use + * @param {Promise} connectionPromise - A connection to use * @param {function()} onClose - Function to be called when transaction is committed or rolled back. + * @param errorTransformer callback use to transform error + * @param bookmark optional bookmark */ - constructor(conn, onClose) { - this._conn = conn; + constructor(connectionPromise, onClose, errorTransformer, bookmark, onBookmark) { + this._connectionPromise = connectionPromise; let streamObserver = new _TransactionStreamObserver(this); - this._conn.run("BEGIN", {}, streamObserver); - this._conn.discardAll(streamObserver); + let params = {}; + if (bookmark) { + params = {bookmark: bookmark}; + } + this._connectionPromise.then((conn) => { + streamObserver.resolveConnection(conn); + conn.run("BEGIN", params, streamObserver); + conn.discardAll(streamObserver); + }).catch(streamObserver.onError); + this._state = _states.ACTIVE; this._onClose = onClose; + this._errorTransformer = errorTransformer; + this._onBookmark = onBookmark || (() => {}); } /** * 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,7 +64,7 @@ class Transaction { parameters = statement.parameters || {}; statement = statement.text; } - return this._state.run(this._conn, new _TransactionStreamObserver(this), statement, parameters); + return this._state.run(this._connectionPromise, new _TransactionStreamObserver(this), statement, parameters); } /** @@ -63,7 +75,7 @@ class Transaction { * @returns {Result} - New Result */ commit() { - let committed = this._state.commit(this._conn, new _TransactionStreamObserver(this)); + let committed = this._state.commit(this._connectionPromise, new _TransactionStreamObserver(this)); this._state = committed.state; //clean up this._onClose(); @@ -79,7 +91,7 @@ class Transaction { * @returns {Result} - New Result */ rollback() { - let committed = this._state.rollback(this._conn, new _TransactionStreamObserver(this)); + let committed = this._state.rollback(this._connectionPromise, new _TransactionStreamObserver(this)); this._state = committed.state; //clean up this._onClose(); @@ -95,7 +107,7 @@ class Transaction { /** Internal stream observer used for transactional results*/ class _TransactionStreamObserver extends StreamObserver { constructor(tx) { - super(); + super(tx._errorTransformer || ((err) => {return err})); this._tx = tx; //this is to to avoid multiple calls to onError caused by IGNORED this._hasFailed = false; @@ -108,23 +120,35 @@ class _TransactionStreamObserver extends StreamObserver { this._hasFailed = true; } } + + onCompleted(meta) { + super.onCompleted(meta); + let bookmark = meta.bookmark; + if (bookmark) { + this._tx._onBookmark(bookmark); + } + } } /** 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), + commit: (connectionPromise, observer) => { + return {result: _runDiscardAll("COMMIT", connectionPromise, observer), state: _states.SUCCEEDED} }, - rollback: (conn, observer) => { - return {result: _runDiscardAll("ROLLBACK", conn, observer), state: _states.ROLLED_BACK}; + rollback: (connectionPromise, observer) => { + return {result: _runDiscardAll("ROLLBACK", connectionPromise, observer), state: _states.ROLLED_BACK}; }, - run: (conn, observer, statement, parameters) => { - conn.run( statement, parameters || {}, observer ); - conn.pullAll( observer ); - conn.sync(); + run: (connectionPromise, observer, statement, parameters) => { + connectionPromise.then((conn) => { + observer.resolveConnection(conn); + conn.run( statement, parameters || {}, observer ); + conn.pullAll( observer ); + conn.sync(); + }).catch(observer.onError); + return new Result( observer, statement, parameters ); } }, @@ -196,10 +220,14 @@ let _states = { } }; -function _runDiscardAll(msg, conn, observer) { - conn.run(msg, {}, observer ); - conn.discardAll(observer); - conn.sync(); +function _runDiscardAll(msg, connectionPromise, observer) { + connectionPromise.then((conn) => { + observer.resolveConnection(conn); + conn.run(msg, {}, observer); + conn.discardAll(observer); + conn.sync(); + }).catch(observer.onError); + return new Result(observer, msg, {}); } diff --git a/test/internal/buf.test.js b/test/internal/buf.test.js index b2ada9c15..77e35efdc 100644 --- a/test/internal/buf.test.js +++ b/test/internal/buf.test.js @@ -19,7 +19,7 @@ var alloc = require('../../lib/v1/internal/buf').alloc; var CombinedBuffer = require('../../lib/v1/internal/buf').CombinedBuffer; -var utf8 = require('../../lib/v1/internal/utf8'); +var utf8 = require('../../lib/v1/internal/utf8').default; var Unpacker = require("../../lib/v1/internal/packstream.js").Unpacker; describe('buffers', function() { diff --git a/test/internal/packstream.test.js b/test/internal/packstream.test.js index 9da74986d..ec9a0c42e 100644 --- a/test/internal/packstream.test.js +++ b/test/internal/packstream.test.js @@ -18,12 +18,11 @@ */ var alloc = require('../../lib/v1/internal/buf').alloc, - packstream = require("../../lib/v1/internal/packstream.js"), - integer = require("../../lib/v1/integer.js"), + packstream = require("../../lib/v1/internal/packstream"), + Integer = require("../../lib/v1/integer").default, Packer = packstream.Packer, Unpacker = packstream.Unpacker, - Structure = packstream.Structure, - Integer = integer.Integer; + Structure = packstream.Structure; describe('packstream', function() { diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index f9a17b5aa..a18d3657a 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -17,17 +17,18 @@ * limitations under the License. */ -var Pool = require('../../lib/v1/internal/pool').Pool; +var Pool = require('../../lib/v1/internal/pool').default; describe('Pool', function() { it('allocates if pool is empty', function() { // Given var counter = 0; - var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + var key = "bolt://localhost:7687"; + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); // Then expect( r0.id ).toBe( 0 ); @@ -37,33 +38,56 @@ describe('Pool', function() { it('pools if resources are returned', function() { // Given a pool that allocates var counter = 0; - var pool = new Pool( function (release) { return new Resource(counter++, release) } ); + var key = "bolt://localhost:7687"; + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); // When - var r0 = pool.acquire(); + var r0 = pool.acquire(key); r0.close(); - var r1 = pool.acquire(); + var r1 = pool.acquire(key); // Then expect( r0.id ).toBe( 0 ); expect( r1.id ).toBe( 0 ); }); + it('handles multiple keys', function() { + // Given a pool that allocates + var counter = 0; + var key1 = "bolt://localhost:7687"; + var key2 = "bolt://localhost:7688"; + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) } ); + + // When + var r0 = pool.acquire(key1); + var r1 = pool.acquire(key2); + r0.close(); + var r2 = pool.acquire(key1); + var r3 = pool.acquire(key2); + + // Then + expect( r0.id ).toBe( 0 ); + expect( r1.id ).toBe( 1 ); + expect( r2.id ).toBe( 0 ); + expect( r3.id ).toBe( 2 ); + }); + it('frees if pool reaches max size', function() { // Given a pool that tracks destroyed resources var counter = 0, destroyed = []; + var key = "bolt://localhost:7687"; var pool = new Pool( - function (release) { return new Resource(counter++, release) }, + function (url, release) { return new Resource(url, counter++, release) }, function (resource) { destroyed.push(resource); }, function (resource) { return true; }, 2 // maxIdle ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); - var r2 = pool.acquire(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); + var r2 = pool.acquire(key); r0.close(); r1.close(); r2.close(); @@ -77,16 +101,17 @@ describe('Pool', function() { // Given a pool that allocates var counter = 0, destroyed = []; + var key = "bolt://localhost:7687"; var pool = new Pool( - function (release) { return new Resource(counter++, release) }, + function (url, release) { return new Resource(url, counter++, release) }, function (resource) { destroyed.push(resource); }, function (resource) { return false; }, 1000 // maxIdle ); // When - var r0 = pool.acquire(); - var r1 = pool.acquire(); + var r0 = pool.acquire(key); + var r1 = pool.acquire(key); r0.close(); r1.close(); @@ -95,10 +120,42 @@ describe('Pool', function() { expect( destroyed[0].id ).toBe( r0.id ); expect( destroyed[1].id ).toBe( r1.id ); }); + + + it('purges keys', function() { + // Given a pool that allocates + var counter = 0; + var key1 = "bolt://localhost:7687"; + var key2 = "bolt://localhost:7688"; + var pool = new Pool( function (url, release) { return new Resource(url, counter++, release) }, + function (res) {res.destroyed = true; return true} + ); + + // When + var r0 = pool.acquire(key1); + var r1 = pool.acquire(key2); + r0.close(); + r1.close(); + expect(pool.has(key1)).toBe(true); + expect(pool.has(key2)).toBe(true); + pool.purge(key1); + expect(pool.has(key1)).toBe(false); + expect(pool.has(key2)).toBe(true); + + var r2 = pool.acquire(key1); + var r3 = pool.acquire(key2); + + // Then + expect( r0.id ).toBe( 0 ); + expect( r0.destroyed ).toBe( true ); + expect( r1.id ).toBe( 1 ); + expect( r2.id ).toBe( 2 ); + expect( r3.id ).toBe( 1 ); + }); }); -function Resource( id, release ) { +function Resource( key, id, release) { var self = this; this.id = id; - this.close = function() { release(self); }; + this.close = function() { release(key, self); }; } \ No newline at end of file diff --git a/test/internal/round-robin-array.test.js b/test/internal/round-robin-array.test.js new file mode 100644 index 000000000..5d2ff8530 --- /dev/null +++ b/test/internal/round-robin-array.test.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2002-2016 "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 RoundRobinArray = require('../../lib/v1/internal/round-robin-array').default; + +describe('round-robin-array', function() { + it('should step through array', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + //.... + }); + + it('should step through single element array', function () { + var array = new RoundRobinArray([5]); + + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(5); + //.... + }); + + it('should handle deleting item before current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + array.remove(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(3); + //.... + }); + + it('should handle deleting item on current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + array.remove(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(4); + //.... + }); + + it('should handle deleting item after current ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + array.remove(4); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + //.... + }); + + it('should handle deleting last item ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + array.remove(5); + expect(array.next()).toEqual(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(1); + //.... + }); + + it('should handle deleting first item ', function () { + var array = new RoundRobinArray([1,2,3,4,5]); + array.remove(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(4); + expect(array.next()).toEqual(5); + //.... + }); + + it('should handle deleting multiple items ', function () { + var array = new RoundRobinArray([1,2,3,1,1]); + array.remove(1); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + expect(array.next()).toEqual(2); + expect(array.next()).toEqual(3); + //.... + }); + +}); diff --git a/test/internal/tls.test.js b/test/internal/tls.test.js index e0fe8f6f8..dbf40ac24 100644 --- a/test/internal/tls.test.js +++ b/test/internal/tls.test.js @@ -20,7 +20,7 @@ var NodeChannel = require('../../lib/v1/internal/ch-node.js'); var neo4j = require("../../lib/v1"); var fs = require("fs"); var path = require('path'); -var hasFeature = require("../../lib/v1/internal/features"); +var hasFeature = require("../../lib/v1/internal/features").default; var isLocalHost = require("../../lib/v1/internal/util").isLocalHost; describe('trust-signed-certificates', function() { diff --git a/test/internal/utf8.test.js b/test/internal/utf8.test.js index ddd17b767..8ab8b5fc2 100644 --- a/test/internal/utf8.test.js +++ b/test/internal/utf8.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -var utf8 = require('../../lib/v1/internal/utf8'); +var utf8 = require('../../lib/v1/internal/utf8').default; var buffers = require('../../lib/v1/internal/buf'); describe('utf8', function() { diff --git a/test/resources/boltkit/acquire_endpoints.script b/test/resources/boltkit/acquire_endpoints.script new file mode 100644 index 000000000..9c92e4863 --- /dev/null +++ b/test/resources/boltkit/acquire_endpoints.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/dead_server.script b/test/resources/boltkit/dead_server.script new file mode 100644 index 000000000..037a6be65 --- /dev/null +++ b/test/resources/boltkit/dead_server.script @@ -0,0 +1,7 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "MATCH (n) RETURN n.name" {} +C: PULL_ALL +S: \ No newline at end of file diff --git a/test/resources/boltkit/discover_new_servers.script b/test/resources/boltkit/discover_new_servers.script new file mode 100644 index 000000000..2d5aa55ae --- /dev/null +++ b/test/resources/boltkit/discover_new_servers.script @@ -0,0 +1,13 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9003"], "role": "READ"},{"addresses": ["127.0.0.1:9004","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/discover_servers.script b/test/resources/boltkit/discover_servers.script new file mode 100644 index 000000000..6f92d458f --- /dev/null +++ b/test/resources/boltkit/discover_servers.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002","127.0.0.1:9003"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {} + diff --git a/test/resources/boltkit/handle_empty_get_servers_response.script b/test/resources/boltkit/handle_empty_get_servers_response.script new file mode 100644 index 000000000..872dffadd --- /dev/null +++ b/test/resources/boltkit/handle_empty_get_servers_response.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/no_writers.script b/test/resources/boltkit/no_writers.script new file mode 100644 index 000000000..c7ae7b29a --- /dev/null +++ b/test/resources/boltkit/no_writers.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": [],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/non_discovery.script b/test/resources/boltkit/non_discovery.script new file mode 100644 index 000000000..b9c99f9dc --- /dev/null +++ b/test/resources/boltkit/non_discovery.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "blabla"} +S: IGNORED diff --git a/test/resources/boltkit/not_able_to_write.script b/test/resources/boltkit/not_able_to_write.script new file mode 100644 index 000000000..6f14544b5 --- /dev/null +++ b/test/resources/boltkit/not_able_to_write.script @@ -0,0 +1,12 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL +!: AUTO DISCARD_ALL +!: AUTO RUN "ROLLBACK" {} +!: AUTO RUN "BEGIN" {} +!: AUTO PULL_ALL + +C: RUN "CREATE ()" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "blabla"} +S: IGNORED \ No newline at end of file diff --git a/test/resources/boltkit/not_able_to_write_in_transaction.script b/test/resources/boltkit/not_able_to_write_in_transaction.script new file mode 100644 index 000000000..a09382494 --- /dev/null +++ b/test/resources/boltkit/not_able_to_write_in_transaction.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL +!: AUTO DISCARD_ALL +!: AUTO RUN "ROLLBACK" {} +!: AUTO RUN "BEGIN" {} +!: AUTO PULL_ALL + +C: RUN "CREATE ()" {} +C: PULL_ALL +S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "blabla"} +S: IGNORED +C: RUN "COMMIT" {} +S: IGNORED \ No newline at end of file diff --git a/test/resources/boltkit/read_server.script b/test/resources/boltkit/read_server.script new file mode 100644 index 000000000..0b4d44748 --- /dev/null +++ b/test/resources/boltkit/read_server.script @@ -0,0 +1,11 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "MATCH (n) RETURN n.name" {} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + RECORD ["Bob"] + RECORD ["Alice"] + RECORD ["Tina"] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/rediscover.script b/test/resources/boltkit/rediscover.script new file mode 100644 index 000000000..ea5c4ed59 --- /dev/null +++ b/test/resources/boltkit/rediscover.script @@ -0,0 +1,14 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"], "role": "ROUTE"}]] + SUCCESS {} +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9004"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005"], "role": "READ"},{"addresses": ["127.0.0.1:9002","127.0.0.1:9003","127.0.0.1:9004"], "role": "ROUTE"}]] + SUCCESS {} diff --git a/test/resources/boltkit/return_x.script b/test/resources/boltkit/return_x.script new file mode 100644 index 000000000..929e73bf3 --- /dev/null +++ b/test/resources/boltkit/return_x.script @@ -0,0 +1,10 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO RUN "RETURN 1 // JavaDriver poll to test connection" {} +!: AUTO PULL_ALL + +C: RUN "RETURN {x}" {"x": 1} + PULL_ALL +S: SUCCESS {"fields": ["x"]} + RECORD [1] + SUCCESS {} diff --git a/test/resources/boltkit/short_ttl.script b/test/resources/boltkit/short_ttl.script new file mode 100644 index 000000000..ceffcc0ef --- /dev/null +++ b/test/resources/boltkit/short_ttl.script @@ -0,0 +1,9 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CALL dbms.cluster.routing.getServers" {} + PULL_ALL +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [0, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9004"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS {} \ No newline at end of file diff --git a/test/resources/boltkit/write_server.script b/test/resources/boltkit/write_server.script new file mode 100644 index 000000000..993910f6c --- /dev/null +++ b/test/resources/boltkit/write_server.script @@ -0,0 +1,8 @@ +!: AUTO INIT +!: AUTO RESET +!: AUTO PULL_ALL + +C: RUN "CREATE (n {name:'Bob'})" {} + PULL_ALL +S: SUCCESS {} + SUCCESS {} \ No newline at end of file diff --git a/test/v1/boltkit.js b/test/v1/boltkit.js new file mode 100644 index 000000000..1c71c7c1d --- /dev/null +++ b/test/v1/boltkit.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2002-2016 "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 childProcess = require("child_process"); + +var BoltKit = function (verbose) { + this.verbose = verbose || false; +}; + +BoltKit.prototype.start = function(script, port) { + var spawn = childProcess.spawn, server, code = -1; + + server = spawn('/usr/local/bin/boltstub', ['-v', port, script]); + if (this.verbose) { + server.stdout.on('data', (data) => { + console.log(`${data}`); + }); + server.stderr.on('data', (data) => { + console.log(`${data}`); + }); + server.on('end', function (data) { + console.log(data); + }); + } + + server.on('close', function (c) { + code = c; + }); + + server.on('error', function (err) { + console.log('Failed to start child process:' + err); + }); + + var Server = function(){}; + //give process some time to exit + Server.prototype.exit = function(callback) {setTimeout(function(){callback(code);}, 1000)}; + + return new Server(); +}; + +//Make sure boltstub is started before running +//user code +BoltKit.prototype.run = function(callback) { + setTimeout(callback, 1000); +}; + +function boltKitSupport() { + try { + var test = childProcess.spawn; + return !!test; + } catch (e) { + return false; + } +} + +module.exports = { + BoltKit: BoltKit, + BoltKitSupport: boltKitSupport() +}; + diff --git a/test/v1/direct.driver.boltkit.it.js b/test/v1/direct.driver.boltkit.it.js new file mode 100644 index 000000000..bf2ab9b45 --- /dev/null +++ b/test/v1/direct.driver.boltkit.it.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2002-2016 "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").default; +var boltkit = require('./boltkit'); + +describe('direct driver', function() { + + it('should run query', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/return_x.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt://localhost:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + // Then + session.run("RETURN {x}", {'x': 1}).then(function (res) { + expect(res.records[0].get('x').toInt()).toEqual(1); + session.close(); + driver.close(); + server.exit(function(code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); +}); + diff --git a/test/v1/driver.test.js b/test/v1/driver.test.js index ef695deb7..dbb2cac72 100644 --- a/test/v1/driver.test.js +++ b/test/v1/driver.test.js @@ -48,6 +48,10 @@ describe('driver', function() { driver.session(); }); + it('should handle wrong scheme ', function() { + expect(function(){neo4j.driver("tank://localhost", neo4j.auth.basic("neo4j", "neo4j"))}).toThrow(new Error("Unknown scheme: tank://")); + }); + it('should fail early on wrong credentials', function(done) { // Given var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "who would use such a password")); @@ -107,7 +111,22 @@ describe('driver', function() { var driver = neo4j.driver("bolt://localhost", neo4j.auth.custom("neo4j", "neo4j", "native", "basic", {secret: 42})); // Expect - driver.onCompleted = function (meta) { + driver.onCompleted = function () { + done(); + }; + + // When + driver.session(); + }); + + it('should fail nicely when connecting with routing to standalone server', function(done) { + // Given + var driver = neo4j.driver("bolt+routing://localhost", neo4j.auth.basic("neo4j", "neo4j")); + + // Expect + driver.onError = function (err) { + expect(err.message).toEqual('Server could not perform routing, make sure you are connecting to a causal cluster'); + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); done(); }; diff --git a/test/v1/examples.test.js b/test/v1/examples.test.js index 42eb213ef..16a7f820e 100644 --- a/test/v1/examples.test.js +++ b/test/v1/examples.test.js @@ -29,24 +29,33 @@ var _console = console; */ describe('examples', function() { - var driverGlobal, sessionGlobal, out, console; + var driverGlobal, out, console, originalTimeout; - beforeEach(function(done) { + beforeAll(function () { var neo4j = neo4jv1; + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + //tag::construct-driver[] - driverGlobal = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); + var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); //end::construct-driver[] - sessionGlobal = driverGlobal.session(); + driverGlobal = driver; + }); + + beforeEach(function(done) { // Override console.log, to assert on stdout output out = []; console = { log: function(msg) { out.push(msg); } }; - - sessionGlobal.run("MATCH (n) DETACH DELETE n").then(done); + var session = driverGlobal.session(); + session.run("MATCH (n) DETACH DELETE n").then(function () { + session.close(); + done(); + }); }); - afterEach(function() { - sessionGlobal.close(); + afterAll(function() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; driverGlobal.close(); }); @@ -100,14 +109,15 @@ describe('examples', function() { }); it('should document a statement', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::statement[] session .run( "CREATE (person:Person {name: {name}})", {name: "Arthur"} ) // end::statement[] .then( function(result) { var theOnesCreated = result.summary.counters.nodesCreated(); - console.log("There were " + theOnesCreated + " the ones created.") + console.log("There were " + theOnesCreated + " the ones created."); + session.close(); }) .then(function() { expect(out[0]).toBe("There were 1 the ones created."); @@ -116,7 +126,7 @@ describe('examples', function() { }); it('should document a statement without parameters', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::statement-without-parameters[] session .run( "CREATE (p:Person { name: 'Arthur' })" ) @@ -124,6 +134,7 @@ describe('examples', function() { .then( function(result) { var theOnesCreated = result.summary.counters.nodesCreated(); console.log("There were " + theOnesCreated + " the ones created."); + session.close(); }); // Then @@ -134,7 +145,7 @@ describe('examples', function() { }); it('should be able to iterate results', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run( "CREATE (weapon:Weapon { name: 'Sword in the stone' })" ) .then(function() { @@ -163,7 +174,7 @@ describe('examples', function() { }); it('should be able to access records', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run( "CREATE (weapon:Weapon { name: 'Sword in the stone', owner: 'Arthur', material: 'Stone', size: 'Huge' })" ) .then(function() { @@ -198,7 +209,7 @@ describe('examples', function() { }); it('should be able to retain for later processing', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session .run("CREATE (knight:Person:Knight { name: 'Lancelot', castle: 'Camelot' })") @@ -208,16 +219,17 @@ describe('examples', function() { .run("MATCH (knight:Person:Knight) WHERE knight.castle = {castle} RETURN knight.name AS name", {castle: "Camelot"}) .then(function (result) { var records = []; - for (i = 0; i < result.records.length; i++) { + for (var i = 0; i < result.records.length; i++) { records.push(result.records[i]); } return records; }) .then(function (records) { - for(i = 0; i < records.length; i ++) - { + for(var i = 0; i < records.length; i ++) { console.log(records[i].get("name") + " is a knight of Camelot"); } + session.close(); + }); // end::retain-result[] }); @@ -230,7 +242,7 @@ describe('examples', function() { }); it('should be able to do nested queries', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session();; session .run( "CREATE (knight:Person:Knight { name: 'Lancelot', castle: 'Camelot' })" + "CREATE (king:Person { name: 'Arthur', title: 'King' })" ) @@ -249,6 +261,7 @@ describe('examples', function() { .run("MATCH (:Knight)-[:DEFENDS]->() RETURN count(*)") .then(function (result) { console.log("Count is " + result.records[0].get(0).toInt()); + session.close(); }); }, onError: function(error) { @@ -266,27 +279,30 @@ describe('examples', function() { }); it('should be able to handle cypher error', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::handle-cypher-error[] session .run("Then will cause a syntax error") .catch( function(err) { expect(err.fields[0].code).toBe( "Neo.ClientError.Statement.SyntaxError" ); + session.close(); done(); }); // end::handle-cypher-error[] }); it('should be able to profile', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); session.run("CREATE (:Person {name:'Arthur'})").then(function() { // tag::result-summary-query-profile[] session .run("PROFILE MATCH (p:Person {name: {name}}) RETURN id(p)", {name: "Arthur"}) .then(function (result) { + //_console.log(result.summary.profile); console.log(result.summary.profile); + session.close(); }); // end::result-summary-query-profile[] }); @@ -295,11 +311,11 @@ describe('examples', function() { setTimeout(function() { expect(out.length).toBe(1); done(); - }, 1000); + }, 2000); }); it('should be able to see notifications', function(done) { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::result-summary-notifications[] session @@ -309,6 +325,7 @@ describe('examples', function() { for (i = 0; i < notifications.length; i++) { console.log(notifications[i].code); } + session.close(); }); // end::result-summary-notifications[] @@ -319,28 +336,26 @@ describe('examples', function() { }); it('should document committing a transaction', function() { - var session = sessionGlobal; + var session = driverGlobal.session(); // tag::transaction-commit[] var tx = session.beginTransaction(); tx.run( "CREATE (:Person {name: 'Guinevere'})" ); - tx.commit(); + tx.commit().then(function() {session.close()}); // end::transaction-commit[] }); it('should document rolling back a transaction', function() { - var session = sessionGlobal; + var session = driverGlobal.session();; // tag::transaction-rollback[] var tx = session.beginTransaction(); tx.run( "CREATE (:Person {name: 'Merlin'})" ); - tx.rollback(); + tx.rollback().then(function() {session.close()}); // end::transaction-rollback[] }); it('should document how to require encryption', function() { - var session = sessionGlobal; - var neo4j = neo4jv1; // tag::tls-require-encryption[] var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { diff --git a/test/v1/record.test.js b/test/v1/record.test.js index c5c5a4a65..c059cdd2e 100644 --- a/test/v1/record.test.js +++ b/test/v1/record.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -var Record = require("../../lib/v1/record").Record; +var Record = require("../../lib/v1/record").default; var Neo4jError = require("../../lib/v1/error").Neo4jError; diff --git a/test/v1/routing.driver.boltkit.it.js b/test/v1/routing.driver.boltkit.it.js new file mode 100644 index 000000000..2cc813ba1 --- /dev/null +++ b/test/v1/routing.driver.boltkit.it.js @@ -0,0 +1,644 @@ +/** + * Copyright (c) 2002-2016 "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 boltkit = require('./boltkit'); +describe('routing driver ', function () { + var originalTimeout; + + beforeAll(function(){ + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + afterAll(function(){ + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + it('should discover server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").then(function () { + + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should discover new servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_new_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").then(function () { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005", "127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should discover new servers using subscribe', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/discover_new_servers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").subscribe({ + onCompleted: function () { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(["127.0.0.1:9004", "127.0.0.1:9002", "127.0.0.1:9003"]); + expect(driver._clusterView.readers.toArray()).toEqual(["127.0.0.1:9005", "127.0.0.1:9003"]); + expect(driver._clusterView.writers.toArray()).toEqual(["127.0.0.1:9001"]); + + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + } + }); + }); + }); + + it('should handle empty response from server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/handle_empty_get_servers_response.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + + // When + var session = driver.session(neo4j.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); + + session.close(); + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }).catch(function (err) { + console.log(err) + }); + }); + }); + + it('should acquire read server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + + session.close(); + + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeTruthy(); + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should pick first available route-server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/short_ttl.script', 9000); + var nextRouter = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9003); + var readServer1 = kit.start('./test/resources/boltkit/read_server.script', 9004); + var readServer2 = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9000", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + + session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + nextRouter.exit(function (code2) { + readServer1.exit(function (code3) { + readServer2.exit(function (code4) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + expect(code4).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + + it('should round-robin among read servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer1 = kit.start('./test/resources/boltkit/read_server.script', 9005); + var readServer2 = kit.start('./test/resources/boltkit/read_server.script', 9006); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + // Then + expect(res.records[0].get('n.name')).toEqual('Bob'); + expect(res.records[1].get('n.name')).toEqual('Alice'); + expect(res.records[2].get('n.name')).toEqual('Tina'); + session.close(); + + driver.close(); + seedServer.exit(function (code1) { + readServer1.exit(function (code2) { + readServer2.exit(function (code3) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should handle missing read server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should acquire write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var writeServer = kit.start('./test/resources/boltkit/write_server.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function () { + + // Then + driver.close(); + seedServer.exit(function (code1) { + writeServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should round-robin among write servers', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer1 = kit.start('./test/resources/boltkit/write_server.script', 9007); + var readServer2 = kit.start('./test/resources/boltkit/write_server.script', 9008); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function () { + session = driver.session(neo4j.session.WRITE); + session.run("CREATE (n {name:'Bob'})").then(function () { + // Then + driver.close(); + seedServer.exit(function (code1) { + readServer1.exit(function (code2) { + readServer2.exit(function (code3) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + expect(code3).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should handle missing write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.WRITE); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should remember endpoints', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function () { + + // Then + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9005', '127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should forget endpoints on failure', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/dead_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").catch(function () { + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeFalsy(); + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should forget endpoints on session acquisition failure', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + session.close(); + // Then + expect(driver._pool.has('127.0.0.1:9001')).toBeTruthy(); + expect(driver._pool.has('127.0.0.1:9005')).toBeFalsy(); + expect(driver._clusterView.routers.toArray()).toEqual(['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003']); + expect(driver._clusterView.readers.toArray()).toEqual(['127.0.0.1:9006']); + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9007', '127.0.0.1:9008']); + driver.close(); + seedServer.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should rediscover if necessary', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/rediscover.script', 9001); + var readServer = kit.start('./test/resources/boltkit/read_server.script', 9005); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + session = driver.session(neo4j.session.READ); + session.run("MATCH (n) RETURN n.name").then(function (res) { + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + }); + + it('should handle server not able to do routing', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var server = kit.start('./test/resources/boltkit/non_discovery.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); + session.close(); + driver.close(); + server.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); + + it('should handle leader switch while writing', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/not_able_to_write.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + session.run("CREATE ()").catch(function (err) { + //the server at 9007 should have been removed + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9008']); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should handle leader switch while writing on transaction', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/acquire_endpoints.script', 9001); + var readServer = kit.start('./test/resources/boltkit/not_able_to_write_in_transaction.script', 9007); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(); + var tx = session.beginTransaction(); + tx.run("CREATE ()"); + + tx.commit().catch(function (err) { + //the server at 9007 should have been removed + expect(driver._clusterView.writers.toArray()).toEqual(['127.0.0.1:9008']); + expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED); + session.close(); + driver.close(); + seedServer.exit(function (code1) { + readServer.exit(function (code2) { + expect(code1).toEqual(0); + expect(code2).toEqual(0); + done(); + }); + }); + }); + }); + }); + + it('should fail if missing write server', function (done) { + if (!boltkit.BoltKitSupport) { + done(); + return; + } + // Given + var kit = new boltkit.BoltKit(); + var seedServer = kit.start('./test/resources/boltkit/no_writers.script', 9001); + + kit.run(function () { + var driver = neo4j.driver("bolt+routing://127.0.0.1:9001", neo4j.auth.basic("neo4j", "neo4j")); + // When + var session = driver.session(neo4j.session.WRITE); + session.run("MATCH (n) RETURN n.name").catch(function (err) { + expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE); + driver.close(); + seedServer.exit(function (code) { + expect(code).toEqual(0); + done(); + }); + }); + }); + }); +}); + diff --git a/test/v1/session.test.js b/test/v1/session.test.js index 6c8ceee76..b53f5261c 100644 --- a/test/v1/session.test.js +++ b/test/v1/session.test.js @@ -19,7 +19,7 @@ var neo4j = require("../../lib/v1"); var StatementType = require("../../lib/v1/result-summary").statementType; -var Session = require("../../lib/v1/session"); +var Session = require("../../lib/v1/session").default; describe('session', function () { diff --git a/test/v1/tck/steps/erroreportingsteps.js b/test/v1/tck/steps/erroreportingsteps.js index 69bcf6c13..fe96a75fd 100644 --- a/test/v1/tck/steps/erroreportingsteps.js +++ b/test/v1/tck/steps/erroreportingsteps.js @@ -36,7 +36,7 @@ module.exports = function () { this.Then(/^it throws a `ClientException`$/, function (table) { var expected = table.rows()[0][0]; - if (this.error === undefined) { + if (!this.error) { throw new Error("Exepcted an error but got none.") } if (this.error.message.indexOf(expected) != 0) { @@ -70,11 +70,12 @@ module.exports = function () { }); this.When(/^I set up a driver with wrong scheme$/, function (callback) { - var self = this; - var driver = neo4j.driver("wrong://localhost:7474", neo4j.auth.basic("neo4j", "neo4j")); - driver.session(); - driver.onError = function (error) { self.error = error; callback()}; - driver.close(); + try { + neo4j.driver("wrong://localhost:7474", neo4j.auth.basic("neo4j", "neo4j")); + } catch (e){ + this.error = e; + callback(); + } }); }; diff --git a/test/v1/tck/steps/matchacceptencesteps.js b/test/v1/tck/steps/matchacceptencesteps.js index 2e460f4a1..10a259cb7 100644 --- a/test/v1/tck/steps/matchacceptencesteps.js +++ b/test/v1/tck/steps/matchacceptencesteps.js @@ -39,7 +39,7 @@ module.exports = function () { this.Then(/^result:$/, function (table, callback) { this.expectedResults = util.literalTableToTestObject(table.hashes()); var self = this; - var errorCallback = function(err) {callback(new Error("Rejected Promise: " + err))} + var errorCallback = function(err) {callback(new Error("Rejected Promise: " + err))}; var successCallback = function(res) { var givenResults = []; var expectedPrint = printable(self.expectedResults); @@ -99,7 +99,7 @@ module.exports = function () { } function getTestObject(rels) { - result = {}; + var result = {}; rels.forEach(function( rel, key ) { if (typeof rel === "object" && rel instanceof Array) { var relArray = []; @@ -120,13 +120,13 @@ module.exports = function () { return val; } var con = val.constructor.name.toLowerCase(); - if (con === NODE) { + if (con === util.NODE) { return stripNode(val); } - else if (con === RELATIONSHIP) { + else if (con === util.RELATIONSHIP) { return stripRelationship(val); } - else if (con === PATH) { + else if (con === util.PATH) { return stripPath(val); } else { @@ -150,8 +150,9 @@ module.exports = function () { var id = 0; var startid = neo4j.int(path.start.identity.toString()); var segments = path.segments; + var segment; for (var i = 0; i < segments.length; i++) { - var segment = segments[i]; + segment = segments[i]; if (startid.notEquals(segment.start.identity)) { throw new Error("Path segment does not make sense") } diff --git a/test/v1/tck/steps/resultapisteps.js b/test/v1/tck/steps/resultapisteps.js index f06ff73c6..d76b64f09 100644 --- a/test/v1/tck/steps/resultapisteps.js +++ b/test/v1/tck/steps/resultapisteps.js @@ -24,7 +24,7 @@ var util = require("./util") module.exports = function () { this.When(/^the `Statement Result` is consumed a `Result Summary` is returned$/, function (callback) { - self = this; + var self = this; this.rc.then(function(res) { self.summary = res.summary; callback(); diff --git a/test/v1/tck/steps/tyepesystemsteps.js b/test/v1/tck/steps/tyepesystemsteps.js index 0116cf7c8..0cd332781 100644 --- a/test/v1/tck/steps/tyepesystemsteps.js +++ b/test/v1/tck/steps/tyepesystemsteps.js @@ -33,19 +33,19 @@ module.exports = function () { this.Given(/^a List of size (\d+) and type (.*)$/, function (size, type) { var list = []; for(var i = 0; i < size; i++ ) { - if (type.toLowerCase() === STRING) { + if (type.toLowerCase() === util.STRING) { list.push(stringOfSize(3)); } - else if (type.toLowerCase() === INT) { + else if (type.toLowerCase() === util.INT) { list.push(randomInt()); } - else if (type.toLowerCase() === BOOL) { + else if (type.toLowerCase() === util.BOOL) { list.push(randomBool()); } - else if (type.toLowerCase() === FLOAT) { + else if (type.toLowerCase() === util.FLOAT) { list.push(randomFloat()); } - else if (type.toLowerCase() === NULL) { + else if (type.toLowerCase() === util.NULL) { list.push(null); } else { @@ -58,19 +58,19 @@ module.exports = function () { this.Given(/^a Map of size (\d+) and type (.*)$/, function (size, type) { var map = {}; for(var i = 0; i < size; i++ ) { - if (type.toLowerCase() === STRING) { + if (type.toLowerCase() === util.STRING) { map["a" + util.sizeOfObject(this.M)] = stringOfSize(3); } - else if (type.toLowerCase() === INT) { + else if (type.toLowerCase() === util.INT) { map["a" + util.sizeOfObject(this.M)] = randomInt(); } - else if (type.toLowerCase() === BOOL) { + else if (type.toLowerCase() === util.BOOL) { map["a" + util.sizeOfObject(this.M)] = randomBool(); } - else if (type.toLowerCase() === FLOAT) { + else if (type.toLowerCase() === util.FLOAT) { map["a" + util.sizeOfObject(this.M)] = randomFloat(); } - else if (type.toLowerCase() === NULL) { + else if (type.toLowerCase() === util.NULL) { map["a" + util.sizeOfObject(this.M)] = null; } else { @@ -208,10 +208,10 @@ module.exports = function () { return f.replace("+", ""); } else if( jsVal === undefined || jsVal === null) { - return 'Null' + return 'Null'; } else if( typeof jsVal === "object" && jsVal instanceof Array ) { - var list = "[" + var list = "["; for(var i = 0; i < jsVal.length; i++ ) { list += jsToCypherLiteral(jsVal[i]); if ( i < jsVal.length -1) { diff --git a/test/v1/tck/steps/util.js b/test/v1/tck/steps/util.js index d9cccf4d9..cd0fc9aac 100644 --- a/test/v1/tck/steps/util.js +++ b/test/v1/tck/steps/util.js @@ -19,14 +19,14 @@ var neo4j = require("../../../../lib/v1"); -INT = 'integer'; -FLOAT = 'float'; -STRING = 'string'; -BOOL = 'boolean'; -NULL = 'null'; -RELATIONSHIP = 'relationship'; -NODE = 'node'; -PATH = 'path'; +const INT = 'integer'; +const FLOAT = 'float'; +const STRING = 'string'; +const BOOL = 'boolean'; +const NULL = 'null'; +const RELATIONSHIP = 'relationship'; +const NODE = 'node'; +const PATH = 'path'; var neorunPath = './neokit/neorun.py'; var neo4jHome = './build/neo4j'; @@ -35,19 +35,6 @@ var neo4jKey = neo4jHome + '/certificates/neo4j.key'; var childProcess = require("child_process"); var fs = require('fs'); -module.exports = { - literalTableToTestObject: literalTableToTestObject, - literalValueToTestValueNormalIntegers : literalValueToTestValueNormalIntegers, - literalValueToTestValue: literalValueToTestValue, - compareValues: compareValues, - sizeOfObject: sizeOfObject, - clone: clone, - printable: printable, - changeCertificates: changeCertificates, - restart: restart, - neo4jCert: neo4jCert -}; - function literalTableToTestObject(literalResults) { var resultValues = []; for ( var i = 0 ; i < literalResults.length ; i++) { @@ -57,7 +44,7 @@ function literalTableToTestObject(literalResults) { } function literalLineToObjects(resultRow) { - resultObject = {}; + var resultObject = {}; for ( var key in resultRow) { resultObject[key] = literalValueToTestValue(resultRow[key]); @@ -177,7 +164,7 @@ function createPath(val) { path.segments = []; if (entities.length > 2) { for (var i = 0; i < entities.length-2; i+=2) { - segment = {"start": entities[i], + var segment = {"start": entities[i], "end": entities[i+2], "relationship": entities[i+1]}; path.segments.push(segment); @@ -206,7 +193,7 @@ function parseNodesAndRelationshipsFromPath(val) { function parseMap(val, bigInt) { if (bigInt) { - return properties = JSON.parse(val, function(k, v) { + return JSON.parse(val, function(k, v) { if (Number.isInteger(v)) { return neo4j.int(v); } @@ -214,7 +201,7 @@ function parseMap(val, bigInt) { }); } else { - return properties = JSON.parse(val); + return JSON.parse(val); } } @@ -256,7 +243,7 @@ function getLabels(val) { if ( val.indexOf(":") < 0) { return []; } - labels = val.split(":") + var labels = val.split(":") labels.splice(0, labels.length-1); return labels; } @@ -384,3 +371,24 @@ var runScript = function(cmd) { throw "Script finished with code " + code } }; + +module.exports = { + literalTableToTestObject: literalTableToTestObject, + literalValueToTestValueNormalIntegers : literalValueToTestValueNormalIntegers, + literalValueToTestValue: literalValueToTestValue, + compareValues: compareValues, + sizeOfObject: sizeOfObject, + clone: clone, + printable: printable, + changeCertificates: changeCertificates, + restart: restart, + neo4jCert: neo4jCert, + INT: INT, + FLOAT: FLOAT, + STRING: STRING, + BOOL: BOOL, + NULL: NULL, + NODE: NODE, + RELATIONSHIP: RELATIONSHIP, + PATH: PATH +}; diff --git a/test/v1/transaction.test.js b/test/v1/transaction.test.js index 9a9b9ba73..b2aa334f3 100644 --- a/test/v1/transaction.test.js +++ b/test/v1/transaction.test.js @@ -21,10 +21,13 @@ var neo4j = require("../../lib/v1"); describe('transaction', function() { - var driver, session; + var driver, session, server; beforeEach(function(done) { driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j")); + driver.onCompleted = function (meta) { + server = meta['server']; + }; session = driver.session(); session.run("MATCH (n) DETACH DELETE n").then(done); @@ -49,7 +52,7 @@ describe('transaction', function() { expect(result.records[0].get('count(t2)').toInt()) .toBe(1); done(); - }); + }).catch(function (e) {console.log(e)}); }); }); @@ -162,7 +165,7 @@ describe('transaction', function() { // When var tx = session.beginTransaction(); tx.run("CREATE (:TXNode1)"); - tx.rollback() + tx.rollback(); tx.commit() .catch(function (error) { @@ -214,4 +217,23 @@ describe('transaction', function() { done(); }); }); + + it('should provide bookmark on commit', function (done) { + //bookmarking is not in 3.0 + if (!server) { + done(); + return; + } + + // When + var tx = session.beginTransaction(); + expect(session.lastBookmark()).not.toBeDefined(); + tx.run("CREATE (:TXNode1)"); + tx.run("CREATE (:TXNode2)"); + tx.commit() + .then(function () { + expect(session.lastBookmark()).toBeDefined(); + done(); + }); + }); });