diff --git a/src/driver.js b/src/driver.js index 8c193589a..65c1dd33e 100644 --- a/src/driver.js +++ b/src/driver.js @@ -103,6 +103,19 @@ class Driver { return connectivityVerifier.verify({ database }) } + /** + * Returns whether the server supports multi database capabilities based on the handshaked protocol + * version. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsMultiDb () { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsMultiDb() + } + /** * Acquire a session to communicate with the database. The session will * borrow connections from the underlying connection pool as required and diff --git a/src/internal/bolt-protocol-v1.js b/src/internal/bolt-protocol-v1.js index 6b2d3de89..14228e4ed 100644 --- a/src/internal/bolt-protocol-v1.js +++ b/src/internal/bolt-protocol-v1.js @@ -16,24 +16,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import RequestMessage from './request-message' -import * as v1 from './packstream-v1' -import Bookmark from './bookmark' -import TxConfig from './tx-config' -import { ACCESS_MODE_WRITE } from './constants' -import Connection from './connection' -import { Chunker } from './chunking' -import { Packer } from './packstream-v1' import { assertDatabaseIsEmpty, assertTxConfigIsEmpty } from './bolt-protocol-util' +import Bookmark from './bookmark' +import { Chunker } from './chunking' +import Connection from './connection' +import { ACCESS_MODE_WRITE, BOLT_PROTOCOL_V1 } from './constants' +import * as v1 from './packstream-v1' +import { Packer } from './packstream-v1' +import RequestMessage from './request-message' import { - ResultStreamObserver, LoginObserver, ResetObserver, + ResultStreamObserver, StreamObserver } from './stream-observers' +import TxConfig from './tx-config' export default class BoltProtocol { /** @@ -48,6 +48,13 @@ export default class BoltProtocol { this._unpacker = this._createUnpacker(disableLosslessIntegers) } + /** + * Returns the numerical version identifier for this protocol + */ + get version () { + return BOLT_PROTOCOL_V1 + } + /** * Get the packer. * @return {Packer} the protocol's packer. diff --git a/src/internal/bolt-protocol-v2.js b/src/internal/bolt-protocol-v2.js index 4b8846301..a22a87d31 100644 --- a/src/internal/bolt-protocol-v2.js +++ b/src/internal/bolt-protocol-v2.js @@ -18,6 +18,7 @@ */ import BoltProtocolV1 from './bolt-protocol-v1' import * as v2 from './packstream-v2' +import { BOLT_PROTOCOL_V2 } from './constants' export default class BoltProtocol extends BoltProtocolV1 { _createPacker (chunker) { @@ -27,4 +28,8 @@ export default class BoltProtocol extends BoltProtocolV1 { _createUnpacker (disableLosslessIntegers) { return new v2.Unpacker(disableLosslessIntegers) } + + get version () { + return BOLT_PROTOCOL_V2 + } } diff --git a/src/internal/bolt-protocol-v3.js b/src/internal/bolt-protocol-v3.js index 33bdb8ec6..34581dd62 100644 --- a/src/internal/bolt-protocol-v3.js +++ b/src/internal/bolt-protocol-v3.js @@ -24,10 +24,15 @@ import { LoginObserver, ResultStreamObserver } from './stream-observers' +import { BOLT_PROTOCOL_V3 } from './constants' const noOpObserver = new StreamObserver() export default class BoltProtocol extends BoltProtocolV2 { + get version () { + return BOLT_PROTOCOL_V3 + } + transformMetadata (metadata) { if ('t_first' in metadata) { // Bolt V3 uses shorter key 't_first' to represent 'result_available_after' diff --git a/src/internal/bolt-protocol-v4.js b/src/internal/bolt-protocol-v4.js index 4a48476a5..4dd2d00bf 100644 --- a/src/internal/bolt-protocol-v4.js +++ b/src/internal/bolt-protocol-v4.js @@ -19,8 +19,13 @@ import BoltProtocolV3 from './bolt-protocol-v3' import RequestMessage from './request-message' import { ResultStreamObserver } from './stream-observers' +import { BOLT_PROTOCOL_V4 } from './constants' export default class BoltProtocol extends BoltProtocolV3 { + get version () { + return BOLT_PROTOCOL_V4 + } + beginTransaction ({ bookmark, txConfig, diff --git a/src/internal/connection-provider-direct.js b/src/internal/connection-provider-direct.js index 42919f470..723945e1a 100644 --- a/src/internal/connection-provider-direct.js +++ b/src/internal/connection-provider-direct.js @@ -19,6 +19,8 @@ import PooledConnectionProvider from './connection-provider-pooled' import DelegateConnection from './connection-delegate' +import ChannelConnection from './connection-channel' +import { BOLT_PROTOCOL_V4 } from './constants' export default class DirectConnectionProvider extends PooledConnectionProvider { constructor ({ id, config, log, address, userAgent, authToken }) { @@ -36,4 +38,26 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { .acquire(this._address) .then(connection => new DelegateConnection(connection, null)) } + + async supportsMultiDb () { + const connection = ChannelConnection.create( + this._address, + this._config, + this._createConnectionErrorHandler(), + this._log + ) + + try { + await connection._negotiateProtocol() + + const protocol = connection.protocol() + if (protocol) { + return protocol.version >= BOLT_PROTOCOL_V4 + } + + return false + } finally { + await connection.close() + } + } } diff --git a/src/internal/connection-provider-routing.js b/src/internal/connection-provider-routing.js index 0a985fb7c..f48b2faa9 100644 --- a/src/internal/connection-provider-routing.js +++ b/src/internal/connection-provider-routing.js @@ -31,12 +31,16 @@ import ConnectionErrorHandler from './connection-error-handler' import DelegateConnection from './connection-delegate' import LeastConnectedLoadBalancingStrategy from './least-connected-load-balancing-strategy' import Bookmark from './bookmark' +import ChannelConnection from './connection-channel' +import { int } from '../integer' +import { BOLT_PROTOCOL_V4 } from './constants' const UNAUTHORIZED_ERROR_CODE = 'Neo.ClientError.Security.Unauthorized' const DATABASE_NOT_FOUND_ERROR_CODE = 'Neo.ClientError.Database.DatabaseNotFound' const SYSTEM_DB_NAME = 'system' const DEFAULT_DB_NAME = '' +const DEFAULT_ROUTING_TABLE_PURGE_DELAY = int(30000) export default class RoutingConnectionProvider extends PooledConnectionProvider { constructor ({ @@ -47,7 +51,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider config, log, userAgent, - authToken + authToken, + routingTablePurgeDelay }) { super({ id, config, log, userAgent, authToken }) @@ -61,6 +66,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._dnsResolver = new HostNameResolver() this._log = log this._useSeedRouter = true + this._routingTablePurgeDelay = routingTablePurgeDelay + ? int(routingTablePurgeDelay) + : DEFAULT_ROUTING_TABLE_PURGE_DELAY } _createConnectionErrorHandler () { @@ -71,11 +79,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider _handleUnavailability (error, address, database) { this._log.warn( - `Routing driver ${ - this._id - } will forget ${address} for database '${database}' because of an error ${ - error.code - } '${error.message}'` + `Routing driver ${this._id} will forget ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) this.forget(address, database || '') return error @@ -83,11 +87,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider _handleWriteFailure (error, address, database) { this._log.warn( - `Routing driver ${ - this._id - } will forget writer ${address} for database '${database}' because of an error ${ - error.code - } '${error.message}'` + `Routing driver ${this._id} will forget writer ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) this.forgetWriter(address, database || '') return newError( @@ -152,6 +152,41 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider } } + async supportsMultiDb () { + const addresses = await this._resolveSeedRouter(this._seedRouter) + + let lastError + for (let i = 0; i < addresses.length; i++) { + const connection = ChannelConnection.create( + addresses[i], + this._config, + this._createConnectionErrorHandler(), + this._log + ) + + try { + await connection._negotiateProtocol() + + const protocol = connection.protocol() + if (protocol) { + return protocol.version >= BOLT_PROTOCOL_V4 + } + + return false + } catch (error) { + lastError = error + } finally { + await connection.close() + } + } + + if (lastError) { + throw lastError + } + + return false + } + forget (address, database) { if (database || database === '') { this._routingTables[database].forget(address) @@ -427,6 +462,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider // close old connections to servers not present in the new routing table await this._connectionPool.keepAll(newRoutingTable.allServers()) + // filter out expired to purge (expired for a pre-configured amount of time) routing table entries + Object.values(this._routingTables).forEach(value => { + if (value.isExpiredFor(this._routingTablePurgeDelay)) { + delete this._routingTables[value.database] + } + }) + // make this driver instance aware of the new table this._routingTables[newRoutingTable.database] = newRoutingTable this._log.info(`Updated routing table ${newRoutingTable}`) diff --git a/src/internal/connection-provider.js b/src/internal/connection-provider.js index 12782d4bc..304cc1d60 100644 --- a/src/internal/connection-provider.js +++ b/src/internal/connection-provider.js @@ -38,6 +38,16 @@ export default class ConnectionProvider { throw new Error('not implemented') } + /** + * This method checks whether the backend database supports multi database functionality + * by checking protocol handshake result. + * + * @returns {Promise} + */ + supportsMultiDb () { + throw new Error('not implemented') + } + /** * Closes this connection provider along with its internals (connections, pools, etc.) * diff --git a/src/internal/constants.js b/src/internal/constants.js index 4dade13df..0a1355b09 100644 --- a/src/internal/constants.js +++ b/src/internal/constants.js @@ -20,4 +20,16 @@ const ACCESS_MODE_READ = 'READ' const ACCESS_MODE_WRITE = 'WRITE' -export { ACCESS_MODE_READ, ACCESS_MODE_WRITE } +const BOLT_PROTOCOL_V1 = 1 +const BOLT_PROTOCOL_V2 = 2 +const BOLT_PROTOCOL_V3 = 3 +const BOLT_PROTOCOL_V4 = 4 + +export { + ACCESS_MODE_READ, + ACCESS_MODE_WRITE, + BOLT_PROTOCOL_V1, + BOLT_PROTOCOL_V2, + BOLT_PROTOCOL_V3, + BOLT_PROTOCOL_V4 +} diff --git a/src/internal/routing-table.js b/src/internal/routing-table.js index ce12ec6f5..bc00d66e4 100644 --- a/src/internal/routing-table.js +++ b/src/internal/routing-table.js @@ -24,6 +24,7 @@ const MIN_ROUTERS = 1 export default class RoutingTable { constructor ({ database, routers, readers, writers, expirationTime } = {}) { this.database = database + this.databaseName = database || 'default database' this.routers = routers || [] this.readers = readers || [] this.writers = writers || [] @@ -61,14 +62,24 @@ export default class RoutingTable { ) } + /** + * Check if this routing table is expired for specified amount of duration + * + * @param {Integer} duration amount of duration in milliseconds to check for expiration + * @returns {boolean} + */ + isExpiredFor (duration) { + return this.expirationTime.add(duration).lessThan(Date.now()) + } + allServers () { return [...this.routers, ...this.readers, ...this.writers] } toString () { return ( - `RoutingTable[` + - `database=${this.database}, ` + + 'RoutingTable[' + + `database=${this.databaseName}, ` + `expirationTime=${this.expirationTime}, ` + `currentTime=${Date.now()}, ` + `routers=[${this.routers}], ` + diff --git a/test/internal/bolt-protocol-v1.test.js b/test/internal/bolt-protocol-v1.test.js index 0e88d3bf2..c71216cd9 100644 --- a/test/internal/bolt-protocol-v1.test.js +++ b/test/internal/bolt-protocol-v1.test.js @@ -57,7 +57,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const onError = error => {} + const onError = _error => {} const onComplete = () => {} const clientName = 'js-driver/1.2.3' const authToken = { username: 'neo4j', password: 'secret' } @@ -166,6 +166,12 @@ describe('#unit BoltProtocolV1', () => { expect(recorder.flushes).toEqual([false, true]) }) + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV1(null, null, false) + + expect(protocol.version).toBe(1) + }) + describe('Bolt V3', () => { /** * @param {function(protocol: BoltProtocolV1)} fn @@ -202,7 +208,7 @@ describe('#unit BoltProtocolV1', () => { describe('run', () => { function verifyRun (txConfig) { - verifyError((protocol, observer) => + verifyError((protocol, _observer) => protocol.run('statement', {}, { txConfig }) ) } diff --git a/test/internal/bolt-protocol-v2.test.js b/test/internal/bolt-protocol-v2.test.js new file mode 100644 index 000000000..a783b17d3 --- /dev/null +++ b/test/internal/bolt-protocol-v2.test.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV2 from '../../src/internal/bolt-protocol-v2' +import utils from './test-utils' + +describe('#unit BoltProtocolV2', () => { + beforeEach(() => { + jasmine.addMatchers(utils.matchers) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV2(null, null, false) + + expect(protocol.version).toBe(2) + }) +}) diff --git a/test/internal/bolt-protocol-v3.test.js b/test/internal/bolt-protocol-v3.test.js index 2c4471f72..4cd2cb648 100644 --- a/test/internal/bolt-protocol-v3.test.js +++ b/test/internal/bolt-protocol-v3.test.js @@ -145,6 +145,12 @@ describe('#unit BoltProtocolV3', () => { expect(recorder.flushes).toEqual([true]) }) + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV3(null, null, false) + + expect(protocol.version).toBe(3) + }) + describe('Bolt V4', () => { /** * @param {function(protocol: BoltProtocolV3)} fn diff --git a/test/internal/bolt-protocol-v4.test.js b/test/internal/bolt-protocol-v4.test.js index a5a3bfa0c..4a2714968 100644 --- a/test/internal/bolt-protocol-v4.test.js +++ b/test/internal/bolt-protocol-v4.test.js @@ -94,4 +94,10 @@ describe('#unit BoltProtocolV4', () => { expect(recorder.observers).toEqual([observer]) expect(recorder.flushes).toEqual([true]) }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV4(null, null, false) + + expect(protocol.version).toBe(4) + }) }) diff --git a/test/internal/connection-provider-routing.test.js b/test/internal/connection-provider-routing.test.js index ca66bd712..f752dba4f 100644 --- a/test/internal/connection-provider-routing.test.js +++ b/test/internal/connection-provider-routing.test.js @@ -1723,6 +1723,92 @@ describe('#unit RoutingConnectionProvider', () => { [serverC] ) }) + + it('should purge expired routing tables after specified duration on update', async () => { + var originalDateNow = Date.now + Date.now = () => 50000 + try { + const routingTableToLoad = newRoutingTable( + 'databaseC', + [server1, server2, server3], + [server2, server3], + [server1] + ) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server1, + [server1], + [ + newRoutingTable( + 'databaseA', + [server1, server2, server3], + [server1, server2], + [server3], + int(Date.now() + 12000) + ), + newRoutingTable( + 'databaseB', + [server1, server2, server3], + [server1, server3], + [server2], + int(Date.now() + 2000) + ) + ], + { + databaseC: { + 'server1:7687': routingTableToLoad + } + }, + null, + 4000 + ) + + expectRoutingTable( + connectionProvider, + 'databaseA', + [server1, server2, server3], + [server1, server2], + [server3] + ) + expectRoutingTable( + connectionProvider, + 'databaseB', + [server1, server2, server3], + [server1, server3], + [server2] + ) + + // make routing table for databaseA to report true for isExpiredFor(4000) + // call. + Date.now = () => 58000 + + // force a routing table update for databaseC + const conn1 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseC' + }) + expect(conn1).not.toBeNull() + expect(conn1.address).toBe(server1) + + // Then + expectRoutingTable( + connectionProvider, + 'databaseA', + [server1, server2, server3], + [server1, server2], + [server3] + ) + expectRoutingTable( + connectionProvider, + 'databaseC', + [server1, server2, server3], + [server2, server3], + [server1] + ) + expectNoRoutingTable(connectionProvider, 'databaseB') + } finally { + Date.now = originalDateNow + } + }) }) }) @@ -1746,7 +1832,8 @@ function newRoutingConnectionProviderWithSeedRouter ( seedRouterResolved, routingTables, routerToRoutingTable = { '': {} }, - connectionPool = null + connectionPool = null, + routingTablePurgeDelay = null ) { const pool = connectionPool || newPool() const connectionProvider = new RoutingConnectionProvider({ @@ -1755,7 +1842,8 @@ function newRoutingConnectionProviderWithSeedRouter ( routingContext: {}, hostNameResolver: new SimpleHostNameResolver(), config: {}, - log: Logger.noOp() + log: Logger.noOp(), + routingTablePurgeDelay: routingTablePurgeDelay }) connectionProvider._connectionPool = pool routingTables.forEach(r => { @@ -1821,6 +1909,10 @@ function expectRoutingTable ( expect(connectionProvider._routingTables[database].writers).toEqual(writers) } +function expectNoRoutingTable (connectionProvider, database) { + expect(connectionProvider._routingTables[database]).toBeFalsy() +} + function expectPoolToContain (pool, addresses) { addresses.forEach(address => { expect(pool.has(address)).toBeTruthy() diff --git a/test/internal/node/direct.driver.boltkit.test.js b/test/internal/node/direct.driver.boltkit.test.js index 56825b286..90b745fa7 100644 --- a/test/internal/node/direct.driver.boltkit.test.js +++ b/test/internal/node/direct.driver.boltkit.test.js @@ -411,6 +411,42 @@ describe('#stub-direct direct driver with stub server', () => { it('v3', () => verifyFailureOnCommit('v3')) }) + describe('should report whether multi db is supported', () => { + async function verifySupportsMultiDb (version, expected) { + if (!boltStub.supported) { + return + } + + const server = await boltStub.start( + `./test/resources/boltstub/${version}/supports_multi_db.script`, + 9001 + ) + + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + + await expectAsync(driver.supportsMultiDb()).toBeResolvedTo(expected) + + await driver.close() + await server.exit() + } + + it('v1', () => verifySupportsMultiDb('v1', false)) + it('v2', () => verifySupportsMultiDb('v2', false)) + it('v3', () => verifySupportsMultiDb('v3', false)) + it('v4', () => verifySupportsMultiDb('v4', true)) + it('on error', async () => { + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + + await expectAsync(driver.supportsMultiDb()).toBeRejectedWith( + jasmine.objectContaining({ + code: SERVICE_UNAVAILABLE + }) + ) + + await driver.close() + }) + }) + function connectionPool (driver, key) { return driver._connectionProvider._connectionPool._pools[key] } diff --git a/test/internal/node/routing.driver.boltkit.test.js b/test/internal/node/routing.driver.boltkit.test.js index 1f0efe2b9..6c77e9c09 100644 --- a/test/internal/node/routing.driver.boltkit.test.js +++ b/test/internal/node/routing.driver.boltkit.test.js @@ -1822,7 +1822,7 @@ describe('#stub-routing routing driver with stub server', () => { const session = driver.session({ defaultAccessMode: WRITE, bookmarks }) const tx = session.beginTransaction() - await tx.run(`CREATE (n {name:'Bob'})`) + await tx.run("CREATE (n {name:'Bob'})") await tx.commit() expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx95']) @@ -1835,14 +1835,14 @@ describe('#stub-routing routing driver with stub server', () => { it('should forget writer on database unavailable error', () => testAddressPurgeOnDatabaseError( './test/resources/boltstub/v3/write_database_unavailable.script', - `CREATE (n {name:'Bob'})`, + "CREATE (n {name:'Bob'})", WRITE )) it('should forget reader on database unavailable error', () => testAddressPurgeOnDatabaseError( './test/resources/boltstub/v3/read_database_unavailable.script', - `RETURN 1`, + 'RETURN 1', READ )) @@ -2196,7 +2196,7 @@ describe('#stub-routing routing driver with stub server', () => { // Given const server = await boltStub.start( - `./test/resources/boltstub/v4/acquire_endpoints_db_not_found.script`, + './test/resources/boltstub/v4/acquire_endpoints_db_not_found.script', 9001 ) @@ -2223,15 +2223,15 @@ describe('#stub-routing routing driver with stub server', () => { // Given const router1 = await boltStub.start( - `./test/resources/boltstub/v4/acquire_endpoints_aDatabase_no_servers.script`, + './test/resources/boltstub/v4/acquire_endpoints_aDatabase_no_servers.script', 9001 ) const router2 = await boltStub.start( - `./test/resources/boltstub/v4/acquire_endpoints_aDatabase.script`, + './test/resources/boltstub/v4/acquire_endpoints_aDatabase.script', 9002 ) const reader1 = await boltStub.start( - `./test/resources/boltstub/v4/read_from_aDatabase.script`, + './test/resources/boltstub/v4/read_from_aDatabase.script', 9005 ) @@ -2272,7 +2272,7 @@ describe('#stub-routing routing driver with stub server', () => { 9001 ) const readServer = await boltStub.start( - `./test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script`, + './test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script', 9005 ) @@ -2307,7 +2307,7 @@ describe('#stub-routing routing driver with stub server', () => { 9001 ) const readServer = await boltStub.start( - `./test/resources/boltstub/v3/read_with_bookmark.script`, + './test/resources/boltstub/v3/read_with_bookmark.script', 9005 ) @@ -2331,6 +2331,70 @@ describe('#stub-routing routing driver with stub server', () => { }) }) + describe('should report whether multi db is supported', () => { + async function verifySupportsMultiDb (version, expected) { + if (!boltStub.supported) { + return + } + + const server = await boltStub.start( + `./test/resources/boltstub/${version}/supports_multi_db.script`, + 9001 + ) + + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + await expectAsync(driver.supportsMultiDb()).toBeResolvedTo(expected) + + await driver.close() + await server.exit() + } + + async function verifySupportsMultiDbWithResolver (version, expected) { + if (!boltStub.supported) { + return + } + + const server = await boltStub.start( + `./test/resources/boltstub/${version}/supports_multi_db.script`, + 9001 + ) + + const driver = boltStub.newDriver('neo4j://127.0.0.1:8000', { + resolver: address => [ + 'neo4j://127.0.0.1:9010', + 'neo4j://127.0.0.1:9005', + 'neo4j://127.0.0.1:9001' + ] + }) + + await expectAsync(driver.supportsMultiDb()).toBeResolvedTo(expected) + + await driver.close() + await server.exit() + } + + it('v1', () => verifySupportsMultiDb('v1', false)) + it('v2', () => verifySupportsMultiDb('v2', false)) + it('v3', () => verifySupportsMultiDb('v3', false)) + it('v4', () => verifySupportsMultiDb('v4', true)) + it('v1 with resolver', () => verifySupportsMultiDbWithResolver('v1', false)) + it('v2 with resolver', () => verifySupportsMultiDbWithResolver('v2', false)) + it('v3 with resolver', () => verifySupportsMultiDbWithResolver('v3', false)) + it('v4 with resolver', () => verifySupportsMultiDbWithResolver('v4', true)) + it('on error', async () => { + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + await expectAsync(driver.supportsMultiDb()).toBeRejectedWith( + jasmine.objectContaining({ + code: SESSION_EXPIRED + }) + ) + + await driver.close() + }) + }) + async function testAddressPurgeOnDatabaseError (script, query, accessMode) { if (!boltStub.supported) { return diff --git a/test/internal/routing-table.test.js b/test/internal/routing-table.test.js index 6ce0ff104..f89ec41d6 100644 --- a/test/internal/routing-table.test.js +++ b/test/internal/routing-table.test.js @@ -201,8 +201,29 @@ describe('#unit RoutingTable', () => { } }) - function expired () { - return Date.now() - 3600 // expired an hour ago + it('should report correct value when expired for is tested', () => { + const originalDateNow = Date.now + try { + Date.now = () => 50000 + const table = createTable( + [server1, server2, server3], + [server2, server1, server5], + [server5, server1], + expired(7200) + ) + + expect(table.isStaleFor(READ)).toBeTruthy() + expect(table.isStaleFor(WRITE)).toBeTruthy() + + expect(table.isExpiredFor(3600)).toBeTruthy() + expect(table.isExpiredFor(10800)).toBeFalsy() + } finally { + Date.now = originalDateNow + } + }) + + function expired (expiredFor) { + return Date.now() - (expiredFor || 3600) // expired an hour ago } function notExpired () { diff --git a/test/resources/boltstub/v1/supports_multi_db.script b/test/resources/boltstub/v1/supports_multi_db.script new file mode 100644 index 000000000..daa154eac --- /dev/null +++ b/test/resources/boltstub/v1/supports_multi_db.script @@ -0,0 +1 @@ +!: BOLT 1 \ No newline at end of file diff --git a/test/resources/boltstub/v2/supports_multi_db.script b/test/resources/boltstub/v2/supports_multi_db.script new file mode 100644 index 000000000..d80a38b89 --- /dev/null +++ b/test/resources/boltstub/v2/supports_multi_db.script @@ -0,0 +1 @@ +!: BOLT 2 \ No newline at end of file diff --git a/test/resources/boltstub/v3/supports_multi_db.script b/test/resources/boltstub/v3/supports_multi_db.script new file mode 100644 index 000000000..325e2d82a --- /dev/null +++ b/test/resources/boltstub/v3/supports_multi_db.script @@ -0,0 +1 @@ +!: BOLT 3 \ No newline at end of file diff --git a/test/resources/boltstub/v4/supports_multi_db.script b/test/resources/boltstub/v4/supports_multi_db.script new file mode 100644 index 000000000..ff3657200 --- /dev/null +++ b/test/resources/boltstub/v4/supports_multi_db.script @@ -0,0 +1 @@ +!: BOLT 4 \ No newline at end of file diff --git a/test/rx/summary.test.js b/test/rx/summary.test.js index b694a420f..bb3239058 100644 --- a/test/rx/summary.test.js +++ b/test/rx/summary.test.js @@ -578,7 +578,7 @@ describe('#integration-rx summary', () => { 'The provided label is not in the database.' ) expect(summary.notifications[0].description).toBe( - 'One of the labels in your query is not available in the database, make sure you didn\'t misspell it or that the label is available when you run this statement in your application (the missing label name is: ThisLabelDoesNotExist)' + "One of the labels in your query is not available in the database, make sure you didn't misspell it or that the label is available when you run this statement in your application (the missing label name is: ThisLabelDoesNotExist)" ) expect(summary.notifications[0].severity).toBe('WARNING') } @@ -652,20 +652,25 @@ describe('#integration-rx summary', () => { } async function dropConstraintsAndIndices (driver) { + function getName (record) { + const obj = record.toObject() + const name = obj.description || obj.name + if (!name) { + throw new Error('unable to identify name of the constraint/index') + } + return name + } + const session = driver.session() try { - const constraints = await session.run( - "CALL db.constraints() yield description RETURN 'DROP ' + description" - ) + const constraints = await session.run('CALL db.constraints()') for (let i = 0; i < constraints.records.length; i++) { - await session.run(constraints.records[0].get(0)) + await session.run(`DROP ${getName(constraints.records[i])}`) } - const indices = await session.run( - "CALL db.indexes() yield description RETURN 'DROP ' + description" - ) + const indices = await session.run('CALL db.indexes()') for (let i = 0; i < indices.records.length; i++) { - await session.run(indices.records[0].get(0)) + await session.run(`DROP ${getName(indices.records[i])}`) } } finally { await session.close() diff --git a/test/session.test.js b/test/session.test.js index 0e6cfb782..4829fcf65 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -171,7 +171,7 @@ describe('#integration session', () => { it('should accept a statement object ', done => { // Given const statement = { - text: 'RETURN 1 = {param} AS a', + text: 'RETURN 1 = $param AS a', parameters: { param: 1 } } @@ -215,7 +215,7 @@ describe('#integration session', () => { it('should expose summarize method for basic metadata ', done => { // Given - const statement = 'CREATE (n:Label {prop:{prop}}) RETURN n' + const statement = 'CREATE (n:Label {prop: $prop}) RETURN n' const params = { prop: 'string' } // When & Then session.run(statement, params).then(result => { @@ -269,7 +269,7 @@ describe('#integration session', () => { it('should expose plan ', done => { // Given - const statement = 'EXPLAIN CREATE (n:Label {prop:{prop}}) RETURN n' + const statement = 'EXPLAIN CREATE (n:Label {prop: $prop}) RETURN n' const params = { prop: 'string' } // When & Then session.run(statement, params).then(result => { @@ -286,7 +286,7 @@ describe('#integration session', () => { it('should expose profile ', done => { // Given - const statement = 'PROFILE MATCH (n:Label {prop:{prop}}) RETURN n' + const statement = 'PROFILE MATCH (n:Label {prop: $prop}) RETURN n' const params = { prop: 'string' } // When & Then session.run(statement, params).then(result => { @@ -403,7 +403,7 @@ describe('#integration session', () => { throw Error() } - const statement = 'RETURN {param}' + const statement = 'RETURN $param' const params = { param: unpackable } // When & Then session.run(statement, params).catch(ignore => { @@ -883,20 +883,20 @@ describe('#integration session', () => { it('should be able to do nested queries', done => { session .run( - 'CREATE (knight:Person:Knight {name: {name1}, castle: {castle}})' + - 'CREATE (king:Person {name: {name2}, title: {title}})', + 'CREATE (knight:Person:Knight {name: $name1, castle: $castle})' + + 'CREATE (king:Person {name: $name2, title: $title})', { name1: 'Lancelot', castle: 'Camelot', name2: 'Arthur', title: 'King' } ) .then(() => { session .run( - 'MATCH (knight:Person:Knight) WHERE knight.castle = {castle} RETURN id(knight) AS knight_id', + 'MATCH (knight:Person:Knight) WHERE knight.castle = $castle RETURN id(knight) AS knight_id', { castle: 'Camelot' } ) .subscribe({ onNext: record => { session.run( - 'MATCH (knight) WHERE id(knight) = {id} MATCH (king:Person) WHERE king.name = {king} CREATE (knight)-[:DEFENDS]->(king)', + 'MATCH (knight) WHERE id(knight) = $id MATCH (king:Person) WHERE king.name = $king CREATE (knight)-[:DEFENDS]->(king)', { id: record.get('knight_id'), king: 'Arthur' } ) }, diff --git a/test/spatial-types.test.js b/test/spatial-types.test.js index 3168ec0c7..5f9c8b2be 100644 --- a/test/spatial-types.test.js +++ b/test/spatial-types.test.js @@ -110,7 +110,7 @@ describe('#integration spatial-types', () => { it('should receive 2D points with crs', done => { testReceivingOfPoints( done, - `RETURN point({x: 2.3, y: 4.5, crs: 'WGS-84'})`, + 'RETURN point({x: 2.3, y: 4.5, crs: \'WGS-84\'})', point => { expect(isPoint(point)).toBeTruthy() expect(point.srid).toEqual(WGS_84_2D_CRS_CODE) @@ -138,7 +138,7 @@ describe('#integration spatial-types', () => { it('should receive 3D points with crs', done => { testReceivingOfPoints( done, - `RETURN point({x: 34.76, y: 11.9, z: -99.01, crs: 'WGS-84-3D'})`, + 'RETURN point({x: 34.76, y: 11.9, z: -99.01, crs: \'WGS-84-3D\'})', point => { expect(isPoint(point)).toBeTruthy() expect(point.srid).toEqual(WGS_84_3D_CRS_CODE) @@ -166,10 +166,10 @@ describe('#integration spatial-types', () => { it('should send and receive array of 2D points', done => { const arrayOfPoints = [ new Point(WGS_84_2D_CRS_CODE, 12.3, 11.2), - new Point(WGS_84_2D_CRS_CODE, 2.45, 91.302), - new Point(WGS_84_2D_CRS_CODE, 0.12, -99.9), - new Point(WGS_84_2D_CRS_CODE, 93.75, 123.213), - new Point(WGS_84_2D_CRS_CODE, 111.13, -90.1), + new Point(WGS_84_2D_CRS_CODE, 2.45, 81.302), + new Point(WGS_84_2D_CRS_CODE, 0.12, -89.9), + new Point(WGS_84_2D_CRS_CODE, 93.75, 23.213), + new Point(WGS_84_2D_CRS_CODE, 111.13, -70.1), new Point(WGS_84_2D_CRS_CODE, 43.99, -1) ] diff --git a/test/stress.test.js b/test/stress.test.js index 8547217ba..99f0a4172 100644 --- a/test/stress.test.js +++ b/test/stress.test.js @@ -40,7 +40,7 @@ describe('#integration stress tests', () => { const READ_QUERY = 'MATCH (n) RETURN n LIMIT 1' const WRITE_QUERY = - 'CREATE (person:Person:Employee {name: {name}, salary: {salary}}) RETURN person' + 'CREATE (person:Person:Employee {name: $name, salary: $salary}) RETURN person' const TEST_MODE = modeFromEnvOrDefault('STRESS_TEST_MODE') const DATABASE_URI = fromEnvOrDefault( @@ -231,7 +231,7 @@ describe('#integration stress tests', () => { .run(query, params) .then(result => { context.queryCompleted(result, accessMode) - context.log(commandId, `Query completed successfully`) + context.log(commandId, 'Query completed successfully') return session.close().then(() => { const possibleError = verifyQueryResult(result) @@ -272,7 +272,7 @@ describe('#integration stress tests', () => { resultPromise .then(result => { context.queryCompleted(result, accessMode, session.lastBookmark()) - context.log(commandId, `Transaction function executed successfully`) + context.log(commandId, 'Transaction function executed successfully') return session.close().then(() => { const possibleError = verifyQueryResult(result) @@ -322,7 +322,7 @@ describe('#integration stress tests', () => { }) .then(() => { context.queryCompleted(result, accessMode, session.lastBookmark()) - context.log(commandId, `Transaction committed successfully`) + context.log(commandId, 'Transaction committed successfully') return session.close().then(() => { callback(commandError) @@ -341,7 +341,7 @@ describe('#integration stress tests', () => { function verifyQueryResult (result) { if (!result) { - return new Error(`Received undefined result`) + return new Error('Received undefined result') } else if (result.records.length === 0) { // it is ok to receive no nodes back for read queries at the beginning of the test return null diff --git a/test/transaction.test.js b/test/transaction.test.js index 021a744d1..6d35ff37a 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -94,7 +94,7 @@ describe('#integration transaction', () => { const tx = session.beginTransaction() tx.run("RETURN 'foo' AS res") .then(result => { - tx.run('CREATE ({name: {param}})', { + tx.run('CREATE ({name: $param})', { param: result.records[0].get('res') }) .then(() => { diff --git a/test/types.test.js b/test/types.test.js index efd20569d..59f2d8377 100644 --- a/test/types.test.js +++ b/test/types.test.js @@ -179,7 +179,7 @@ describe('#integration path values', () => { }) describe('#integration byte arrays', () => { - let originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + const originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL beforeAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 @@ -213,7 +213,7 @@ describe('#integration byte arrays', () => { const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) const session = driver.session() session - .run('RETURN {array}', { array: randomByteArray(42) }) + .run('RETURN $array', { array: randomByteArray(42) }) .catch(error => { expect(error.message).toEqual( 'Byte arrays are not supported by the database this driver is connected to' @@ -255,7 +255,7 @@ function runReturnQuery (driver, actual, expected) { const session = driver.session() return new Promise((resolve, reject) => { session - .run('RETURN {val} as v', { val: actual }) + .run('RETURN $val as v', { val: actual }) .then(result => { expect(result.records[0].get('v')).toEqual(expected || actual) }) diff --git a/test/types/driver.test.ts b/test/types/driver.test.ts index bd168cbf9..5ca333a36 100644 --- a/test/types/driver.test.ts +++ b/test/types/driver.test.ts @@ -107,6 +107,10 @@ driver.verifyConnectivity().then((serverInfo: ServerInfo) => { console.log(serverInfo.address) }) +driver.supportsMultiDb().then((supported: boolean) => { + console.log(`multi database is supported? => ${supported}`) +}) + const rxSession1: RxSession = driver.rxSession() const rxSession2: RxSession = driver.rxSession({ defaultAccessMode: READ }) const rxSession3: RxSession = driver.rxSession({ defaultAccessMode: 'READ' }) diff --git a/types/driver.d.ts b/types/driver.d.ts index 66221d38d..94a0a9cf7 100644 --- a/types/driver.d.ts +++ b/types/driver.d.ts @@ -86,6 +86,8 @@ declare interface Driver { close(): Promise verifyConnectivity(): Promise + + supportsMultiDb(): Promise } export {