diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-util.js b/packages/bolt-connection/src/bolt/bolt-protocol-util.js index 8a6ef2e4d..e3458a9a4 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-util.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-util.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError } from 'neo4j-driver-core' +import { newError, json } from 'neo4j-driver-core' // eslint-disable-next-line no-unused-vars import { ResultStreamObserver } from './stream-observers' @@ -79,4 +79,23 @@ function assertImpersonatedUserIsEmpty (impersonatedUser, onProtocolError = () = } } -export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty } +/* Asserts that the passed-in notificationFilters is empty + * @param {string[]} notificationFilters + * @param {function (err:Error)} onProtocolError Called when it does have notificationFilters user set + * @param {any} observer + */ +function assertNotificationFiltersIsEmpty (notificationFilters, onProtocolError = () => {}, observer) { + if (notificationFilters !== undefined) { + const error = newError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${json.stringify(notificationFilters)}.` + ) + // unsupported API was used, consider this a fatal error for the current connection + onProtocolError(error.message) + observer.onError(error) + throw error + } +} + +export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index f3a7b32a0..c3a1f2381 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -19,7 +19,8 @@ import { assertDatabaseIsEmpty, assertTxConfigIsEmpty, - assertImpersonatedUserIsEmpty + assertImpersonatedUserIsEmpty, + assertNotificationFiltersIsEmpty } from './bolt-protocol-util' // eslint-disable-next-line no-unused-vars import { Chunker } from '../channel' @@ -149,14 +150,18 @@ export default class BoltProtocol { * @param {Object} param.authToken the authentication token. * @param {function(err: Error)} param.onError the callback to invoke on error. * @param {function()} param.onComplete the callback to invoke on completion. + * @param {?string[]} param.notificationFilters the filtering for notifications. * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write(RequestMessage.init(userAgent, authToken), observer, true) return observer @@ -177,6 +182,7 @@ export default class BoltProtocol { * @param {string} param.database the target database name. * @param {string} param.mode the access mode. * @param {string} param.impersonatedUser the impersonated user + * @param {?string[]} param.notificationFilters the filtering for notifications. * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. * @param {function()} param.beforeComplete the callback to invoke before handling the completion. @@ -189,6 +195,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -203,6 +210,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -285,6 +293,7 @@ export default class BoltProtocol { * @param {TxConfig} param.txConfig the transaction configuration. * @param {string} param.database the target database name. * @param {string} param.impersonatedUser the impersonated user + * @param {?string[]} param.notificationFilters the filtering for notifications. * @param {string} param.mode the access mode. * @param {function(keys: string[])} param.beforeKeys the callback to invoke before handling the keys. * @param {function(keys: string[])} param.afterKeys the callback to invoke after handling the keys. @@ -304,6 +313,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeKeys, afterKeys, beforeError, @@ -327,6 +337,8 @@ export default class BoltProtocol { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // bookmarks and mode are ignored in this version of the protocol assertTxConfigIsEmpty(txConfig, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v3.js index 25dd7ad5a..d313aa636 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v3.js @@ -18,7 +18,7 @@ */ import BoltProtocolV2 from './bolt-protocol-v2' import RequestMessage from './request-message' -import { assertDatabaseIsEmpty, assertImpersonatedUserIsEmpty } from './bolt-protocol-util' +import { assertDatabaseIsEmpty, assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } from './bolt-protocol-util' import { StreamObserver, LoginObserver, @@ -69,12 +69,15 @@ export default class BoltProtocol extends BoltProtocolV2 { return metadata } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write(RequestMessage.hello(userAgent, authToken), observer, true) return observer @@ -89,6 +92,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeError, afterError, @@ -104,6 +108,8 @@ export default class BoltProtocol extends BoltProtocolV2 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error @@ -166,6 +172,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeKeys, afterKeys, @@ -190,6 +197,8 @@ export default class BoltProtocol extends BoltProtocolV2 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js index 2dfc1f541..3c09d421c 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js @@ -18,7 +18,7 @@ */ import BoltProtocolV3 from './bolt-protocol-v3' import RequestMessage from './request-message' -import { assertImpersonatedUserIsEmpty } from './bolt-protocol-util' +import { assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } from './bolt-protocol-util' import { ResultStreamObserver, ProcedureRouteObserver @@ -56,6 +56,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeError, afterError, @@ -71,6 +72,8 @@ export default class BoltProtocol extends BoltProtocolV3 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) @@ -91,6 +94,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeKeys, afterKeys, @@ -121,6 +125,8 @@ export default class BoltProtocol extends BoltProtocolV3 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js index f8a1de208..ee31b99e9 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js @@ -23,6 +23,7 @@ import { internal } from 'neo4j-driver-core' import transformersFactories from './bolt-protocol-v4x1.transformer' import Transformer from './transformer' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util' const { constants: { BOLT_PROTOCOL_V4_1 } @@ -72,12 +73,15 @@ export default class BoltProtocol extends BoltProtocolV4 { return this._transformer } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting), observer, diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js index 3ca55e4f4..823a27401 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js @@ -25,6 +25,7 @@ import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' import Transformer from './transformer' import { internal } from 'neo4j-driver-core' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util' const { bookmarks: { Bookmarks }, @@ -85,9 +86,10 @@ export default class BoltProtocol extends BoltProtocolV42 { * @param {any} param0.authToken The auth token * @param {function(error)} param0.onError On error callback * @param {function(onComplte)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => { @@ -98,6 +100,9 @@ export default class BoltProtocol extends BoltProtocolV42 { } }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting, ['utc']), observer, diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js index bd2c94228..9cfc7aca3 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js @@ -26,6 +26,8 @@ import transformersFactories from './bolt-protocol-v4x4.transformer' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' import Transformer from './transformer' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util' + const { constants: { BOLT_PROTOCOL_V4_4, FETCH_ALL }, bookmarks: { Bookmarks } @@ -87,6 +89,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilters, beforeKeys, afterKeys, beforeError, @@ -116,6 +119,9 @@ export default class BoltProtocol extends BoltProtocolV43 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + const flushRun = reactive this.write( RequestMessage.runWithMetadata(query, parameters, { @@ -142,6 +148,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -156,6 +163,9 @@ export default class BoltProtocol extends BoltProtocolV43 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.begin({ bookmarks, txConfig, database, mode, impersonatedUser }), observer, diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js index 8cfbefa2b..09db2d255 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js @@ -24,6 +24,7 @@ import RequestMessage from './request-message' import { LoginObserver } from './stream-observers' import { internal } from 'neo4j-driver-core' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util' const { constants: { BOLT_PROTOCOL_V5_0 } @@ -49,14 +50,18 @@ export default class BoltProtocol extends BoltProtocolV44 { * @param {any} param0.authToken The auth token * @param {function(error)} param0.onError On error callback * @param {function(onComplte)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting), observer, diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.js new file mode 100644 index 000000000..dc2bd29a8 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.js @@ -0,0 +1,186 @@ +/** + * Copyright (c) "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 BoltProtocolV5x0 from './bolt-protocol-v5x0' + +import transformersFactories from './bolt-protocol-v5x1.transformer' +import Transformer from './transformer' +import RequestMessage from './request-message' +import { LoginObserver, ResultStreamObserver } from './stream-observers' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V5_1, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x0 { + get version () { + return BOLT_PROTOCOL_V5_1 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Initialize a connection with the server + * + * @param {Object} param0 The params + * @param {string} param0.userAgent The user agent + * @param {any} param0.authToken The auth token + * @param {function(error)} param0.onError On error callback + * @param {function(onComplete)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. + * @returns {LoginObserver} The Login observer + */ + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) + }) + + this.write( + RequestMessage.hello5x1(authToken, { + userAgent, + notificationFilters: sanitizeNotificationFilters(notificationFilters), + routing: this._serversideRouting + }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + reactive = false, + fetchSize = FETCH_ALL, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + reactive: reactive, + fetchSize: fetchSize, + moreFunction: this._requestMore.bind(this), + discardFunction: this._requestDiscard.bind(this), + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + const flushRun = reactive + this.write( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters: sanitizeNotificationFilters(notificationFilters) + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this.write( + RequestMessage.begin({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters: sanitizeNotificationFilters(notificationFilters) + }), + observer, + true + ) + + return observer + } +} + +function sanitizeNotificationFilters (filters) { + if (filters == null || filters === []) { + return filters + } + + if (filters[0] === 'NONE') { + return [] + } + + if (filters[0] === 'SERVER_DEFAULT') { + return null + } + + return filters.map(filter => filter.replace(/^ALL\./, '*.').replace(/\.ALL$/, '.*')) +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.transformer.js new file mode 100644 index 000000000..0923aef4d --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.transformer.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) "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 v5x0 from './bolt-protocol-v5x0.transformer' + +export default { + ...v5x0 +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index f2f4065b3..374266fda 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -27,6 +27,7 @@ import BoltProtocolV4x2 from './bolt-protocol-v4x2' import BoltProtocolV4x3 from './bolt-protocol-v4x3' import BoltProtocolV4x4 from './bolt-protocol-v4x4' import BoltProtocolV5x0 from './bolt-protocol-v5x0' +import BoltProtocolV5x1 from './bolt-protocol-v5x1' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -191,6 +192,16 @@ function createProtocol ( onProtocolError, serversideRouting ) + case 5.1: + return new BoltProtocolV5x1( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/bolt-connection/src/bolt/handshake.js b/packages/bolt-connection/src/bolt/handshake.js index c41d4bd82..09eb4beff 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -76,7 +76,7 @@ function parseNegotiatedResponse (buffer) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - version(5, 0), + [version(5, 1), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index 4a525daa7..e4b712a49 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -104,6 +104,7 @@ export default class RequestMessage { * @param {string} userAgent the user agent. * @param {Object} authToken the authentication token. * @param {Object} optional server side routing, set to routing context to turn on server side routing (> 4.1) + * @param {?string[]} patchs patches to be applied to the server (valid in 4.3 and 4.4) * @return {RequestMessage} new HELLO message. */ static hello (userAgent, authToken, routing = null, patchs = null) { @@ -121,6 +122,31 @@ export default class RequestMessage { ) } + /** + * Create a new HELLO message. + * @param {Object} authToken the authentication token. + * @param {Object} param1 the extra information + * @param {string} param1.userAgent the user agent + * @param {?Object} param1.routing the server side routing context, when set the server side routing is enabled + * @param {?string[]} param1.notificationFilters the cypher notification filters + */ + static hello5x1 (authToken, { userAgent, routing, notificationFilters } = {}) { + const extra = { user_agent: userAgent } + if (routing) { + extra.routing = routing + } + + if (notificationFilters !== undefined) { + extra.notifications = notificationFilters + } + + return new RequestMessage( + HELLO, + [authToken, extra], + () => `HELLO {...} ${json.stringify(extra)}` + ) + } + /** * Create a new BEGIN message. * @param {Bookmarks} bookmarks the bookmarks. @@ -128,10 +154,11 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {?string[]} notificationFilters the notification filters * @return {RequestMessage} new BEGIN message. */ - static begin ({ bookmarks, txConfig, database, mode, impersonatedUser } = {}) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + static begin ({ bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) return new RequestMessage( BEGIN, [metadata], @@ -164,14 +191,15 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {?string[]} notificationFilters the notification filters * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata ( query, parameters, - { bookmarks, txConfig, database, mode, impersonatedUser } = {} + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters } = {} ) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) return new RequestMessage( RUN, [query, parameters, metadata], @@ -282,9 +310,10 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user mode. + * @param {?string[]} notificationFilters the notification filters * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -301,6 +330,9 @@ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) if (impersonatedUser) { metadata.imp_user = assertString(impersonatedUser, 'impersonatedUser') } + if (notificationFilters !== undefined) { + metadata.notifications = notificationFilters + } if (mode === ACCESS_MODE_READ) { metadata.mode = READ_MODE } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 42cb30fbc..60ae3cbd4 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -34,6 +34,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._log = log this._userAgent = userAgent this._authToken = authToken + this._notificationFilters = this._config.notificationFilters this._createChannelConnection = createChannelConnectionHook || (address => { @@ -76,7 +77,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } this._openConnections[connection.id] = connection return connection - .connect(this._userAgent, this._authToken) + .connect(this._userAgent, this._authToken, this._notificationFilters) .catch(error => { // let's destroy this connection this._destroyConnection(connection) diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 4efcb5f51..f1fc0e516 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -169,24 +169,27 @@ export default class ChannelConnection extends Connection { * Send initialization message. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { - return this._initialize(userAgent, authToken) + connect (userAgent, authToken, notificationFilters) { + return this._initialize(userAgent, authToken, notificationFilters) } /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if initialization is successful. Rejected promise otherwise. */ - _initialize (userAgent, authToken) { + _initialize (userAgent, authToken, notificationFilters) { const self = this return new Promise((resolve, reject) => { this._protocol.initialize({ userAgent, authToken, + notificationFilters, onError: err => reject(err), onComplete: metadata => { if (metadata) { @@ -340,13 +343,14 @@ export default class ChannelConnection extends Connection { }) } - _reset(observer) { + _reset (observer) { if (this._reseting) { if (!this._protocol.isLastMessageReset()) { this._protocol.reset({ onError: error => { observer.onError(error) - }, onComplete: () => { + }, + onComplete: () => { observer.onComplete() } }) @@ -369,7 +373,8 @@ export default class ChannelConnection extends Connection { this._protocol.reset({ onError: error => { notifyFinish(obs => obs.onError(error)) - }, onComplete: () => { + }, + onComplete: () => { notifyFinish(obs => obs.onComplete()) } }) diff --git a/packages/bolt-connection/src/connection/connection-delegate.js b/packages/bolt-connection/src/connection/connection-delegate.js index 16d2da48a..9723962fa 100644 --- a/packages/bolt-connection/src/connection/connection-delegate.js +++ b/packages/bolt-connection/src/connection/connection-delegate.js @@ -71,8 +71,8 @@ export default class DelegateConnection extends Connection { return this._delegate.protocol() } - connect (userAgent, authToken) { - return this._delegate.connect(userAgent, authToken) + connect (userAgent, authToken, notificationFilters) { + return this._delegate.connect(userAgent, authToken, notificationFilters) } write (message, observer, flush) { diff --git a/packages/bolt-connection/src/connection/connection.js b/packages/bolt-connection/src/connection/connection.js index d3c692712..6d919d7a8 100644 --- a/packages/bolt-connection/src/connection/connection.js +++ b/packages/bolt-connection/src/connection/connection.js @@ -79,9 +79,10 @@ export default class Connection { * Connect to the target address, negotiate Bolt protocol and send initialization message. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { + connect (userAgent, authToken, notificationFilters) { throw new Error('not implemented') } diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap new file mode 100644 index 000000000..8f5271141 --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x1.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:\\"b\\"})"`; + +exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:\\"c\\"}]->(f)"`; + +exports[`#unit BoltProtocolV5x1 .packable() should pack not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:\\"c\\"}]->"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x1 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`; diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js index 9d69a2909..4d6490ac5 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -334,6 +334,59 @@ describe('#unit BoltProtocolV1', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV1)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV1(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js index a9d1f1a50..aea0716b5 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js @@ -111,6 +111,59 @@ describe('#unit BoltProtocolV2', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV2)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV2(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('watermarks', () => { it('.run() should configure watermarks', () => { const recorder = new utils.MessageRecordingConnection() diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js index b3139c3cd..e5458d8f1 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -293,6 +293,59 @@ describe('#unit BoltProtocolV3', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV3)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV3(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js index 0bd626c9d..37c5aad60 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js @@ -211,6 +211,59 @@ describe('#unit BoltProtocolV4x0', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV4x0)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV4x0(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js index c80b94f86..e09ebe329 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js @@ -85,6 +85,59 @@ describe('#unit BoltProtocolV4x1', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV4x1)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV4x1(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js index 998ac3206..f8cffa3e6 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js @@ -84,6 +84,60 @@ describe('#unit BoltProtocolV4x2', () => { }) }) }) + + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV4x2)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV4x2(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js index 5b2d393ff..c1b649c70 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -299,6 +299,59 @@ describe('#unit BoltProtocolV4x3', () => { }) }) + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV4x3)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV4x3(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) + describe('unpacker configuration', () => { test.each([ [false, false], diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js index 8d250e7c2..25f8f3927 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js @@ -1128,4 +1128,57 @@ describe('#unit BoltProtocolV4x4', () => { }) }) }) + + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV4x4)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV4x4(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) }) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index 221c1893b..f35c9f45e 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -1027,4 +1027,57 @@ describe('#unit BoltProtocolV5x0', () => { expect(unpacked).toEqual(struct) }) }) + + describe('Bolt v5.1', () => { + /** + * @param {string[]} notificationFilters The notification filters. + * @param {function(protocol: BoltProtocolV5x0)} fn + */ + function verifyNotificationFiltersNotSupportedError (notificationFilters, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false, undefined, undefined, () => {}) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilters)}.` + ) + } + + describe('initialize', () => { + function verifyInitialize (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.initialize({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyInitialize(['test']) + }) + }) + + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.beginTransaction({ notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyBeginTransaction(['test']) + }) + }) + + describe('run', () => { + function verifyRun (notificationFilters) { + verifyNotificationFiltersNotSupportedError( + notificationFilters, + protocol => protocol.run('query', {}, { notificationFilters })) + } + + it('should throw error when notificationFilters is set', () => { + verifyRun(['test']) + }) + }) + }) }) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js new file mode 100644 index 000000000..652246dda --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js @@ -0,0 +1,1130 @@ +/** + * Copyright (c) "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 BoltProtocolV5x1 from '../../src/bolt/bolt-protocol-v5x1' +import RequestMessage from '../../src/bolt/request-message' +import { v2, structure } from '../../src/packstream' +import utils from '../test-utils' +import { RouteObserver } from '../../src/bolt/stream-observers' +import fc from 'fast-check' +import { + Date, + DateTime, + Duration, + LocalDateTime, + LocalTime, + Path, + PathSegment, + Point, + Relationship, + Time, + UnboundRelationship, + Node, + internal +} from 'neo4j-driver-core' + +import { alloc } from '../../src/channel' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV5x1', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, [], { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should request routing information sending bookmarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const listOfBookmarks = ['a', 'b', 'c'] + const bookmarks = new Bookmarks(listOfBookmarks) + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName, + sessionContext: { bookmarks } + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, listOfBookmarks, { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should run a query', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should run a with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each(notificationFiltersFixtures())('should run a with notification filters [%s]', + (notificationFilters, sentNotificationFilters) => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + notificationFilters + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + notificationFilters: sentNotificationFilters + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should begin a transaction', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, impersonatedUser }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it.each(notificationFiltersFixtures())('should begin a transaction with notification filters [%s]', + (notificationFilters, sentNotificationFilters) => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + notificationFilters + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, notificationFilters: sentNotificationFilters }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV5x1(null, null, false) + + expect(protocol.version).toBe(5.1) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x1(null, null, false) + + const transformedMetadata = protocol.transformMetadata(metadata) + + expect(transformedMetadata).toEqual({ + result_available_after: 1, + result_consumed_after: 2, + db_hits: 3, + some_other_key: 4 + }) + }) + + it('should initialize connection', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x1(authToken, { userAgent: clientName }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it.each(notificationFiltersFixtures())('should initialize connection with notification filters [%s]', + (notificationFilters, sentNotificationFilters) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken, notificationFilters }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x1(authToken, { routing: false, userAgent: clientName, notificationFilters: sentNotificationFilters }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should commit', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.commitTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.commit()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should rollback', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x1(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.rollbackTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.rollback()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV5x1(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV5x1(recorder, null, false) + ) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + const observer = protocol.run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + lowRecordWatermark: 100, + highRecordWatermark: 200 + }) + + expect(observer._lowRecordWatermark).toEqual(100) + expect(observer._highRecordWatermark).toEqual(200) + }) + }) + + describe('packstream', () => { + it('should configure v2 packer', () => { + const protocol = new BoltProtocolV5x1(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV5x1(null, null, false) + expect(protocol.unpacker()).toBeInstanceOf(v2.Unpacker) + }) + }) + + describe('.packable()', () => { + it.each([ + ['Node', new Node(1, ['a'], { a: 'b' }, 'c')], + ['Relationship', new Relationship(1, 2, 3, 'a', { b: 'c' }, 'd', 'e', 'f')], + ['UnboundRelationship', new UnboundRelationship(1, 'a', { b: 'c' }, '1')], + ['Path', new Path(new Node(1, [], {}), new Node(2, [], {}), [])] + ])('should pack not pack graph types (%s)', (_, graphType) => { + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + null, + false + ) + + const packable = protocol.packable(graphType) + + expect(packable).toThrowErrorMatchingSnapshot() + }) + + it.each([ + ['Duration', new Duration(1, 1, 1, 1)], + ['LocalTime', new LocalTime(1, 1, 1, 1)], + ['Time', new Time(1, 1, 1, 1, 1)], + ['Date', new Date(1, 1, 1)], + ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ], + ['Point2D', new Point(1, 1, 1)], + ['Point3D', new Point(1, 1, 1, 1)] + ])('should pack spatial types and temporal types (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(1978, 12, 16, 12, 35, 59, 128000987, undefined, 'Europe/Istanbul') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Pacific/Honolulu') + ], + [ + 'DateWithWithZoneId / Berlin before common era', + new DateTime(-2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateWithWithZoneId / Max Date', + new DateTime(99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Kiritimati') + ], + [ + 'DateWithWithZoneId / Min Date', + new DateTime(-99_999, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ], + [ + 'DateWithWithZoneId / Ambiguous date between 00 and 99', + new DateTime(50, 12, 31, 23, 59, 59, 999_999_999, undefined, 'Pacific/Samoa') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it('should pack and unpack DateTimeWithOffset', () => { + fc.assert( + fc.property( + fc.date({ + min: temporalUtil.newDate(utils.MIN_UTC_IN_MS + utils.ONE_DAY_IN_MS), + max: temporalUtil.newDate(utils.MAX_UTC_IN_MS - utils.ONE_DAY_IN_MS) + }), + fc.integer({ min: 0, max: 999_999 }), + utils.arbitraryTimeZoneId(), + (date, nanoseconds, timeZoneId) => { + const object = new DateTime( + date.getUTCFullYear(), + date.getUTCMonth() + 1, + date.getUTCDate(), + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds() * 1_000_000 + nanoseconds, + undefined, + timeZoneId + ) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + ) + }) + + it('should pack and unpack DateTimeWithZoneIdAndNoOffset', () => { + fc.assert( + fc.property(fc.date(), date => { + const object = DateTime.fromStandardDate(date) + const buffer = alloc(256) + const loggerFunction = jest.fn() + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + }, + undefined, + new Logger('debug', loggerFunction) + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + expect(unpacked).toEqual(object) + }) + ) + }) + }) + + describe('.unpack()', () => { + it.each([ + [ + 'Node', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, 'elementId']), + new Node(1, ['a'], { c: 'd' }, 'elementId') + ], + [ + 'Relationship', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2']), + new Relationship(1, 2, 3, '4', { 5: 6 }, 'elementId', 'node1', 'node2') + ], + [ + 'UnboundRelationship', + new structure.Structure(0x72, [1, '2', { 3: 4 }, 'elementId']), + new UnboundRelationship(1, '2', { 3: 4 }, 'elementId') + ], + [ + 'Path', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }, 'node1']), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }, 'node2']), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }, 'node3']) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2']), + new structure.Structure(0x52, [5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3']) + ], + [1, 1, 2, 2] + ] + ), + new Path( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Node(2, ['3'], { 4: '5' }, 'node3'), + [ + new PathSegment( + new Node(1, ['2'], { 3: '4' }, 'node1'), + new Relationship(3, 1, 4, 'reltype1', { 4: '5' }, 'rel1', 'node1', 'node2'), + new Node(4, ['5'], { 6: 7 }, 'node2') + ), + new PathSegment( + new Node(4, ['5'], { 6: 7 }, 'node2'), + new Relationship(5, 4, 2, 'reltype2', { 6: 7 }, 'rel2', 'node2', 'node3'), + new Node(2, ['3'], { 4: '5' }, 'node3') + ) + ] + ) + ] + ])('should unpack graph types (%s)', (_, struct, graphObject) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(graphObject) + }) + + it.each([ + [ + 'Node with less fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }]) + ], + [ + 'Node with more fields', + new structure.Structure(0x4e, [1, ['a'], { c: 'd' }, '1', 'b']) + ], + [ + 'Relationship with less fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }]) + ], + [ + 'Relationship with more fields', + new structure.Structure(0x52, [1, 2, 3, '4', { 5: 6 }, '1', '2', '3', '4']) + ], + [ + 'UnboundRelationship with less fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }]) + ], + [ + 'UnboundRelationship with more fields', + new structure.Structure(0x72, [1, '2', { 3: 4 }, '1', '2']) + ], + [ + 'Path with less fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ] + ] + ) + ], + [ + 'Path with more fields', + new structure.Structure( + 0x50, + [ + [ + new structure.Structure(0x4e, [1, ['2'], { 3: '4' }]), + new structure.Structure(0x4e, [4, ['5'], { 6: 7 }]), + new structure.Structure(0x4e, [2, ['3'], { 4: '5' }]) + ], + [ + new structure.Structure(0x52, [3, 1, 4, 'rel1', { 4: '5' }]), + new structure.Structure(0x52, [5, 4, 2, 'rel2', { 6: 7 }]) + ], + [1, 1, 2, 2], + 'a' + ] + ) + ], + [ + 'Point with less fields', + new structure.Structure(0x58, [1, 2]) + ], + [ + 'Point with more fields', + new structure.Structure(0x58, [1, 2, 3, 4]) + ], + [ + 'Point3D with less fields', + new structure.Structure(0x59, [1, 2, 3]) + ], + + [ + 'Point3D with more fields', + new structure.Structure(0x59, [1, 2, 3, 4, 6]) + ], + [ + 'Duration with less fields', + new structure.Structure(0x45, [1, 2, 3]) + ], + [ + 'Duration with more fields', + new structure.Structure(0x45, [1, 2, 3, 4, 5]) + ], + [ + 'LocalTime with less fields', + new structure.Structure(0x74, []) + ], + [ + 'LocalTime with more fields', + new structure.Structure(0x74, [1, 2]) + ], + [ + 'Time with less fields', + new structure.Structure(0x54, [1]) + ], + [ + 'Time with more fileds', + new structure.Structure(0x54, [1, 2, 3]) + ], + [ + 'Date with less fields', + new structure.Structure(0x44, []) + ], + [ + 'Date with more fields', + new structure.Structure(0x44, [1, 2]) + ], + [ + 'LocalDateTime with less fields', + new structure.Structure(0x64, [1]) + ], + [ + 'LocalDateTime with more fields', + new structure.Structure(0x64, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + false + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(() => unpacked instanceof structure.Structure).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'Point', + new structure.Structure(0x58, [1, 2, 3]), + new Point(1, 2, 3) + ], + [ + 'Point3D', + new structure.Structure(0x59, [1, 2, 3, 4]), + new Point(1, 2, 3, 4) + ], + [ + 'Duration', + new structure.Structure(0x45, [1, 2, 3, 4]), + new Duration(1, 2, 3, 4) + ], + [ + 'LocalTime', + new structure.Structure(0x74, [1]), + new LocalTime(0, 0, 0, 1) + ], + [ + 'Time', + new structure.Structure(0x54, [1, 2]), + new Time(0, 0, 0, 1, 2) + ], + [ + 'Date', + new structure.Structure(0x44, [1]), + new Date(1970, 1, 2) + ], + [ + 'LocalDateTime', + new structure.Structure(0x64, [1, 2]), + new LocalDateTime(1970, 1, 1, 0, 0, 1, 2) + ], + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneOffset / 1978', + new structure.Structure(0x49, [ + 282659759, 128000987, -150 * 60 + ]), + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId / Honolulu', + new structure.Structure(0x69, [ + 1592231400, 183_000_000, 'Pacific/Honolulu' + ]), + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, -10 * 60 * 60, 'Pacific/Honolulu') + ] + ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao_Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x1( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + function notificationFiltersFixtures () { + return [ + [['ALL.ALL', 'WARNING.ALL', 'ALL.HINT', 'INFORMATION.QUERY'], ['*.*', 'WARNING.*', '*.HINT', 'INFORMATION.QUERY']], + [['NONE'], []], + [['SERVER_DEFAULT'], null] + ] + } +}) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index 4542c6008..2860f4ef7 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -31,6 +31,7 @@ import BoltProtocolV4x2 from '../../src/bolt/bolt-protocol-v4x2' import BoltProtocolV4x3 from '../../src/bolt/bolt-protocol-v4x3' import BoltProtocolV4x4 from '../../src/bolt/bolt-protocol-v4x4' import BoltProtocolV5x0 from '../../src/bolt/bolt-protocol-v5x0' +import BoltProtocolV5x1 from '../../src/bolt/bolt-protocol-v5x1' const { logger: { Logger } @@ -44,13 +45,13 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x0 = '00 00 00 05' + const protocolVersion5x1to5x0 = '00 01 01 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x1to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) @@ -359,7 +360,8 @@ describe('#unit Bolt', () => { v(4.2, BoltProtocolV4x2), v(4.3, BoltProtocolV4x3), v(4.4, BoltProtocolV4x4), - v(5.0, BoltProtocolV5x0) + v(5.0, BoltProtocolV5x0), + v(5.1, BoltProtocolV5x1) ] availableProtocols.forEach(lambda) diff --git a/packages/bolt-connection/test/bolt/request-message.test.js b/packages/bolt-connection/test/bolt/request-message.test.js index f0f66dd68..d56054926 100644 --- a/packages/bolt-connection/test/bolt/request-message.test.js +++ b/packages/bolt-connection/test/bolt/request-message.test.js @@ -429,4 +429,274 @@ describe('#unit RequestMessage', () => { }) }) }) + + describe('BoltV5.1', () => { + it('should create HELLO message with notification filters', () => { + const userAgent = 'my-driver/1.0.2' + const authToken = { username: 'neo4j', password: 'secret' } + const notificationFilters = ['*.*', 'WARNING.*'] + + const message = RequestMessage.hello5x1(authToken, { userAgent, notificationFilters }) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + { username: 'neo4j', password: 'secret' }, + { user_agent: userAgent, notifications: notificationFilters } + ]) + expect(message.toString()).toEqual( + `HELLO {...} {"user_agent":"${userAgent}","notifications":["*.*","WARNING.*"]}` + ) + }) + + it('should create HELLO message with notification filters=null', () => { + const userAgent = 'my-driver/1.0.2' + const authToken = { username: 'neo4j', password: 'secret' } + const notificationFilters = null + + const message = RequestMessage.hello5x1(authToken, { userAgent, notificationFilters }) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + { username: 'neo4j', password: 'secret' }, + { user_agent: userAgent, notifications: notificationFilters } + ]) + expect(message.toString()).toEqual( + `HELLO {...} {"user_agent":"${userAgent}","notifications":null}` + ) + }) + + it('should create HELLO message without notification filters if it is not supplied', () => { + const userAgent = 'my-driver/1.0.2' + const authToken = { username: 'neo4j', password: 'secret' } + + const message = RequestMessage.hello5x1(authToken, { userAgent, notificationFilters: undefined }) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + { username: 'neo4j', password: 'secret' }, + { user_agent: userAgent } + ]) + expect(message.toString()).toEqual( + `HELLO {...} {"user_agent":"${userAgent}"}` + ) + }) + + it('should create HELLO message with routing', () => { + const userAgent = 'my-driver/1.0.2' + const authToken = { username: 'neo4j', password: 'secret' } + const routing = { address: 'localhost123:9090', otherInfo: 'info' } + + const message = RequestMessage.hello5x1(authToken, { userAgent, routing }) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + { username: 'neo4j', password: 'secret' }, + { user_agent: userAgent, routing } + ]) + expect(message.toString()).toEqual( + `HELLO {...} {"user_agent":"${userAgent}","routing":{"address":"localhost123:9090","otherInfo":"info"}}` + ) + }) + + it('should create BEGIN message with notification filters', () => { + ;[READ, WRITE].forEach(mode => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10' + ]) + const notificationFilters = ['*.*', 'WARNING.*'] + const txConfig = new TxConfig({ timeout: 42, metadata: { key: 42 } }) + + const message = RequestMessage.begin({ bookmarks, txConfig, mode, notificationFilters }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(42), + tx_metadata: { key: 42 }, + notifications: notificationFilters + } + if (mode === READ) { + expectedMetadata.mode = 'r' + } + + expect(message.signature).toEqual(0x11) + expect(message.fields).toEqual([expectedMetadata]) + expect(message.toString()).toEqual( + `BEGIN ${json.stringify(expectedMetadata)}` + ) + }) + }) + + it('should create BEGIN message with notification filters=null', () => { + ;[READ, WRITE].forEach(mode => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10' + ]) + const notificationFilters = null + const txConfig = new TxConfig({ timeout: 42, metadata: { key: 42 } }) + + const message = RequestMessage.begin({ bookmarks, txConfig, mode, notificationFilters }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(42), + tx_metadata: { key: 42 }, + notifications: notificationFilters + } + if (mode === READ) { + expectedMetadata.mode = 'r' + } + + expect(message.signature).toEqual(0x11) + expect(message.fields).toEqual([expectedMetadata]) + expect(message.toString()).toEqual( + `BEGIN ${json.stringify(expectedMetadata)}` + ) + }) + }) + + it('should create BEGIN message without notification filters if it is not supplied', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10' + ]) + const mode = WRITE + const txConfig = new TxConfig({ timeout: 42, metadata: { key: 42 } }) + + const message = RequestMessage.begin({ bookmarks, txConfig, mode, notificationFilters: undefined }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(42), + tx_metadata: { key: 42 } + } + + expect(message.signature).toEqual(0x11) + expect(message.fields).toEqual([expectedMetadata]) + expect(message.toString()).toEqual( + `BEGIN ${json.stringify(expectedMetadata)}` + ) + }) + + it('should create RUN message with the notification filters', () => { + ;[READ, WRITE].forEach(mode => { + const query = 'RETURN $x' + const parameters = { x: 42 } + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10', + 'neo4j:bookmark:v1:tx100' + ]) + const txConfig = new TxConfig({ + timeout: 999, + metadata: { a: 'a', b: 'b' } + }) + const notificationFilters = ['*.*', 'WARNING.*'] + + const message = RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + mode, + notificationFilters + }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(999), + tx_metadata: { a: 'a', b: 'b' }, + notifications: notificationFilters + } + if (mode === READ) { + expectedMetadata.mode = 'r' + } + + expect(message.signature).toEqual(0x10) + expect(message.fields).toEqual([query, parameters, expectedMetadata]) + expect(message.toString()).toEqual( + `RUN ${query} ${json.stringify(parameters)} ${json.stringify( + expectedMetadata + )}` + ) + }) + }) + + it('should create RUN message with the notification filters = null', () => { + ;[READ, WRITE].forEach(mode => { + const query = 'RETURN $x' + const parameters = { x: 42 } + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10', + 'neo4j:bookmark:v1:tx100' + ]) + const txConfig = new TxConfig({ + timeout: 999, + metadata: { a: 'a', b: 'b' } + }) + const notificationFilters = null + + const message = RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + mode, + notificationFilters + }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(999), + tx_metadata: { a: 'a', b: 'b' }, + notifications: notificationFilters + } + if (mode === READ) { + expectedMetadata.mode = 'r' + } + + expect(message.signature).toEqual(0x10) + expect(message.fields).toEqual([query, parameters, expectedMetadata]) + expect(message.toString()).toEqual( + `RUN ${query} ${json.stringify(parameters)} ${json.stringify( + expectedMetadata + )}` + ) + }) + }) + + it('should create RUN message without notification filters if it is not supplied', () => { + const mode = WRITE + const query = 'RETURN $x' + const parameters = { x: 42 } + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10', + 'neo4j:bookmark:v1:tx100' + ]) + const txConfig = new TxConfig({ + timeout: 999, + metadata: { a: 'a', b: 'b' } + }) + + const message = RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + mode, + notificationFilters: undefined + }) + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(999), + tx_metadata: { a: 'a', b: 'b' } + } + + expect(message.signature).toEqual(0x10) + expect(message.fields).toEqual([query, parameters, expectedMetadata]) + expect(message.toString()).toEqual( + `RUN ${query} ${json.stringify(parameters)} ${json.stringify( + expectedMetadata + )}` + ) + }) + }) }) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 21696bd27..e3cf80a46 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -124,6 +124,27 @@ describe('ChannelConnection', () => { ) } ) + + it.each([ + undefined, + null, + [], + ['*.*', 'WARNING.*'] + ])('should call protocol.initialize with notification filters when notificationFilters is %o', + async (notificationFilters) => { + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + await connection.connect('userAgent', {}, notificationFilters) + + expect(protocol.initialize).toHaveBeenCalledTimes(1) + expect(protocol.initialize).toBeCalledWith(expect.objectContaining({ + notificationFilters + })) + }) }) describe('._handleFatalError()', () => { diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 3c07d3c44..e7abb7060 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -20,6 +20,7 @@ import ConnectionProvider from './connection-provider' import { Bookmarks } from './internal/bookmarks' import ConfiguredCustomResolver from './internal/resolver/configured-custom-resolver' +import { isValidFilter, NotificationFilter } from './notification-filter' import { ACCESS_MODE_READ, @@ -41,6 +42,7 @@ import { } from './types' import { ServerAddress } from './internal/server-address' import BookmarkManager from './bookmark-manager' +import { stringify } from './json' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -89,6 +91,7 @@ type CreateSession = (args: { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }) => Session interface DriverConfig { @@ -96,6 +99,7 @@ interface DriverConfig { trust?: TrustStrategy fetchSize?: number logging?: LoggingConfig + notificationFilters?: NotificationFilter[] } /** @@ -110,6 +114,7 @@ class SessionConfig { impersonatedUser?: string fetchSize?: number bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] /** * @constructor @@ -159,10 +164,10 @@ class SessionConfig { * Enabling it is done by supplying an BookmarkManager implementation instance to this param. * A default implementation could be acquired by calling the factory function {@link bookmarkManager}. * - * **Warning**: Share the same BookmarkManager instance accross all session can have a negative impact + * **Warning**: Share the same BookmarkManager instance across all session can have a negative impact * on performance since all the queries will wait for the latest changes being propagated across the cluster. * For keeping consistency between a group of queries, use {@link Session} for grouping them. - * For keeping consistency between a group of sessions, use {@link BookmarkManager} instance for groupping them. + * For keeping consistency between a group of sessions, use {@link BookmarkManager} instance for grouping them. * * @example * const bookmarkManager = neo4j.bookmarkManager() @@ -179,7 +184,7 @@ class SessionConfig { * * // Reading Driver User will wait of the changes being propagated to the server before RUN the query * // So the 'Driver User' person should exist in the Result, unless deleted. - * const linkedSesssion2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) + * const linkedSession2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) * * await linkedSession1.close() * await linkedSession2.close() @@ -190,6 +195,63 @@ class SessionConfig { * @since 5.0 */ this.bookmarkManager = undefined + + /** + * Configure filter for {@link Notification} objects returned in {@link ResultSummary#notifications}. + * + * The filters are defined by "{@link NotificationSeverityLevel}.{@link NotificationCategory}" with the + * exception of the "UNKNOWN" severity and category. + * Additionally, "ALL" can be set as category, severity, or both. + * + * Disabling the filters is done by setting this configuration to ['NONE']. + * Using the default values is done by setting this configuration to ['SERVER_DEFAULT']. + * + * Helper constants and methods are defined at {@link notificationFilter}. + * + * @example + * // disabling notifications + * const sessionWithoutNotifications = driver.session({ database: 'neo4j', notificationFilters: neo4j.notificationFilter.disabled() }) + * // EQUIVALENT TO: const sessionWithoutNotifications = driver.session({ database:'neo4j', notificationFilters: ["NONE"] }) + * + * // using default server configuration + * const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j', notificationFilters: neo4j.notificationFilter.serverDefault() }) + * // EQUIVALENT TO: const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j', notificationFilters: ["SERVER_DEFAULT"] }) + * + * // using default configured in the connection/driver configuration + * const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j' }) + * + * // Enable all notifications + * const sessionWithAllNotifications = driver.session({ database: 'neo4j', notificationFilters: [neo4j.notificationFilter.ALL.ALL] }) + * // EQUIVALENT TO: const sessionWithAllNotifications = driver.session({ database: 'neo4j', notificationFilters: ['ALL.ALL'] }) + * + * // Configuring for any categories with severity 'WARNING', + * // or any severity with category 'HINT' + * // or severity 'INFORMATION' and category 'PERFORMANCE'. + * const sessionFineConfigured = driver.session({ + * database: 'neo4j', + * notificationFilters: [ + * neo4j.notificationFilter.WARNING.ALL, + * neo4j.notificationFilter.ALL.HINT, + * neo4j.notificationFilter.INFORMATION.PERFORMANCE + * ] + * }) + * + * // Configuring for any categories with severity 'WARNING', + * // or any severity with category 'HINT' + * // or severity 'INFORMATION' and category 'PERFORMANCE'. + * // const sessionFineConfigured = driver.session({ + * // database: 'neo4j', + * // notificationFilters: [ + * // 'WARNING.ALL', + * // 'ALL.HINT', + * // 'INFORMATION.PERFORMANCE' + * // ] + * // }) + * + * @type {NotificationFilter[]|undefined} + * @since 5.3 + */ + this.notificationFilters = undefined } } @@ -388,7 +450,8 @@ class Driver { database = '', impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -398,7 +461,8 @@ class Driver { impersonatedUser, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), - bookmarkManager + bookmarkManager, + notificationFilters }) } @@ -435,7 +499,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -444,7 +509,9 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }): Session { + validateNotificationFilters(notificationFilters) const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() const bookmarks = bookmarkOrBookmarks != null @@ -460,7 +527,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }) } @@ -501,6 +569,9 @@ function validateConfig (config: any, log: Logger): any { 'where a new connection is created while it is acquired' ) } + + validateNotificationFilters(config.notificationFilters) + return config } @@ -589,6 +660,35 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE } -export type { SessionConfig } +/** + * @private + */ +function validateNotificationFilters (filters: any): void { + if (filters == null || filters === []) { + return filters + } + + if (!Array.isArray(filters)) { + throw new TypeError('Expect "notificationFilters" to be instance of Array.') + } + + if (filters.length > 1) { + if (filters.includes('NONE')) { + throw new Error('Expect "notificationFilters" to not have "NONE" configured along with other filters.') + } + + if (filters.includes('SERVER_DEFAULT')) { + throw new Error('Expect "notificationFilters" to not have "SERVER_DEFAULT" configured along with other filters.') + } + } + + const invalidFilters = filters.filter(filter => !isValidFilter(filter)) + + if (invalidFilters.length > 0) { + const invalidValuesString = invalidFilters.map(stringify).join(', ') + throw new Error(`Invalid "notificationFilters". Invalid values: ${invalidValuesString}`) + } +} + +export { Driver, READ, WRITE, SessionConfig } export default Driver diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8f0bb0ff1..19bd3e9ce 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,8 +64,13 @@ import ResultSummary, { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + NotificationSeverityLevel, + NotificationCategory, + notificationCategory, + notificationSeverityLevel } from './result-summary' +import notificationFilter, { NotificationFilter } from './notification-filter' import Result, { QueryResult, ResultObserver } from './result' import ConnectionProvider from './connection-provider' import Connection from './connection' @@ -149,7 +154,10 @@ const forExport = { driver, json, auth, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export { @@ -209,7 +217,11 @@ export { driver, json, auth, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter, + SessionConfig } export type { @@ -221,7 +233,9 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, - SessionConfig + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter } export default forExport diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index 39e790f2f..1b9519387 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -34,6 +34,7 @@ const BOLT_PROTOCOL_V4_2: number = 4.2 const BOLT_PROTOCOL_V4_3: number = 4.3 const BOLT_PROTOCOL_V4_4: number = 4.4 const BOLT_PROTOCOL_V5_0: number = 5.0 +const BOLT_PROTOCOL_V5_1: number = 5.1 export { FETCH_ALL, @@ -50,5 +51,6 @@ export { BOLT_PROTOCOL_V4_2, BOLT_PROTOCOL_V4_3, BOLT_PROTOCOL_V4_4, - BOLT_PROTOCOL_V5_0 + BOLT_PROTOCOL_V5_0, + BOLT_PROTOCOL_V5_1 } diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts new file mode 100644 index 000000000..eac67e2d3 --- /dev/null +++ b/packages/core/src/notification-filter.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) "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 { NotificationCategory, NotificationSeverityLevel } from './result-summary' + +type Combine = `${Enum1}${Separator}${Enum2}` +type ExcludeUnknown = Exclude + +type FilterKeys = ExcludeUnknown | 'ALL' +type FilterInnerKeys = ExcludeUnknown | 'ALL' +type CombinedFilter = Combine + +type SeverityDotCategoryFilters = { [key in FilterKeys]: { [k in FilterInnerKeys]: NotificationFilter } } + +type NotificationFilter = 'NONE' | 'SERVER_DEFAULT' | CombinedFilter + +/** + * + * Notification filters used for configuring {@link Driver} and {@link Session}. + * + * @typedef { 'NONE' | 'SERVER_DEFAULT' | + * 'ALL.ALL' | 'ALL.DEPRECATION' | 'ALL.GENERIC' | 'ALL.HINT' | + * 'ALL.PERFORMANCE' | 'ALL.UNRECOGNIZED' | 'ALL.UNSUPPORTED' | + * 'INFORMATION.ALL' | 'INFORMATION.DEPRECATION' | 'INFORMATION.GENERIC' | 'INFORMATION.HINT' | + * 'INFORMATION.PERFORMANCE' | 'INFORMATION.UNRECOGNIZED' | 'INFORMATION.UNSUPPORTED' | + * 'WARNING.ALL' | 'WARNING.DEPRECATION' | 'WARNING.GENERIC' | 'WARNING.HINT' | + * 'WARNING.PERFORMANCE' | 'WARNING.UNRECOGNIZED' | 'WARNING.UNSUPPORTED' } NotificationFilter + */ +/** + * Defines the category filters available for a given severity level filter + * + * @typedef {object} CategoryFiltersInSeverityLevel + * @property {NotificationFilter} ALL + * @property {NotificationFilter} DEPRECATION + * @property {NotificationFilter} GENERIC + * @property {NotificationFilter} HINT + * @property {NotificationFilter} PERFORMANCE + * @property {NotificationFilter} UNRECOGNIZED + * @property {NotificationFilter} UNSUPPORTED + */ +/** + * Constants that represents the available notification filters + * + * @property {function(): Array} disabled Creates a configuration with notifications disabled + * @property {function(): Array} serverDefault Creates a configuration for using the server default + * @property {CategoryFiltersInSeverityLevel} ALL Filters with all severities for category + * @property {CategoryFiltersInSeverityLevel} WARNING Filters with warning severity for category + * @property {CategoryFiltersInSeverityLevel} INFORMATION Filters with information severity for category + */ +const notificationFilter: SeverityDotCategoryFilters & { + disabled: () => NotificationFilter[] + serverDefault: () => NotificationFilter[] +} = { + disabled: () => ['NONE'], + serverDefault: () => ['SERVER_DEFAULT'], + ALL: { + ALL: 'ALL.ALL', + DEPRECATION: 'ALL.DEPRECATION', + GENERIC: 'ALL.GENERIC', + HINT: 'ALL.HINT', + PERFORMANCE: 'ALL.PERFORMANCE', + UNRECOGNIZED: 'ALL.UNRECOGNIZED', + UNSUPPORTED: 'ALL.UNSUPPORTED' + }, + INFORMATION: { + ALL: 'INFORMATION.ALL', + DEPRECATION: 'INFORMATION.DEPRECATION', + GENERIC: 'INFORMATION.GENERIC', + HINT: 'INFORMATION.HINT', + PERFORMANCE: 'INFORMATION.PERFORMANCE', + UNRECOGNIZED: 'INFORMATION.UNRECOGNIZED', + UNSUPPORTED: 'INFORMATION.UNSUPPORTED' + }, + WARNING: { + ALL: 'WARNING.ALL', + DEPRECATION: 'WARNING.DEPRECATION', + GENERIC: 'WARNING.GENERIC', + HINT: 'WARNING.HINT', + PERFORMANCE: 'WARNING.PERFORMANCE', + UNRECOGNIZED: 'WARNING.UNRECOGNIZED', + UNSUPPORTED: 'WARNING.UNSUPPORTED' + } +} + +Object.freeze(notificationFilter) + +const filters = Object.values(notificationFilter) + .map(value => { + if (typeof value === 'function') { + return value() + } + return Object.values(value) + }) + .reduce((previous, current) => [...previous, ...current], []) + +/** + * @private + */ +function isValidFilter (value: any): boolean { + return filters.includes(value) +} + +export default notificationFilter + +export { + isValidFilter +} + +export type { + NotificationFilter +} diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 1431fefa2..ed2ec6708 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -419,6 +419,43 @@ interface NotificationPosition { column?: number } +type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' +/** + * @typedef {'WARNING' | 'INFORMATION' | 'UNKNOWN'} NotificationSeverityLevel + */ +/** + * Constants that represents the Severity level in the {@link Notification} + */ +const notificationSeverityLevel: { [key in NotificationSeverityLevel]: key } = { + WARNING: 'WARNING', + INFORMATION: 'INFORMATION', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationSeverityLevel) +const severityLevels = Object.values(notificationSeverityLevel) + +type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | +'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'DEPRECATION' | 'RUNTIME' | 'UNKNOWN'} NotificationCategory + */ +/** + * Constants that represents the Category in the {@link Notification} + */ +const notificationCategory: { [key in NotificationCategory]: key } = { + HINT: 'HINT', + UNRECOGNIZED: 'UNRECOGNIZED', + UNSUPPORTED: 'UNSUPPORTED', + PERFORMANCE: 'PERFORMANCE', + DEPRECATION: 'DEPRECATION', + GENERIC: 'GENERIC', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationCategory) +const categories = Object.values(notificationCategory) + /** * Class for Cypher notifications * @access public @@ -429,6 +466,10 @@ class Notification { description: string severity: string position: NotificationPosition | {} + severityLevel: NotificationSeverityLevel + category: NotificationCategory + rawSeverityLevel: string + rawCategory?: string /** * Create a Notification instance @@ -436,11 +477,113 @@ class Notification { * @param {Object} notification - Object with notification data */ constructor (notification: any) { + /** + * The code + * @type {string} + * @public + */ this.code = notification.code + /** + * The title + * @type {string} + * @public + */ this.title = notification.title + /** + * The description + * @type {string} + * @public + */ this.description = notification.description + /** + * The raw severity + * + * Use {@link Notification#rawSeverityLevel} for the raw value or {@link Notification#severityLevel} for an enumerated value. + * + * @type {string} + * @public + * @deprecated This property will be removed in 6.0. + */ this.severity = notification.severity + /** + * The position which the notification had occur. + * + * @type {NotificationPosition} + * @public + */ this.position = Notification._constructPosition(notification.position) + + /** + * The severity level + * + * @type {NotificationSeverityLevel} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const notification of summary.notifications) { + * switch(notification.severityLevel) { + * case neo4j.notificationSeverityLevel.INFORMATION: // or simply 'INFORMATION' + * console.info(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationSeverityLevel.WARNING: // or simply 'WARNING' + * console.warn(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationSeverityLevel.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawSeverityLevel + * console.log(`${notification.title} - ${notification.description}`) + * break + * } + * } + */ + this.severityLevel = severityLevels.includes(notification.severity) + ? notification.severity + : notificationSeverityLevel.UNKNOWN + + /** + * The severity level returned by the server without any validation. + * + * @type {string} + * @public + */ + this.rawSeverityLevel = notification.severity + + /** + * The category + * + * @type {NotificationCategory} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const notification of summary.notifications) { + * switch(notification.category) { + * case neo4j.notificationCategory.QUERY: // or simply 'QUERY' + * console.info(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationCategory.PERFORMANCE: // or simply 'PERFORMANCE' + * console.warn(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationCategory.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawCategory + * console.log(`${notification.title} - ${notification.description}`) + * break + * } + * } + */ + this.category = categories.includes(notification.category) + ? notification.category + : notificationCategory.UNKNOWN + + /** + * The category returned by the server without any validation. + * + * @type {string|undefined} + * @public + */ + this.rawCategory = notification.category } static _constructPosition (pos: NotificationPosition): NotificationPosition { @@ -540,10 +683,14 @@ export { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + notificationSeverityLevel, + notificationCategory } export type { - NotificationPosition + NotificationPosition, + NotificationSeverityLevel, + NotificationCategory } export default ResultSummary diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 09c5a1892..e3b245f34 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -37,6 +37,7 @@ import TransactionPromise from './transaction-promise' import ManagedTransaction from './transaction-managed' import BookmarkManager from './bookmark-manager' import { Dict } from './record' +import { NotificationFilter } from './notification-filter' type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise | Promise type TransactionWork = (tx: Transaction) => Promise | T @@ -72,6 +73,7 @@ class Session { private readonly _highRecordWatermark: number private readonly _results: Result[] private readonly _bookmarkManager?: BookmarkManager + private readonly _notificationFilters?: NotificationFilter[] /** * @constructor * @protected @@ -94,7 +96,8 @@ class Session { reactive, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilters }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -105,6 +108,7 @@ class Session { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }) { this._mode = mode this._database = database @@ -142,6 +146,7 @@ class Session { this._highRecordWatermark = calculatedWatermaks.high this._results = [] this._bookmarkManager = bookmarkManager + this._notificationFilters = notificationFilters } /** @@ -181,7 +186,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilters: this._notificationFilters }) }) this._results.push(result) @@ -292,6 +298,7 @@ class Session { const tx = new TransactionPromise({ connectionHolder, impersonatedUser: this._impersonatedUser, + notificationFilters: this._notificationFilters, onClose: this._transactionClosed.bind(this), onBookmarks: (newBm, oldBm, db) => this._updateBookmarks(newBm, oldBm, db), onConnection: this._assertSessionIsOpen.bind(this), diff --git a/packages/core/src/transaction-promise.ts b/packages/core/src/transaction-promise.ts index 04798195c..86c550876 100644 --- a/packages/core/src/transaction-promise.ts +++ b/packages/core/src/transaction-promise.ts @@ -26,6 +26,7 @@ import { import { Bookmarks } from './internal/bookmarks' import { TxConfig } from './internal/tx-config' +import { NotificationFilter } from './notification-filter' /** * Represents a {@link Promise} object and a {@link Transaction} object. @@ -65,7 +66,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }: { connectionHolder: ConnectionHolder onClose: () => void @@ -76,6 +78,7 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilters?: NotificationFilter[] }) { super({ connectionHolder, @@ -86,7 +89,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }) } diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 3bee6d480..bf413107c 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -38,6 +38,7 @@ import { newError } from './error' import Result from './result' import { Query } from './types' import { Dict } from './record' +import { NotificationFilter } from './notification-filter' /** * Represents a transaction in the Neo4j database. @@ -61,6 +62,7 @@ class Transaction { private _bookmarks: Bookmarks private readonly _activePromise: Promise private _acceptActive: () => void + private readonly _notificationFilters?: NotificationFilter[] /** * @constructor @@ -84,7 +86,8 @@ class Transaction { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }: { connectionHolder: ConnectionHolder onClose: () => void @@ -95,6 +98,7 @@ class Transaction { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilters?: NotificationFilter[] }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -110,6 +114,7 @@ class Transaction { this._lowRecordWatermak = lowRecordWatermark this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() + this._notificationFilters = notificationFilters this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -138,6 +143,7 @@ class Transaction { mode: this._connectionHolder.mode(), database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, + notificationFilters: this._notificationFilters, beforeError: (error: Error) => { if (events != null) { events.onError(error) diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index c7c4c4b71..32b90815c 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -17,7 +17,7 @@ * limitations under the License. */ /* eslint-disable @typescript-eslint/promise-function-async */ -import { bookmarkManager, ConnectionProvider, newError, ServerInfo, Session } from '../src' +import { bookmarkManager, ConnectionProvider, newError, NotificationFilter, notificationFilter, ServerInfo, Session } from '../src' import Driver, { READ } from '../src/driver' import { Bookmarks } from '../src/internal/bookmarks' import { Logger } from '../src/internal/logger' @@ -155,6 +155,107 @@ describe('Driver', () => { } }) }) + + describe('when set config.notificationFilters', () => { + it.each([ + [], + undefined, + null, + notificationFilter.disabled(), + notificationFilter.serverDefault(), + [notificationFilter.ALL.ALL, notificationFilter.INFORMATION.GENERIC], + ['WARNING.HINT', 'INFORMATION.GENERIC'] + ])('should send valid "notificationFilters" to the session', async (notificationFilters?: NotificationFilter[]) => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + const session = driver.session({ notificationFilters }) + + try { + expect(createSession).toBeCalledWith(expect.objectContaining({ + notificationFilters + })) + } finally { + await session.close() + await driver.close() + } + }) + + it.each([ + notificationFilter.ALL.DEPRECATION, + 'WARNING.HINT', + 'INFO', + 1234, + { 'WARNING.HINT': notificationFilter.WARNING.HINT }, + () => [notificationFilter.ALL.DEPRECATION] + ])('should thrown when "notificationFilters" is not an array', async (notificationFilters?: any) => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + try { + expect(() => driver.session({ notificationFilters })).toThrow(new TypeError('Expect "notificationFilters" to be instance of Array.')) + } finally { + await driver.close() + } + }) + + it('should throw when "NONE" is configured with other filters', async () => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + try { + expect(() => driver.session({ notificationFilters: ['NONE', 'ALL.DEPRECATION'] })) + .toThrow(new Error('Expect "notificationFilters" to not have "NONE" configured along with other filters.')) + } finally { + await driver.close() + } + }) + + it('should throw when "SERVER_DEFAULT" is configured with other filters', async () => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + try { + expect(() => driver.session({ notificationFilters: ['SERVER_DEFAULT', 'ALL.DEPRECATION'] })) + .toThrow(new Error('Expect "notificationFilters" to not have "SERVER_DEFAULT" configured along with other filters.')) + } finally { + await driver.close() + } + }) + + it('should throw when invalid filters are configured', async () => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + try { + // @ts-expect-error + expect(() => driver.session({ notificationFilters: ['ALL.DEPRECATION', 'ABC', 123] })) + .toThrow(new Error('Invalid "notificationFilters". Invalid values: "ABC", 123')) + } finally { + await driver.close() + } + }) + }) }) it.each([ @@ -307,6 +408,91 @@ describe('Driver', () => { promise?.catch(_ => 'Do nothing').finally(() => { }) }) + describe('constructor', () => { + describe('when set config.notificationFilters', () => { + it.each([ + [], + null, + undefined, + notificationFilter.disabled(), + notificationFilter.serverDefault(), + [notificationFilter.ALL.ALL, notificationFilter.INFORMATION.GENERIC], + ['WARNING.HINT', 'INFORMATION.GENERIC'] + ])('should send valid "notificationFilters" to the connection provider', async (notificationFilters?: NotificationFilter[]) => { + const createConnectionProviderMock = jest.fn(mockCreateConnectonProvider(connectionProvider)) + const driver = new Driver( + META_INFO, + { notificationFilters }, + createConnectionProviderMock, + createSession + ) + + driver._getOrCreateConnectionProvider() + + expect(createConnectionProviderMock).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ notificationFilters }), + expect.any(Object), + expect.any(Object) + ) + + await driver.close() + }) + + it.each([ + notificationFilter.ALL.DEPRECATION, + 'WARNING.HINT', + 'INFO', + 1234, + { 'WARNING.HINT': notificationFilter.WARNING.HINT }, + () => [notificationFilter.ALL.DEPRECATION] + ])('should thrown when "notificationFilters" is not an array', async (notificationFilters?: any) => { + const createConnectionProviderMock = mockCreateConnectonProvider(connectionProvider) + + expect(() => new Driver( + META_INFO, + { notificationFilters }, + createConnectionProviderMock, + createSession + )).toThrow(new TypeError('Expect "notificationFilters" to be instance of Array.')) + }) + + it('should throw when "NONE" is configured with other filters', async () => { + const createConnectionProviderMock = mockCreateConnectonProvider(connectionProvider) + + expect(() => new Driver( + META_INFO, + { notificationFilters: ['NONE', 'ALL.DEPRECATION'] }, + createConnectionProviderMock, + createSession + )).toThrow(new Error('Expect "notificationFilters" to not have "NONE" configured along with other filters.')) + }) + + it('should throw when "SERVER_DEFAULT" is configured with other filters', async () => { + const createConnectionProviderMock = mockCreateConnectonProvider(connectionProvider) + + expect(() => new Driver( + META_INFO, + { notificationFilters: ['ALL.DEPRECATION', 'SERVER_DEFAULT'] }, + createConnectionProviderMock, + createSession + )).toThrow(new Error('Expect "notificationFilters" to not have "SERVER_DEFAULT" configured along with other filters.')) + }) + + it('should throw when invalid filters are configured', async () => { + const createConnectionProviderMock = mockCreateConnectonProvider(connectionProvider) + + expect(() => new Driver( + META_INFO, + // @ts-expect-error + { notificationFilters: ['ALL.DEPRECATION', 'ABC', 123] }, + createConnectionProviderMock, + createSession + )).toThrow(new Error('Invalid "notificationFilters". Invalid values: "ABC", 123')) + }) + }) + }) + function mockCreateConnectonProvider (connectionProvider: ConnectionProvider) { return ( id: number, diff --git a/packages/core/test/result-summary.test.ts b/packages/core/test/result-summary.test.ts index cc3ea7c44..20b6fd77a 100644 --- a/packages/core/test/result-summary.test.ts +++ b/packages/core/test/result-summary.test.ts @@ -17,7 +17,14 @@ * limitations under the License. */ -import { ServerInfo } from '../src/result-summary' +import { + ServerInfo, + Notification, + NotificationSeverityLevel, + NotificationCategory, + notificationSeverityLevel, + notificationCategory +} from '../src/result-summary' describe('ServerInfo', () => { it.each([ @@ -48,3 +55,125 @@ describe('ServerInfo', () => { } ) }) + +describe('Notification', () => { + describe('.severityLevel', () => { + it.each(getValidSeverityLevels())('should fill severityLevel with the rawSeverityLevel equals to %s', rawSeverityLevel => { + const rawNotification = { + severity: rawSeverityLevel + } + + const notification = new Notification(rawNotification) + + expect(notification.severityLevel).toBe(rawSeverityLevel) + expect(notification.rawSeverityLevel).toBe(rawSeverityLevel) + }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should fill severityLevel UNKNOWN if the rawSeverityLevel equals to %s', rawSeverityLevel => { + const rawNotification = { + severity: rawSeverityLevel + } + + const notification = new Notification(rawNotification) + + expect(notification.severityLevel).toBe('UNKNOWN') + expect(notification.rawSeverityLevel).toBe(rawSeverityLevel) + }) + }) + + describe('.category', () => { + it.each(getValidCategories())('should fill category with the rawCategory equals to %s', rawCategory => { + const rawNotification = { + category: rawCategory + } + + const notification = new Notification(rawNotification) + + expect(notification.category).toBe(rawCategory) + expect(notification.rawCategory).toBe(rawCategory) + }) + + it.each([ + 'UNKNOWN', + undefined, + null, + 'DUNNO', + 'deprecation' + ])('should fill category with UNKNOWN the rawCategory equals to %s', rawCategory => { + const rawNotification = { + category: rawCategory + } + + const notification = new Notification(rawNotification) + + expect(notification.category).toBe('UNKNOWN') + expect(notification.rawCategory).toBe(rawCategory) + }) + }) +}) + +describe('notificationSeverityLevel', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationSeverityLevel)) { + expect(key).toEqual(value) + } + }) + + it('should have values assignable to NotificationSeverityLevel', () => { + for (const [, value] of Object.entries(notificationSeverityLevel)) { + const assignableValue: NotificationSeverityLevel = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidSeverityLevels())('should have %s as key', (severity) => { + const keys = Object.keys(notificationSeverityLevel) + expect(keys.includes(severity)).toBe(true) + }) +}) + +describe('notificationCategory', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationCategory)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationCategory', () => { + for (const [, value] of Object.entries(notificationCategory)) { + const assignableValue: NotificationCategory = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidCategories())('should have %s as key', (category) => { + const keys = Object.keys(notificationCategory) + expect(keys.includes(category)).toBe(true) + }) +}) + +function getValidSeverityLevels (): NotificationSeverityLevel[] { + return [ + 'WARNING', + 'INFORMATION', + 'UNKNOWN' + ] +} + +function getValidCategories (): NotificationCategory[] { + return [ + 'HINT', + 'UNRECOGNIZED', + 'UNSUPPORTED', + 'PERFORMANCE', + 'DEPRECATION', + 'GENERIC', + 'UNKNOWN' + ] +} diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index 6d1d1f417..c67d2d192 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConnectionProvider, Session, Connection, TransactionPromise, Transaction, BookmarkManager, bookmarkManager } from '../src' +import { ConnectionProvider, Session, Connection, TransactionPromise, Transaction, BookmarkManager, bookmarkManager, notificationFilter, NotificationFilter } from '../src' import { bookmarks } from '../src/internal' import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants' import ManagedTransaction from '../src/transaction-managed' @@ -446,6 +446,30 @@ describe('session', () => { expect(updateBookmarksSpy).not.toBeCalled() }) + + it.each([ + undefined, + [], + [notificationFilter.ALL.ALL], + [notificationFilter.INFORMATION.DEPRECATION, 'WARNING.HINT'] + ])('should call run query with notificationFilters', async (notificationFilters: NotificationFilter[]) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j', + notificationFilters + }) + + await session.beginTransaction() + + expect(connection.seenBeginTransaction[0][0]).toEqual( + expect.objectContaining({ + notificationFilters + }) + ) + }) }) describe('.commit()', () => { @@ -839,6 +863,30 @@ describe('session', () => { expect(updateBookmarksSpy).not.toBeCalled() }) + + it.each([ + undefined, + [], + [notificationFilter.ALL.ALL], + [notificationFilter.INFORMATION.DEPRECATION, 'WARNING.HINT'] + ])('should call run query with notificationFilters', async (notificationFilters: NotificationFilter[]) => { + const connection = newFakeConnection() + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j', + notificationFilters + }) + + await session.run('query') + + expect(connection.seenProtocolOptions[0]).toEqual( + expect.objectContaining({ + notificationFilters + }) + ) + }) }) }) @@ -890,7 +938,8 @@ function setupSession ({ fetchSize = 1000, database = '', lastBookmarks = bookmarks.Bookmarks.empty(), - bookmarkManager + bookmarkManager, + notificationFilters }: { connection: Connection beginTx?: boolean @@ -898,6 +947,7 @@ function setupSession ({ lastBookmarks?: bookmarks.Bookmarks database?: string bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }): { session: Session, connectionProvider: ConnectionProvider } { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = jest.fn(async () => await Promise.resolve(connection)) @@ -911,7 +961,8 @@ function setupSession ({ config: {}, reactive: false, bookmarks: lastBookmarks, - bookmarkManager + bookmarkManager, + notificationFilters }) if (beginTx) { diff --git a/packages/core/test/transaction.test.ts b/packages/core/test/transaction.test.ts index d61459ff3..358f8a210 100644 --- a/packages/core/test/transaction.test.ts +++ b/packages/core/test/transaction.test.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { ConnectionProvider, newError, Transaction, TransactionPromise } from '../src' +import { ConnectionProvider, newError, NotificationFilter, notificationFilter, Transaction, TransactionPromise } from '../src' import { Bookmarks } from '../src/internal/bookmarks' import { ConnectionHolder } from '../src/internal/connection-holder' import { TxConfig } from '../src/internal/tx-config' @@ -375,6 +375,28 @@ function testTx (transactionName: string, newTransaction: expect(connection.seenBeginTransaction.length).toEqual(1) expect(connection.seenQueries.length).toEqual(1) }) + + it.each([ + undefined, + [], + [notificationFilter.ALL.ALL], + [notificationFilter.INFORMATION.DEPRECATION, 'WARNING.HINT'] + ])('should call not run query with notificationFilters', async (notificationFilters: NotificationFilter[]) => { + const connection = newFakeConnection() + const tx = newTransaction({ + connection, + notificationFilters + }) + + tx._begin(async () => Bookmarks.empty(), TxConfig.empty()) + + await tx.run('RETURN 1') + expect(connection.seenProtocolOptions[0]).not.toEqual( + expect.objectContaining({ + notificationFilters + }) + ) + }) }) describe('.close()', () => { @@ -467,6 +489,7 @@ type TransactionFactory = (_: { fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number + notificationFilters?: NotificationFilter[] }) => T function newTransactionPromise ({ @@ -474,13 +497,15 @@ function newTransactionPromise ({ fetchSize = 1000, highRecordWatermark = 700, lowRecordWatermark = 300, - errorResolvingConnection = undefined + errorResolvingConnection = undefined, + notificationFilters }: { connection?: FakeConnection fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number errorResolvingConnection?: Error + notificationFilters?: NotificationFilter[] }): TransactionPromise { const connectionProvider = new ConnectionProvider() // @ts-expect-error @@ -504,7 +529,8 @@ function newTransactionPromise ({ fetchSize, impersonatedUser: '', highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }) return transaction @@ -514,12 +540,14 @@ function newRegularTransaction ({ connection, fetchSize = 1000, highRecordWatermark = 700, - lowRecordWatermark = 300 + lowRecordWatermark = 300, + notificationFilters }: { connection: FakeConnection fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number + notificationFilters?: NotificationFilter[] }): Transaction { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = async () => await Promise.resolve(connection) @@ -537,7 +565,8 @@ function newRegularTransaction ({ fetchSize, impersonatedUser: '', highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }) return transaction diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js index 21293dd7a..6a74ed98f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError } from '../../core/index.ts' +import { newError, json } from '../../core/index.ts' // eslint-disable-next-line no-unused-vars import { ResultStreamObserver } from './stream-observers.js' @@ -79,4 +79,23 @@ function assertImpersonatedUserIsEmpty (impersonatedUser, onProtocolError = () = } } -export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty } +/* Asserts that the passed-in notificationFilters is empty + * @param {string[]} notificationFilters + * @param {function (err:Error)} onProtocolError Called when it does have notificationFilters user set + * @param {any} observer + */ +function assertNotificationFiltersIsEmpty (notificationFilters, onProtocolError = () => {}, observer) { + if (notificationFilters !== undefined) { + const error = newError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to neo4j 5.3.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${json.stringify(notificationFilters)}.` + ) + // unsupported API was used, consider this a fatal error for the current connection + onProtocolError(error.message) + observer.onError(error) + throw error + } +} + +export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 20bf7a31b..3a35c328d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -19,7 +19,8 @@ import { assertDatabaseIsEmpty, assertTxConfigIsEmpty, - assertImpersonatedUserIsEmpty + assertImpersonatedUserIsEmpty, + assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' // eslint-disable-next-line no-unused-vars import { Chunker } from '../channel/index.js' @@ -149,14 +150,18 @@ export default class BoltProtocol { * @param {Object} param.authToken the authentication token. * @param {function(err: Error)} param.onError the callback to invoke on error. * @param {function()} param.onComplete the callback to invoke on completion. + * @param {?string[]} param.notificationFilters the filtering for notifications. * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write(RequestMessage.init(userAgent, authToken), observer, true) return observer @@ -177,6 +182,7 @@ export default class BoltProtocol { * @param {string} param.database the target database name. * @param {string} param.mode the access mode. * @param {string} param.impersonatedUser the impersonated user + * @param {?string[]} param.notificationFilters the filtering for notifications. * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. * @param {function()} param.beforeComplete the callback to invoke before handling the completion. @@ -189,6 +195,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -203,6 +210,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -285,6 +293,7 @@ export default class BoltProtocol { * @param {TxConfig} param.txConfig the transaction configuration. * @param {string} param.database the target database name. * @param {string} param.impersonatedUser the impersonated user + * @param {?string[]} param.notificationFilters the filtering for notifications. * @param {string} param.mode the access mode. * @param {function(keys: string[])} param.beforeKeys the callback to invoke before handling the keys. * @param {function(keys: string[])} param.afterKeys the callback to invoke after handling the keys. @@ -304,6 +313,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilters, beforeKeys, afterKeys, beforeError, @@ -327,6 +337,8 @@ export default class BoltProtocol { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // bookmarks and mode are ignored in this version of the protocol assertTxConfigIsEmpty(txConfig, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js index 57b5632ae..a0fa8363b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js @@ -18,7 +18,7 @@ */ import BoltProtocolV2 from './bolt-protocol-v2.js' import RequestMessage from './request-message.js' -import { assertDatabaseIsEmpty, assertImpersonatedUserIsEmpty } from './bolt-protocol-util.js' +import { assertDatabaseIsEmpty, assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' import { StreamObserver, LoginObserver, @@ -69,12 +69,15 @@ export default class BoltProtocol extends BoltProtocolV2 { return metadata } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write(RequestMessage.hello(userAgent, authToken), observer, true) return observer @@ -89,6 +92,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeError, afterError, @@ -104,6 +108,8 @@ export default class BoltProtocol extends BoltProtocolV2 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error @@ -166,6 +172,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeKeys, afterKeys, @@ -190,6 +197,8 @@ export default class BoltProtocol extends BoltProtocolV2 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js index a9eb5c856..bc371129d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js @@ -18,7 +18,7 @@ */ import BoltProtocolV3 from './bolt-protocol-v3.js' import RequestMessage from './request-message.js' -import { assertImpersonatedUserIsEmpty } from './bolt-protocol-util.js' +import { assertImpersonatedUserIsEmpty, assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' import { ResultStreamObserver, ProcedureRouteObserver @@ -56,6 +56,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeError, afterError, @@ -71,6 +72,8 @@ export default class BoltProtocol extends BoltProtocolV3 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) @@ -91,6 +94,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilters, mode, beforeKeys, afterKeys, @@ -121,6 +125,8 @@ export default class BoltProtocol extends BoltProtocolV3 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js index 001f41e68..b8ebdf333 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js @@ -23,6 +23,7 @@ import { internal } from '../../core/index.ts' import transformersFactories from './bolt-protocol-v4x1.transformer.js' import Transformer from './transformer.js' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' const { constants: { BOLT_PROTOCOL_V4_1 } @@ -72,12 +73,15 @@ export default class BoltProtocol extends BoltProtocolV4 { return this._transformer } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting), observer, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js index 86d62a1f1..e6e3626fe 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js @@ -25,6 +25,7 @@ import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' import Transformer from './transformer.js' import { internal } from '../../core/index.ts' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' const { bookmarks: { Bookmarks }, @@ -85,9 +86,10 @@ export default class BoltProtocol extends BoltProtocolV42 { * @param {any} param0.authToken The auth token * @param {function(error)} param0.onError On error callback * @param {function(onComplte)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => { @@ -98,6 +100,9 @@ export default class BoltProtocol extends BoltProtocolV42 { } }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting, ['utc']), observer, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js index 22d95f52b..5631a4e0d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js @@ -26,6 +26,8 @@ import transformersFactories from './bolt-protocol-v4x4.transformer.js' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' import Transformer from './transformer.js' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' + const { constants: { BOLT_PROTOCOL_V4_4, FETCH_ALL }, bookmarks: { Bookmarks } @@ -87,6 +89,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilters, beforeKeys, afterKeys, beforeError, @@ -116,6 +119,9 @@ export default class BoltProtocol extends BoltProtocolV43 { lowRecordWatermark }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + const flushRun = reactive this.write( RequestMessage.runWithMetadata(query, parameters, { @@ -142,6 +148,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilters, beforeError, afterError, beforeComplete, @@ -156,6 +163,9 @@ export default class BoltProtocol extends BoltProtocolV43 { }) observer.prepareToHandleSingleResponse() + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.begin({ bookmarks, txConfig, database, mode, impersonatedUser }), observer, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js index 4e30ccb0b..07cd95e7d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js @@ -24,6 +24,7 @@ import RequestMessage from './request-message.js' import { LoginObserver } from './stream-observers.js' import { internal } from '../../core/index.ts' +import { assertNotificationFiltersIsEmpty } from './bolt-protocol-util.js' const { constants: { BOLT_PROTOCOL_V5_0 } @@ -49,14 +50,18 @@ export default class BoltProtocol extends BoltProtocolV44 { * @param {any} param0.authToken The auth token * @param {function(error)} param0.onError On error callback * @param {function(onComplte)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filters user on this protocol version throws an error + assertNotificationFiltersIsEmpty(notificationFilters, this._onProtocolError, observer) + this.write( RequestMessage.hello(userAgent, authToken, this._serversideRouting), observer, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.js new file mode 100644 index 000000000..6ec334afe --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.js @@ -0,0 +1,186 @@ +/** + * Copyright (c) "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 BoltProtocolV5x0 from './bolt-protocol-v5x0.js' + +import transformersFactories from './bolt-protocol-v5x1.transformer.js' +import Transformer from './transformer.js' +import RequestMessage from './request-message.js' +import { LoginObserver, ResultStreamObserver } from './stream-observers.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V5_1, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x0 { + get version () { + return BOLT_PROTOCOL_V5_1 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Initialize a connection with the server + * + * @param {Object} param0 The params + * @param {string} param0.userAgent The user agent + * @param {any} param0.authToken The auth token + * @param {function(error)} param0.onError On error callback + * @param {function(onComplete)} param0.onComplete On complete callback + * @param {?string[]} param0.notificationFilters the filtering for notifications. + * @returns {LoginObserver} The Login observer + */ + initialize ({ userAgent, authToken, onError, onComplete, notificationFilters } = {}) { + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) + }) + + this.write( + RequestMessage.hello5x1(authToken, { + userAgent, + notificationFilters: sanitizeNotificationFilters(notificationFilters), + routing: this._serversideRouting + }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + reactive = false, + fetchSize = FETCH_ALL, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + reactive: reactive, + fetchSize: fetchSize, + moreFunction: this._requestMore.bind(this), + discardFunction: this._requestDiscard.bind(this), + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + const flushRun = reactive + this.write( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters: sanitizeNotificationFilters(notificationFilters) + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this.write( + RequestMessage.begin({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilters: sanitizeNotificationFilters(notificationFilters) + }), + observer, + true + ) + + return observer + } +} + +function sanitizeNotificationFilters (filters) { + if (filters == null || filters === []) { + return filters + } + + if (filters[0] === 'NONE') { + return [] + } + + if (filters[0] === 'SERVER_DEFAULT') { + return null + } + + return filters.map(filter => filter.replace(/^ALL\./, '*.').replace(/\.ALL$/, '.*')) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.transformer.js new file mode 100644 index 000000000..b8583e846 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x1.transformer.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) "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 v5x0 from './bolt-protocol-v5x0.transformer.js' + +export default { + ...v5x0 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js index 9a4549cd6..45c33eef2 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -27,6 +27,7 @@ import BoltProtocolV4x2 from './bolt-protocol-v4x2.js' import BoltProtocolV4x3 from './bolt-protocol-v4x3.js' import BoltProtocolV4x4 from './bolt-protocol-v4x4.js' import BoltProtocolV5x0 from './bolt-protocol-v5x0.js' +import BoltProtocolV5x1 from './bolt-protocol-v5x1.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -191,6 +192,16 @@ function createProtocol ( onProtocolError, serversideRouting ) + case 5.1: + return new BoltProtocolV5x1( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js index 515458ee5..f8c0de714 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js @@ -76,7 +76,7 @@ function parseNegotiatedResponse (buffer) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - version(5, 0), + [version(5, 1), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js index 0ceb430c0..acf7d1ac5 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js @@ -104,6 +104,7 @@ export default class RequestMessage { * @param {string} userAgent the user agent. * @param {Object} authToken the authentication token. * @param {Object} optional server side routing, set to routing context to turn on server side routing (> 4.1) + * @param {?string[]} patchs patches to be applied to the server (valid in 4.3 and 4.4) * @return {RequestMessage} new HELLO message. */ static hello (userAgent, authToken, routing = null, patchs = null) { @@ -121,6 +122,31 @@ export default class RequestMessage { ) } + /** + * Create a new HELLO message. + * @param {Object} authToken the authentication token. + * @param {Object} param1 the extra information + * @param {string} param1.userAgent the user agent + * @param {?Object} param1.routing the server side routing context, when set the server side routing is enabled + * @param {?string[]} param1.notificationFilters the cypher notification filters + */ + static hello5x1 (authToken, { userAgent, routing, notificationFilters } = {}) { + const extra = { user_agent: userAgent } + if (routing) { + extra.routing = routing + } + + if (notificationFilters !== undefined) { + extra.notifications = notificationFilters + } + + return new RequestMessage( + HELLO, + [authToken, extra], + () => `HELLO {...} ${json.stringify(extra)}` + ) + } + /** * Create a new BEGIN message. * @param {Bookmarks} bookmarks the bookmarks. @@ -128,10 +154,11 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {?string[]} notificationFilters the notification filters * @return {RequestMessage} new BEGIN message. */ - static begin ({ bookmarks, txConfig, database, mode, impersonatedUser } = {}) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + static begin ({ bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) return new RequestMessage( BEGIN, [metadata], @@ -164,14 +191,15 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {?string[]} notificationFilters the notification filters * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata ( query, parameters, - { bookmarks, txConfig, database, mode, impersonatedUser } = {} + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters } = {} ) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) return new RequestMessage( RUN, [query, parameters, metadata], @@ -282,9 +310,10 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user mode. + * @param {?string[]} notificationFilters the notification filters * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilters) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -301,6 +330,9 @@ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) if (impersonatedUser) { metadata.imp_user = assertString(impersonatedUser, 'impersonatedUser') } + if (notificationFilters !== undefined) { + metadata.notifications = notificationFilters + } if (mode === ACCESS_MODE_READ) { metadata.mode = READ_MODE } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 208cbd585..5d72ee881 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -34,6 +34,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._log = log this._userAgent = userAgent this._authToken = authToken + this._notificationFilters = this._config.notificationFilters this._createChannelConnection = createChannelConnectionHook || (address => { @@ -76,7 +77,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } this._openConnections[connection.id] = connection return connection - .connect(this._userAgent, this._authToken) + .connect(this._userAgent, this._authToken, this._notificationFilters) .catch(error => { // let's destroy this connection this._destroyConnection(connection) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 8dbcbc509..84af8c3d2 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -169,24 +169,27 @@ export default class ChannelConnection extends Connection { * Send initialization message. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { - return this._initialize(userAgent, authToken) + connect (userAgent, authToken, notificationFilters) { + return this._initialize(userAgent, authToken, notificationFilters) } /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if initialization is successful. Rejected promise otherwise. */ - _initialize (userAgent, authToken) { + _initialize (userAgent, authToken, notificationFilters) { const self = this return new Promise((resolve, reject) => { this._protocol.initialize({ userAgent, authToken, + notificationFilters, onError: err => reject(err), onComplete: metadata => { if (metadata) { @@ -340,13 +343,14 @@ export default class ChannelConnection extends Connection { }) } - _reset(observer) { + _reset (observer) { if (this._reseting) { if (!this._protocol.isLastMessageReset()) { this._protocol.reset({ onError: error => { observer.onError(error) - }, onComplete: () => { + }, + onComplete: () => { observer.onComplete() } }) @@ -369,7 +373,8 @@ export default class ChannelConnection extends Connection { this._protocol.reset({ onError: error => { notifyFinish(obs => obs.onError(error)) - }, onComplete: () => { + }, + onComplete: () => { notifyFinish(obs => obs.onComplete()) } }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js index 6d195d1d9..36b2694d3 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js @@ -71,8 +71,8 @@ export default class DelegateConnection extends Connection { return this._delegate.protocol() } - connect (userAgent, authToken) { - return this._delegate.connect(userAgent, authToken) + connect (userAgent, authToken, notificationFilters) { + return this._delegate.connect(userAgent, authToken, notificationFilters) } write (message, observer, flush) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js index dc996522c..fa65045f0 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js @@ -79,9 +79,10 @@ export default class Connection { * Connect to the target address, negotiate Bolt protocol and send initialization message. * @param {string} userAgent the user agent for this driver. * @param {Object} authToken the object containing auth information. + * @param {?string[]} notificationFilters the notification filters. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { + connect (userAgent, authToken, notificationFilters) { throw new Error('not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index cde7a6865..5b8095510 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -20,6 +20,7 @@ import ConnectionProvider from './connection-provider.ts' import { Bookmarks } from './internal/bookmarks.ts' import ConfiguredCustomResolver from './internal/resolver/configured-custom-resolver.ts' +import { isValidFilter, NotificationFilter } from './notification-filter.ts' import { ACCESS_MODE_READ, @@ -41,6 +42,7 @@ import { } from './types.ts' import { ServerAddress } from './internal/server-address.ts' import BookmarkManager from './bookmark-manager.ts' +import { stringify } from './json.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -89,6 +91,7 @@ type CreateSession = (args: { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }) => Session interface DriverConfig { @@ -96,6 +99,7 @@ interface DriverConfig { trust?: TrustStrategy fetchSize?: number logging?: LoggingConfig + notificationFilters?: NotificationFilter[] } /** @@ -110,6 +114,7 @@ class SessionConfig { impersonatedUser?: string fetchSize?: number bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] /** * @constructor @@ -159,10 +164,10 @@ class SessionConfig { * Enabling it is done by supplying an BookmarkManager implementation instance to this param. * A default implementation could be acquired by calling the factory function {@link bookmarkManager}. * - * **Warning**: Share the same BookmarkManager instance accross all session can have a negative impact + * **Warning**: Share the same BookmarkManager instance across all session can have a negative impact * on performance since all the queries will wait for the latest changes being propagated across the cluster. * For keeping consistency between a group of queries, use {@link Session} for grouping them. - * For keeping consistency between a group of sessions, use {@link BookmarkManager} instance for groupping them. + * For keeping consistency between a group of sessions, use {@link BookmarkManager} instance for grouping them. * * @example * const bookmarkManager = neo4j.bookmarkManager() @@ -179,7 +184,7 @@ class SessionConfig { * * // Reading Driver User will wait of the changes being propagated to the server before RUN the query * // So the 'Driver User' person should exist in the Result, unless deleted. - * const linkedSesssion2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) + * const linkedSession2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) * * await linkedSession1.close() * await linkedSession2.close() @@ -190,6 +195,63 @@ class SessionConfig { * @since 5.0 */ this.bookmarkManager = undefined + + /** + * Configure filter for {@link Notification} objects returned in {@link ResultSummary#notifications}. + * + * The filters are defined by "{@link NotificationSeverityLevel}.{@link NotificationCategory}" with the + * exception of the "UNKNOWN" severity and category. + * Additionally, "ALL" can be set as category, severity, or both. + * + * Disabling the filters is done by setting this configuration to ['NONE']. + * Using the default values is done by setting this configuration to ['SERVER_DEFAULT']. + * + * Helper constants and methods are defined at {@link notificationFilter}. + * + * @example + * // disabling notifications + * const sessionWithoutNotifications = driver.session({ database: 'neo4j', notificationFilters: neo4j.notificationFilter.disabled() }) + * // EQUIVALENT TO: const sessionWithoutNotifications = driver.session({ database:'neo4j', notificationFilters: ["NONE"] }) + * + * // using default server configuration + * const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j', notificationFilters: neo4j.notificationFilter.serverDefault() }) + * // EQUIVALENT TO: const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j', notificationFilters: ["SERVER_DEFAULT"] }) + * + * // using default configured in the connection/driver configuration + * const sessionWithSeverDefaultNotifications = driver.session({ database: 'neo4j' }) + * + * // Enable all notifications + * const sessionWithAllNotifications = driver.session({ database: 'neo4j', notificationFilters: [neo4j.notificationFilter.ALL.ALL] }) + * // EQUIVALENT TO: const sessionWithAllNotifications = driver.session({ database: 'neo4j', notificationFilters: ['ALL.ALL'] }) + * + * // Configuring for any categories with severity 'WARNING', + * // or any severity with category 'HINT' + * // or severity 'INFORMATION' and category 'PERFORMANCE'. + * const sessionFineConfigured = driver.session({ + * database: 'neo4j', + * notificationFilters: [ + * neo4j.notificationFilter.WARNING.ALL, + * neo4j.notificationFilter.ALL.HINT, + * neo4j.notificationFilter.INFORMATION.PERFORMANCE + * ] + * }) + * + * // Configuring for any categories with severity 'WARNING', + * // or any severity with category 'HINT' + * // or severity 'INFORMATION' and category 'PERFORMANCE'. + * // const sessionFineConfigured = driver.session({ + * // database: 'neo4j', + * // notificationFilters: [ + * // 'WARNING.ALL', + * // 'ALL.HINT', + * // 'INFORMATION.PERFORMANCE' + * // ] + * // }) + * + * @type {NotificationFilter[]|undefined} + * @since 5.3 + */ + this.notificationFilters = undefined } } @@ -388,7 +450,8 @@ class Driver { database = '', impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -398,7 +461,8 @@ class Driver { impersonatedUser, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), - bookmarkManager + bookmarkManager, + notificationFilters }) } @@ -435,7 +499,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -444,7 +509,9 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }): Session { + validateNotificationFilters(notificationFilters) const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() const bookmarks = bookmarkOrBookmarks != null @@ -460,7 +527,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilters }) } @@ -501,6 +569,9 @@ function validateConfig (config: any, log: Logger): any { 'where a new connection is created while it is acquired' ) } + + validateNotificationFilters(config.notificationFilters) + return config } @@ -589,6 +660,35 @@ function createHostNameResolver (config: any): ConfiguredCustomResolver { return new ConfiguredCustomResolver(config.resolver) } -export { Driver, READ, WRITE } -export type { SessionConfig } +/** + * @private + */ +function validateNotificationFilters (filters: any): void { + if (filters == null || filters === []) { + return filters + } + + if (!Array.isArray(filters)) { + throw new TypeError('Expect "notificationFilters" to be instance of Array.') + } + + if (filters.length > 1) { + if (filters.includes('NONE')) { + throw new Error('Expect "notificationFilters" to not have "NONE" configured along with other filters.') + } + + if (filters.includes('SERVER_DEFAULT')) { + throw new Error('Expect "notificationFilters" to not have "SERVER_DEFAULT" configured along with other filters.') + } + } + + const invalidFilters = filters.filter(filter => !isValidFilter(filter)) + + if (invalidFilters.length > 0) { + const invalidValuesString = invalidFilters.map(stringify).join(', ') + throw new Error(`Invalid "notificationFilters". Invalid values: ${invalidValuesString}`) + } +} + +export { Driver, READ, WRITE, SessionConfig } export default Driver diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 1cd30eeea..85798c2b7 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -64,8 +64,13 @@ import ResultSummary, { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + NotificationSeverityLevel, + NotificationCategory, + notificationCategory, + notificationSeverityLevel } from './result-summary.ts' +import notificationFilter, { NotificationFilter } from './notification-filter.ts' import Result, { QueryResult, ResultObserver } from './result.ts' import ConnectionProvider from './connection-provider.ts' import Connection from './connection.ts' @@ -149,7 +154,10 @@ const forExport = { driver, json, auth, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export { @@ -209,7 +217,11 @@ export { driver, json, auth, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter, + SessionConfig } export type { @@ -221,7 +233,9 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, - SessionConfig + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter } export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index 39e790f2f..1b9519387 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -34,6 +34,7 @@ const BOLT_PROTOCOL_V4_2: number = 4.2 const BOLT_PROTOCOL_V4_3: number = 4.3 const BOLT_PROTOCOL_V4_4: number = 4.4 const BOLT_PROTOCOL_V5_0: number = 5.0 +const BOLT_PROTOCOL_V5_1: number = 5.1 export { FETCH_ALL, @@ -50,5 +51,6 @@ export { BOLT_PROTOCOL_V4_2, BOLT_PROTOCOL_V4_3, BOLT_PROTOCOL_V4_4, - BOLT_PROTOCOL_V5_0 + BOLT_PROTOCOL_V5_0, + BOLT_PROTOCOL_V5_1 } diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts new file mode 100644 index 000000000..5ed38e1ae --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) "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 { NotificationCategory, NotificationSeverityLevel } from './result-summary.ts' + +type Combine = `${Enum1}${Separator}${Enum2}` +type ExcludeUnknown = Exclude + +type FilterKeys = ExcludeUnknown | 'ALL' +type FilterInnerKeys = ExcludeUnknown | 'ALL' +type CombinedFilter = Combine + +type SeverityDotCategoryFilters = { [key in FilterKeys]: { [k in FilterInnerKeys]: NotificationFilter } } + +type NotificationFilter = 'NONE' | 'SERVER_DEFAULT' | CombinedFilter + +/** + * + * Notification filters used for configuring {@link Driver} and {@link Session}. + * + * @typedef { 'NONE' | 'SERVER_DEFAULT' | + * 'ALL.ALL' | 'ALL.DEPRECATION' | 'ALL.GENERIC' | 'ALL.HINT' | + * 'ALL.PERFORMANCE' | 'ALL.UNRECOGNIZED' | 'ALL.UNSUPPORTED' | + * 'INFORMATION.ALL' | 'INFORMATION.DEPRECATION' | 'INFORMATION.GENERIC' | 'INFORMATION.HINT' | + * 'INFORMATION.PERFORMANCE' | 'INFORMATION.UNRECOGNIZED' | 'INFORMATION.UNSUPPORTED' | + * 'WARNING.ALL' | 'WARNING.DEPRECATION' | 'WARNING.GENERIC' | 'WARNING.HINT' | + * 'WARNING.PERFORMANCE' | 'WARNING.UNRECOGNIZED' | 'WARNING.UNSUPPORTED' } NotificationFilter + */ +/** + * Defines the category filters available for a given severity level filter + * + * @typedef {object} CategoryFiltersInSeverityLevel + * @property {NotificationFilter} ALL + * @property {NotificationFilter} DEPRECATION + * @property {NotificationFilter} GENERIC + * @property {NotificationFilter} HINT + * @property {NotificationFilter} PERFORMANCE + * @property {NotificationFilter} UNRECOGNIZED + * @property {NotificationFilter} UNSUPPORTED + */ +/** + * Constants that represents the available notification filters + * + * @property {function(): Array} disabled Creates a configuration with notifications disabled + * @property {function(): Array} serverDefault Creates a configuration for using the server default + * @property {CategoryFiltersInSeverityLevel} ALL Filters with all severities for category + * @property {CategoryFiltersInSeverityLevel} WARNING Filters with warning severity for category + * @property {CategoryFiltersInSeverityLevel} INFORMATION Filters with information severity for category + */ +const notificationFilter: SeverityDotCategoryFilters & { + disabled: () => NotificationFilter[] + serverDefault: () => NotificationFilter[] +} = { + disabled: () => ['NONE'], + serverDefault: () => ['SERVER_DEFAULT'], + ALL: { + ALL: 'ALL.ALL', + DEPRECATION: 'ALL.DEPRECATION', + GENERIC: 'ALL.GENERIC', + HINT: 'ALL.HINT', + PERFORMANCE: 'ALL.PERFORMANCE', + UNRECOGNIZED: 'ALL.UNRECOGNIZED', + UNSUPPORTED: 'ALL.UNSUPPORTED' + }, + INFORMATION: { + ALL: 'INFORMATION.ALL', + DEPRECATION: 'INFORMATION.DEPRECATION', + GENERIC: 'INFORMATION.GENERIC', + HINT: 'INFORMATION.HINT', + PERFORMANCE: 'INFORMATION.PERFORMANCE', + UNRECOGNIZED: 'INFORMATION.UNRECOGNIZED', + UNSUPPORTED: 'INFORMATION.UNSUPPORTED' + }, + WARNING: { + ALL: 'WARNING.ALL', + DEPRECATION: 'WARNING.DEPRECATION', + GENERIC: 'WARNING.GENERIC', + HINT: 'WARNING.HINT', + PERFORMANCE: 'WARNING.PERFORMANCE', + UNRECOGNIZED: 'WARNING.UNRECOGNIZED', + UNSUPPORTED: 'WARNING.UNSUPPORTED' + } +} + +Object.freeze(notificationFilter) + +const filters = Object.values(notificationFilter) + .map(value => { + if (typeof value === 'function') { + return value() + } + return Object.values(value) + }) + .reduce((previous, current) => [...previous, ...current], []) + +/** + * @private + */ +function isValidFilter (value: any): boolean { + return filters.includes(value) +} + +export default notificationFilter + +export { + isValidFilter +} + +export type { + NotificationFilter +} diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index 771075922..700f83631 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -419,6 +419,43 @@ interface NotificationPosition { column?: number } +type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' +/** + * @typedef {'WARNING' | 'INFORMATION' | 'UNKNOWN'} NotificationSeverityLevel + */ +/** + * Constants that represents the Severity level in the {@link Notification} + */ +const notificationSeverityLevel: { [key in NotificationSeverityLevel]: key } = { + WARNING: 'WARNING', + INFORMATION: 'INFORMATION', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationSeverityLevel) +const severityLevels = Object.values(notificationSeverityLevel) + +type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | +'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'DEPRECATION' | 'RUNTIME' | 'UNKNOWN'} NotificationCategory + */ +/** + * Constants that represents the Category in the {@link Notification} + */ +const notificationCategory: { [key in NotificationCategory]: key } = { + HINT: 'HINT', + UNRECOGNIZED: 'UNRECOGNIZED', + UNSUPPORTED: 'UNSUPPORTED', + PERFORMANCE: 'PERFORMANCE', + DEPRECATION: 'DEPRECATION', + GENERIC: 'GENERIC', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationCategory) +const categories = Object.values(notificationCategory) + /** * Class for Cypher notifications * @access public @@ -429,6 +466,10 @@ class Notification { description: string severity: string position: NotificationPosition | {} + severityLevel: NotificationSeverityLevel + category: NotificationCategory + rawSeverityLevel: string + rawCategory?: string /** * Create a Notification instance @@ -436,11 +477,113 @@ class Notification { * @param {Object} notification - Object with notification data */ constructor (notification: any) { + /** + * The code + * @type {string} + * @public + */ this.code = notification.code + /** + * The title + * @type {string} + * @public + */ this.title = notification.title + /** + * The description + * @type {string} + * @public + */ this.description = notification.description + /** + * The raw severity + * + * Use {@link Notification#rawSeverityLevel} for the raw value or {@link Notification#severityLevel} for an enumerated value. + * + * @type {string} + * @public + * @deprecated This property will be removed in 6.0. + */ this.severity = notification.severity + /** + * The position which the notification had occur. + * + * @type {NotificationPosition} + * @public + */ this.position = Notification._constructPosition(notification.position) + + /** + * The severity level + * + * @type {NotificationSeverityLevel} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const notification of summary.notifications) { + * switch(notification.severityLevel) { + * case neo4j.notificationSeverityLevel.INFORMATION: // or simply 'INFORMATION' + * console.info(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationSeverityLevel.WARNING: // or simply 'WARNING' + * console.warn(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationSeverityLevel.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawSeverityLevel + * console.log(`${notification.title} - ${notification.description}`) + * break + * } + * } + */ + this.severityLevel = severityLevels.includes(notification.severity) + ? notification.severity + : notificationSeverityLevel.UNKNOWN + + /** + * The severity level returned by the server without any validation. + * + * @type {string} + * @public + */ + this.rawSeverityLevel = notification.severity + + /** + * The category + * + * @type {NotificationCategory} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const notification of summary.notifications) { + * switch(notification.category) { + * case neo4j.notificationCategory.QUERY: // or simply 'QUERY' + * console.info(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationCategory.PERFORMANCE: // or simply 'PERFORMANCE' + * console.warn(`${notification.title} - ${notification.description}`) + * break + * case neo4j.notificationCategory.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawCategory + * console.log(`${notification.title} - ${notification.description}`) + * break + * } + * } + */ + this.category = categories.includes(notification.category) + ? notification.category + : notificationCategory.UNKNOWN + + /** + * The category returned by the server without any validation. + * + * @type {string|undefined} + * @public + */ + this.rawCategory = notification.category } static _constructPosition (pos: NotificationPosition): NotificationPosition { @@ -540,10 +683,14 @@ export { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + notificationSeverityLevel, + notificationCategory } export type { - NotificationPosition + NotificationPosition, + NotificationSeverityLevel, + NotificationCategory } export default ResultSummary diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index fd6770345..82b4a6adb 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -37,6 +37,7 @@ import TransactionPromise from './transaction-promise.ts' import ManagedTransaction from './transaction-managed.ts' import BookmarkManager from './bookmark-manager.ts' import { Dict } from './record.ts' +import { NotificationFilter } from './notification-filter.ts' type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise | Promise type TransactionWork = (tx: Transaction) => Promise | T @@ -72,6 +73,7 @@ class Session { private readonly _highRecordWatermark: number private readonly _results: Result[] private readonly _bookmarkManager?: BookmarkManager + private readonly _notificationFilters?: NotificationFilter[] /** * @constructor * @protected @@ -94,7 +96,8 @@ class Session { reactive, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilters }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -105,6 +108,7 @@ class Session { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilters?: NotificationFilter[] }) { this._mode = mode this._database = database @@ -142,6 +146,7 @@ class Session { this._highRecordWatermark = calculatedWatermaks.high this._results = [] this._bookmarkManager = bookmarkManager + this._notificationFilters = notificationFilters } /** @@ -181,7 +186,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilters: this._notificationFilters }) }) this._results.push(result) @@ -292,6 +298,7 @@ class Session { const tx = new TransactionPromise({ connectionHolder, impersonatedUser: this._impersonatedUser, + notificationFilters: this._notificationFilters, onClose: this._transactionClosed.bind(this), onBookmarks: (newBm, oldBm, db) => this._updateBookmarks(newBm, oldBm, db), onConnection: this._assertSessionIsOpen.bind(this), diff --git a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts index 157588735..8cd54285d 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts @@ -26,6 +26,7 @@ import { import { Bookmarks } from './internal/bookmarks.ts' import { TxConfig } from './internal/tx-config.ts' +import { NotificationFilter } from './notification-filter.ts' /** * Represents a {@link Promise} object and a {@link Transaction} object. @@ -65,7 +66,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }: { connectionHolder: ConnectionHolder onClose: () => void @@ -76,6 +78,7 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilters?: NotificationFilter[] }) { super({ connectionHolder, @@ -86,7 +89,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }) } diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts index 5e4b98b96..e09b86c88 100644 --- a/packages/neo4j-driver-deno/lib/core/transaction.ts +++ b/packages/neo4j-driver-deno/lib/core/transaction.ts @@ -38,6 +38,7 @@ import { newError } from './error.ts' import Result from './result.ts' import { Query } from './types.ts' import { Dict } from './record.ts' +import { NotificationFilter } from './notification-filter.ts' /** * Represents a transaction in the Neo4j database. @@ -61,6 +62,7 @@ class Transaction { private _bookmarks: Bookmarks private readonly _activePromise: Promise private _acceptActive: () => void + private readonly _notificationFilters?: NotificationFilter[] /** * @constructor @@ -84,7 +86,8 @@ class Transaction { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilters }: { connectionHolder: ConnectionHolder onClose: () => void @@ -95,6 +98,7 @@ class Transaction { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilters?: NotificationFilter[] }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -110,6 +114,7 @@ class Transaction { this._lowRecordWatermak = lowRecordWatermark this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() + this._notificationFilters = notificationFilters this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -138,6 +143,7 @@ class Transaction { mode: this._connectionHolder.mode(), database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, + notificationFilters: this._notificationFilters, beforeError: (error: Error) => { if (events != null) { events.onError(error) diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 0c221215b..3e6097a6c 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -73,7 +73,13 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + notificationCategory, + notificationSeverityLevel, + NotificationSeverityLevel, + NotificationCategory, + notificationFilter, + NotificationFilter } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -218,7 +224,11 @@ const { * return ['127.0.0.1:8888', 'fallback.db.com:7687']; * }, * - * // Optionally override the default user agent name. + * // Configure filter for Notification objects returned in ResultSummary#notifications. + * // See SessionConfig#notificationFilters for usage instructions. + * notificationFilters: ['SERVER_DEFAULT'] + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -466,7 +476,10 @@ const forExport = { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export { @@ -519,7 +532,10 @@ export { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export type { QueryResult, @@ -532,6 +548,9 @@ export type { NotificationPosition, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index ed5abc899..4f00800bc 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -73,7 +73,13 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + notificationCategory, + notificationSeverityLevel, + NotificationSeverityLevel, + NotificationCategory, + notificationFilter, + NotificationFilter } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -217,7 +223,11 @@ const { * return ['127.0.0.1:8888', 'fallback.db.com:7687']; * }, * - * // Optionally override the default user agent name. + * // Configure filter for Notification objects returned in ResultSummary#notifications. + * // See SessionConfig#notificationFilters for usage instructions. + * notificationFilters: ['SERVER_DEFAULT'] + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -465,7 +475,10 @@ const forExport = { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export { @@ -518,7 +531,10 @@ export { DateTime, ConnectionProvider, Connection, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export type { QueryResult, @@ -531,6 +547,9 @@ export type { NotificationPosition, BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter } export default forExport diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index c19f37ae6..b06034dcc 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -307,4 +307,31 @@ describe('index', () => { const date: DateTime = new DateTime(1, 2, 3, 3, 5, 6, 6, 5) expect(date).toBeDefined() }) + + it('should export notificationSeverityLevel', () => { + expect(neo4j.notificationSeverityLevel).toBeDefined() + expect(neo4j.notificationSeverityLevel.WARNING).toBeDefined() + expect(neo4j.notificationSeverityLevel.INFORMATION).toBeDefined() + expect(neo4j.notificationSeverityLevel.UNKNOWN).toBeDefined() + }) + + it('should export notificationCategory', () => { + expect(neo4j.notificationCategory).toBeDefined() + expect(neo4j.notificationCategory.HINT).toBeDefined() + expect(neo4j.notificationCategory.UNRECOGNIZED).toBeDefined() + expect(neo4j.notificationCategory.UNSUPPORTED).toBeDefined() + expect(neo4j.notificationCategory.PERFORMANCE).toBeDefined() + expect(neo4j.notificationCategory.DEPRECATION).toBeDefined() + expect(neo4j.notificationCategory.GENERIC).toBeDefined() + expect(neo4j.notificationCategory.UNKNOWN).toBeDefined() + }) + + it('should export notificationFilter', () => { + expect(neo4j.notificationFilter).toBeDefined() + expect(neo4j.notificationFilter.ALL).toBeDefined() + expect(neo4j.notificationFilter.INFORMATION).toBeDefined() + expect(neo4j.notificationFilter.WARNING).toBeDefined() + expect(neo4j.notificationFilter.disabled).toBeDefined() + expect(neo4j.notificationFilter.serverDefault).toBeDefined() + }) }) diff --git a/packages/neo4j-driver/src/driver.js b/packages/neo4j-driver/src/driver.js index 491c43f02..1c73be7d1 100644 --- a/packages/neo4j-driver/src/driver.js +++ b/packages/neo4j-driver/src/driver.js @@ -58,7 +58,8 @@ class Driver extends CoreDriver { database = '', fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilters } = {}) { return new RxSession({ session: this._newSession({ @@ -68,7 +69,8 @@ class Driver extends CoreDriver { impersonatedUser, reactive: false, fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize), - bookmarkManager + bookmarkManager, + notificationFilters }), config: this._config }) diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 101e5ab6e..cbff2c9a2 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -61,7 +61,10 @@ import { Session, Transaction, ManagedTransaction, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -200,7 +203,11 @@ const { * return ['127.0.0.1:8888', 'fallback.db.com:7687']; * }, * - * // Optionally override the default user agent name. + * // Configure filter for Notification objects returned in ResultSummary#notifications. + * // See SessionConfig#notificationFilters for usage instructions. + * notificationFilters: ['SERVER_DEFAULT'] + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -455,7 +462,10 @@ const forExport = { Date, LocalDateTime, DateTime, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export { @@ -509,6 +519,9 @@ export { Date, LocalDateTime, DateTime, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export default forExport diff --git a/packages/neo4j-driver/test/types/index.test.ts b/packages/neo4j-driver/test/types/index.test.ts index 97dbbde01..401042e1d 100644 --- a/packages/neo4j-driver/test/types/index.test.ts +++ b/packages/neo4j-driver/test/types/index.test.ts @@ -28,7 +28,13 @@ import { session, spatial, temporal, - DateTime + DateTime, + notificationSeverityLevel, + NotificationSeverityLevel, + notificationCategory, + NotificationCategory, + notificationFilter, + NotificationFilter } from '../../types/index' import Driver from '../../types/driver' @@ -86,3 +92,73 @@ const isNeo4jLocalDateTime: boolean = temporal.isLocalDateTime({}) const isNeo4jLocalTime: boolean = temporal.isLocalTime({}) const isNeo4jTime: boolean = temporal.isTime({}) const dateTime = DateTime.fromStandardDate(new Date()) + +const unknownSeverityString: string = notificationSeverityLevel.UNKNOWN +const warningSeverityString: string = notificationSeverityLevel.WARNING +const informationSeverityString: string = notificationSeverityLevel.INFORMATION +const unknownSeverity: NotificationSeverityLevel = notificationSeverityLevel.UNKNOWN +const warningSeverity: NotificationSeverityLevel = notificationSeverityLevel.WARNING +const informationSeverity: NotificationSeverityLevel = notificationSeverityLevel.INFORMATION + +const hintCategoryString: string = notificationCategory.HINT +const deprecationCategoryString: string = notificationCategory.DEPRECATION +const performanceCategoryString: string = notificationCategory.PERFORMANCE +const genericCategoryString: string = notificationCategory.GENERIC +const unrecognizedCategoryString: string = notificationCategory.UNRECOGNIZED +const unsupportedCategoryString: string = notificationCategory.UNSUPPORTED +const unknownCategoryString: string = notificationCategory.UNKNOWN +const hintCategory: NotificationCategory = notificationCategory.HINT +const deprecationCategory: NotificationCategory = notificationCategory.DEPRECATION +const performanceCategory: NotificationCategory = notificationCategory.PERFORMANCE +const genericCategory: NotificationCategory = notificationCategory.GENERIC +const unrecognizedCategory: NotificationCategory = notificationCategory.UNRECOGNIZED +const unsupportedCategory: NotificationCategory = notificationCategory.UNSUPPORTED +const unknownCategory: NotificationCategory = notificationCategory.UNKNOWN + +const allAllString: string = notificationFilter.ALL.ALL +const allDeprecationString: string = notificationFilter.ALL.DEPRECATION +const allPerformanceString: string = notificationFilter.ALL.PERFORMANCE +const allGenericString: string = notificationFilter.ALL.GENERIC +const allUnrecognizedString: string = notificationFilter.ALL.UNRECOGNIZED +const allUnsupportedString: string = notificationFilter.ALL.UNSUPPORTED +const allHintString: string = notificationFilter.ALL.HINT + +const allAllFilter: NotificationFilter = notificationFilter.ALL.ALL +const allDeprecationFilter: NotificationFilter = notificationFilter.ALL.DEPRECATION +const allPerformanceFilter: NotificationFilter = notificationFilter.ALL.PERFORMANCE +const allGenericFilter: NotificationFilter = notificationFilter.ALL.GENERIC +const allUnrecognizedFilter: NotificationFilter = notificationFilter.ALL.UNRECOGNIZED +const allUnsupportedFilter: NotificationFilter = notificationFilter.ALL.UNSUPPORTED +const allHintFilter: NotificationFilter = notificationFilter.ALL.HINT + +const informationAllString: string = notificationFilter.INFORMATION.ALL +const informationDeprecationString: string = notificationFilter.INFORMATION.DEPRECATION +const informationPerformanceString: string = notificationFilter.INFORMATION.PERFORMANCE +const informationGenericString: string = notificationFilter.INFORMATION.GENERIC +const informationUnrecognizedString: string = notificationFilter.INFORMATION.UNRECOGNIZED +const informationUnsupportedString: string = notificationFilter.INFORMATION.UNSUPPORTED +const informationHintString: string = notificationFilter.INFORMATION.HINT + +const informationAllFilter: NotificationFilter = notificationFilter.INFORMATION.ALL +const informationDeprecationFilter: NotificationFilter = notificationFilter.INFORMATION.DEPRECATION +const informationPerformanceFilter: NotificationFilter = notificationFilter.INFORMATION.PERFORMANCE +const informationGenericFilter: NotificationFilter = notificationFilter.INFORMATION.GENERIC +const informationUnrecognizedFilter: NotificationFilter = notificationFilter.INFORMATION.UNRECOGNIZED +const informationUnsupportedFilter: NotificationFilter = notificationFilter.INFORMATION.UNSUPPORTED +const informationHintFilter: NotificationFilter = notificationFilter.INFORMATION.HINT + +const warningAllString: string = notificationFilter.WARNING.ALL +const warningDeprecationString: string = notificationFilter.WARNING.DEPRECATION +const warningPerformanceString: string = notificationFilter.WARNING.PERFORMANCE +const warningGenericString: string = notificationFilter.WARNING.GENERIC +const warningUnrecognizedString: string = notificationFilter.WARNING.UNRECOGNIZED +const warningUnsupportedString: string = notificationFilter.WARNING.UNSUPPORTED +const warningHintString: string = notificationFilter.WARNING.HINT + +const warningAllFilter: NotificationFilter = notificationFilter.WARNING.ALL +const warningDeprecationFilter: NotificationFilter = notificationFilter.WARNING.DEPRECATION +const warningPerformanceFilter: NotificationFilter = notificationFilter.WARNING.PERFORMANCE +const warningGenericFilter: NotificationFilter = notificationFilter.WARNING.GENERIC +const warningUnrecognizedFilter: NotificationFilter = notificationFilter.WARNING.UNRECOGNIZED +const warningUnsupportedFilter: NotificationFilter = notificationFilter.WARNING.UNSUPPORTED +const warningHintFilter: NotificationFilter = notificationFilter.WARNING.HINT diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 16a570dfd..80631aa7a 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -64,7 +64,13 @@ import { BookmarkManager, bookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + notificationCategory, + notificationSeverityLevel, + NotificationCategory, + NotificationSeverityLevel, + notificationFilter, + NotificationFilter } from 'neo4j-driver-core' import { AuthToken, @@ -225,6 +231,9 @@ declare const forExport: { isLocalDateTime: typeof isLocalDateTime isDateTime: typeof isDateTime bookmarkManager: typeof bookmarkManager + notificationCategory: typeof notificationCategory + notificationSeverityLevel: typeof notificationSeverityLevel + notificationFilter: typeof notificationFilter } export { @@ -286,13 +295,19 @@ export { isDate, isLocalDateTime, isDateTime, - bookmarkManager + bookmarkManager, + notificationCategory, + notificationSeverityLevel, + notificationFilter } export type { BookmarkManager, BookmarkManagerConfig, - SessionConfig + SessionConfig, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter } export default forExport diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index e7c8c5a2a..c0d200fc7 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -1,6 +1,4 @@ - - const features = [ 'Feature:Auth:Custom', 'Feature:Auth:Kerberos', @@ -18,10 +16,13 @@ const features = [ 'Feature:Bolt:4.3', 'Feature:Bolt:4.4', 'Feature:Bolt:5.0', + 'Feature:Bolt:5.1', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver:GetServerInfo', + 'Feature:API:Driver:NotificationFilters', 'Feature:API:Driver.VerifyConnectivity', + 'Feature:API:Session:NotificationFilters', 'Optimization:EagerTransactionBegin', 'Optimization:ImplicitDefaultArguments', 'Optimization:MinimalBookmarksSet', diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 8a882dfa4..6fa3939a8 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -25,7 +25,16 @@ export { } from './request-handlers.js' export function NewSession (neo4j, context, data, wire) { - let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data + let { + driverId, + accessMode, + bookmarks, + database, + fetchSize, + impersonatedUser, + bookmarkManagerId, + notificationFilters + } = data switch (accessMode) { case 'r': accessMode = neo4j.session.READ @@ -52,7 +61,8 @@ export function NewSession (neo4j, context, data, wire) { database, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilters }) const id = context.addSession(session) wire.writeResponse(responses.Session({ id })) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 49b8fb0a2..d628e6017 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -83,6 +83,10 @@ export function NewDriver (neo4j, context, data, wire) { if ('maxTxRetryTimeMs' in data) { config.maxTransactionRetryTime = data.maxTxRetryTimeMs } + if ('notificationFilters' in data) { + config.notificationFilters = data.notificationFilters + } + let driver try { driver = neo4j.driver(uri, parsedAuthToken, config) @@ -106,7 +110,17 @@ export function DriverClose (_, context, data, wire) { } export function NewSession (neo4j, context, data, wire) { - let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data + let { + driverId, + accessMode, + bookmarks, + database, + fetchSize, + impersonatedUser, + bookmarkManagerId, + notificationFilters + } = data + switch (accessMode) { case 'r': accessMode = neo4j.session.READ @@ -133,7 +147,8 @@ export function NewSession (neo4j, context, data, wire) { database, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilters }) const id = context.addSession(session) wire.writeResponse(responses.Session({ id }))