diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index d93b0638b..b4cb1ce61 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -92,6 +92,13 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } + getNegotiatedProtocolVersion () { + return new Promise((resolve, reject) => { + this._hasProtocolVersion(resolve) + .catch(reject) + }) + } + async supportsTransactionConfig () { return await this._hasProtocolVersion( version => version >= BOLT_PROTOCOL_V3 diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index d8f573a0a..7e8e77790 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -244,6 +244,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } + getNegotiatedProtocolVersion () { + return new Promise((resolve, reject) => { + this._hasProtocolVersion(resolve) + .catch(reject) + }) + } + async verifyConnectivityAndGetServerInfo ({ database, accessMode }) { const context = { database: database || DEFAULT_DB_NAME } diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index a6b2bb53d..b0775683c 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -98,6 +98,18 @@ class ConnectionProvider { throw Error('Not implemented') } + /** + * Returns the protocol version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} the protocol version negotiated via handshake. + * @throws {Error} When protocol negotiation fails + */ + getNegotiatedProtocolVersion (): Promise { + throw Error('Not Implemented') + } + /** * Closes this connection provider along with its internals (connections, pools, etc.) * diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 9e2b7d641..241115e8d 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -220,6 +220,19 @@ class Driver { return connectionProvider.supportsUserImpersonation() } + /** + * Returns the protocol version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} the protocol version negotiated via handshake. + * @throws {Error} When protocol negotiation fails + */ + getNegotiatedProtocolVersion (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.getNegotiatedProtocolVersion() + } + /** * Returns boolean to indicate if driver has been configured with encryption enabled. * diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index e961d2108..3456b190c 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -328,6 +328,26 @@ function driver ( } } +/** + * Verifies if the driver can reach a server at the given url. + * + * @experimental + * @since 5.0.0 + * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" + * @param {Pick} config Configuration object. See the {@link driver} + * @returns {true} When the server is reachable + * @throws {Error} When the server is not reachable or the url is invalid + */ +async function hasReachableServer (url: string, config?: Pick): Promise { + const nonLoggedDriver = driver(url, { scheme: 'none', principal: '', credentials: '' }, config) + try { + await nonLoggedDriver.getNegotiatedProtocolVersion() + return true + } finally { + await nonLoggedDriver.close() + } +} + const USER_AGENT: string = 'neo4j-javascript/' + VERSION /** @@ -393,6 +413,7 @@ const temporal = { */ const forExport = { driver, + hasReachableServer, int, isInt, isPoint, @@ -444,6 +465,7 @@ const forExport = { export { driver, + hasReachableServer, int, isInt, isPoint, diff --git a/packages/neo4j-driver-lite/test/integration/driver.test.ts b/packages/neo4j-driver-lite/test/integration/driver.test.ts index 3bac73513..1bd4b167c 100644 --- a/packages/neo4j-driver-lite/test/integration/driver.test.ts +++ b/packages/neo4j-driver-lite/test/integration/driver.test.ts @@ -45,4 +45,12 @@ describe('neo4j-driver-lite', () => { expect(result.records[0].length).toEqual(1) result.records[0].forEach(val => expect(val).toEqual(int(2))) }) + + test('hasReachableServer success', async () => { + await expect(neo4j.hasReachableServer(`${scheme}://${hostname}`)).resolves.toBe(true) + }) + + test('hasReachableServer failure', async () => { + await expect(neo4j.hasReachableServer(`${scheme}://${hostname}:9999`)).rejects.toBeInstanceOf(Error) + }) }) diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index 3d19fe85c..9899a1e16 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -222,7 +222,8 @@ describe('index', () => { supportsMultiDb: async () => true, supportsTransactionConfig: async () => true, supportsUserImpersonation: async () => true, - verifyConnectivityAndGetServerInfo: async () => new ServerInfo({}) + verifyConnectivityAndGetServerInfo: async () => new ServerInfo({}), + getNegotiatedProtocolVersion: async () => 5.0 } }) expect(session).toBeDefined() diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index cafef42f0..d706e5add 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -305,6 +305,26 @@ function driver (url, authToken, config = {}) { } } +/** + * Verifies if the driver can reach a server at the given url. + * + * @experimental + * @since 5.0.0 + * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" + * @param {object} config Configuration object. See the {@link driver} + * @returns {true} When the server is reachable + * @throws {Error} When the server is not reachable or the url is invalid + */ +async function hasReachableServer (url, config) { + const nonLoggedDriver = driver(url, { scheme: 'none', principal: '', credentials: '' }, config) + try { + await nonLoggedDriver.getNegotiatedProtocolVersion() + return true + } finally { + await nonLoggedDriver.close() + } +} + const USER_AGENT = 'neo4j-javascript/' + VERSION /** @@ -385,6 +405,7 @@ const temporal = { */ const forExport = { driver, + hasReachableServer, int, isInt, isPoint, @@ -437,6 +458,7 @@ const forExport = { export { driver, + hasReachableServer, int, isInt, isPoint, diff --git a/packages/neo4j-driver/test/driver.test.js b/packages/neo4j-driver/test/driver.test.js index f4b7abaa5..1883a541c 100644 --- a/packages/neo4j-driver/test/driver.test.js +++ b/packages/neo4j-driver/test/driver.test.js @@ -588,6 +588,16 @@ describe('#integration driver', () => { }) }) + it('hasReachableServer success', async () => { + await expectAsync(neo4j.hasReachableServer(`${sharedNeo4j.scheme}://${sharedNeo4j.hostname}`)) + .toBeResolvedTo(true) + }) + + it('hasReachableServer failure', async () => { + await expectAsync(neo4j.hasReachableServer(`${sharedNeo4j.scheme}://${sharedNeo4j.hostname}:9999`)) + .toBeRejected() + }) + const integersWithNativeNumberEquivalent = [ [neo4j.int(0), 0], [neo4j.int(42), 42], diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 872c94817..89937a80e 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -100,6 +100,11 @@ declare function driver ( config?: Config ): Driver +declare function hasReachableServer ( + url: string, + config?: Pick +): Promise + declare const types: { Node: typeof Node Relationship: typeof Relationship @@ -158,6 +163,7 @@ declare const temporal: { declare const forExport: { driver: typeof driver + hasReachableServer: typeof hasReachableServer int: typeof int isInt: typeof isInt integer: typeof integer @@ -218,6 +224,7 @@ declare const forExport: { export { driver, + hasReachableServer, int, isInt, integer,