diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-util.js b/packages/bolt-connection/src/bolt/bolt-protocol-util.js index 8a6ef2e4d..0073752ec 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,24 @@ function assertImpersonatedUserIsEmpty (impersonatedUser, onProtocolError = () = } } -export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty } +/** + * Asserts that the passed-in notificationFilter is empty + * @param {NotificationFilter} notificationFilter + * @param {function (err:Error)} onProtocolError Called when it does have notificationFilter user set + * @param {any} observer + */ +function assertNotificationFilterIsEmpty (notificationFilter, onProtocolError = () => {}, observer) { + if (notificationFilter !== undefined) { + const error = newError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to Neo4j 5.7.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${json.stringify(notificationFilter)}.` + ) + // 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, assertNotificationFilterIsEmpty } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 24d00c5fa..35c8b5dfa 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, + assertNotificationFilterIsEmpty } from './bolt-protocol-util' // eslint-disable-next-line no-unused-vars import { Chunker } from '../channel' @@ -169,16 +170,20 @@ export default class BoltProtocol { * @param {Object} param * @param {string} param.userAgent the user agent. * @param {Object} param.authToken the authentication token. + * @param {NotificationFilter} param.notificationFilter the notification filter. * @param {function(err: Error)} param.onError the callback to invoke on error. * @param {function()} param.onComplete the callback to invoke on completion. * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + this.write(RequestMessage.init(userAgent, authToken), observer, true) return observer @@ -254,6 +259,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 {NotificationFilter} param.notificationFilter the notification filter. * @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. @@ -266,6 +272,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -280,6 +287,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -362,6 +370,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 {NotificationFilter} param.notificationFilter the notification filter. * @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. @@ -381,6 +390,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeKeys, afterKeys, beforeError, @@ -410,6 +420,8 @@ export default class BoltProtocol { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write(RequestMessage.run(query, parameters), observer, false) this.write(RequestMessage.pullAll(), observer, flush) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v3.js index 523f6b229..783e35080 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, assertNotificationFilterIsEmpty } 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, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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, + notificationFilter, mode, beforeError, afterError, @@ -108,6 +112,8 @@ export default class BoltProtocol extends BoltProtocolV2 { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.begin({ bookmarks, txConfig, mode }), @@ -166,6 +172,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeKeys, afterKeys, @@ -194,6 +201,8 @@ export default class BoltProtocol extends BoltProtocolV2 { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.runWithMetadata(query, parameters, { diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js index 2dfc1f541..cb4438ec0 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, assertNotificationFilterIsEmpty } from './bolt-protocol-util' import { ResultStreamObserver, ProcedureRouteObserver @@ -56,6 +56,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeError, afterError, @@ -73,6 +74,8 @@ export default class BoltProtocol extends BoltProtocolV3 { // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.begin({ bookmarks, txConfig, database, mode }), @@ -91,6 +94,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeKeys, afterKeys, @@ -123,6 +127,8 @@ export default class BoltProtocol extends BoltProtocolV3 { // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) const flushRun = reactive this.write( diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js index 7c79f6e35..d507241d2 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js @@ -20,6 +20,7 @@ import BoltProtocolV4 from './bolt-protocol-v4x0' import RequestMessage from './request-message' import { LoginObserver } from './stream-observers' import { internal } from 'neo4j-driver-core' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util' import transformersFactories from './bolt-protocol-v4x1.transformer' import Transformer from './transformer' @@ -72,12 +73,15 @@ export default class BoltProtocol extends BoltProtocolV4 { return this._transformer } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 2efedb1af..291c348a8 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js @@ -19,6 +19,7 @@ import BoltProtocolV42 from './bolt-protocol-v4x2' import RequestMessage from './request-message' import { LoginObserver, RouteObserver } from './stream-observers' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util' import transformersFactories from './bolt-protocol-v4x3.transformer' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' @@ -80,14 +81,15 @@ export default class BoltProtocol extends BoltProtocolV42 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filter. + * @param {function(error)} args.onError On error callback + * @param {function(onComplte)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => { @@ -98,6 +100,9 @@ export default class BoltProtocol extends BoltProtocolV42 { } }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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..2e8e99c9d 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js @@ -21,6 +21,7 @@ import BoltProtocolV43 from './bolt-protocol-v4x3' import { internal } from 'neo4j-driver-core' import RequestMessage from './request-message' import { RouteObserver, ResultStreamObserver } from './stream-observers' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util' import transformersFactories from './bolt-protocol-v4x4.transformer' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' @@ -87,6 +88,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilter, beforeKeys, afterKeys, beforeError, @@ -116,6 +118,9 @@ export default class BoltProtocol extends BoltProtocolV43 { lowRecordWatermark }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + const flushRun = reactive this.write( RequestMessage.runWithMetadata(query, parameters, { @@ -142,6 +147,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -156,6 +162,9 @@ export default class BoltProtocol extends BoltProtocolV43 { }) observer.prepareToHandleSingleResponse() + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 d4834f4b7..85000baa5 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js @@ -18,6 +18,7 @@ */ import BoltProtocolV44 from './bolt-protocol-v4x4' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util' import transformersFactories from './bolt-protocol-v5x0.transformer' import Transformer from './transformer' import RequestMessage from './request-message' @@ -44,19 +45,23 @@ export default class BoltProtocol extends BoltProtocolV44 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filter. + * @param {function(error)} args.onError On error callback + * @param {function(onComplte)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 index 1a4861623..7fef848d4 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x1.js @@ -18,6 +18,7 @@ */ import BoltProtocolV5x0 from './bolt-protocol-v5x0' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util' import transformersFactories from './bolt-protocol-v5x1.transformer' import Transformer from './transformer' import RequestMessage from './request-message' @@ -48,14 +49,15 @@ export default class BoltProtocol extends BoltProtocolV5x0 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filters. + * @param {function(error)} args.onError On error callback + * @param {function(onComplete)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const state = {} const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), @@ -65,6 +67,9 @@ export default class BoltProtocol extends BoltProtocolV5x0 { } }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + this.write( RequestMessage.hello5x1(userAgent, this._serversideRouting), observer, diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.js new file mode 100644 index 000000000..4d49b5b42 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.js @@ -0,0 +1,172 @@ +/** + * 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 './bolt-protocol-v5x1' + +import transformersFactories from './bolt-protocol-v5x2.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_2, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x1 { + get version () { + return BOLT_PROTOCOL_V5_2 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + get supportsReAuth () { + return true + } + + /** + * Initialize a connection with the server + * + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filters. + * @param {function(error)} args.onError On error callback + * @param {function(onComplete)} args.onComplete On complete callback + * @returns {LoginObserver} The Login observer + */ + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { + const state = {} + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => { + state.metadata = metadata + return this._onLoginCompleted(metadata) + } + }) + + this.write( + RequestMessage.hello5x2(userAgent, notificationFilter, this._serversideRouting), + observer, + false + ) + + return this.logon({ + authToken, + onComplete: metadata => onComplete({ ...metadata, ...state.metadata }), + onError, + flush: true + }) + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilter, + 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, notificationFilter }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilter, + 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, + notificationFilter + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.transformer.js new file mode 100644 index 000000000..e3649215d --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x2.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 v5x1 from './bolt-protocol-v5x1.transformer' + +export default { + ...v5x1 +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 374266fda..21539cb54 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -28,6 +28,7 @@ import BoltProtocolV4x3 from './bolt-protocol-v4x3' import BoltProtocolV4x4 from './bolt-protocol-v4x4' import BoltProtocolV5x0 from './bolt-protocol-v5x0' import BoltProtocolV5x1 from './bolt-protocol-v5x1' +import BoltProtocolV5x2 from './bolt-protocol-v5x2' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -202,6 +203,16 @@ function createProtocol ( onProtocolError, serversideRouting ) + case 5.2: + return new BoltProtocolV5x2( + 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 09eb4beff..76067aba4 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, 1), version(5, 0)], + [version(5, 2), 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 446a7828d..b60486962 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -160,6 +160,37 @@ export default class RequestMessage { ) } + /** + * Create a new HELLO message. + * @param {string} userAgent the user agent. + * @param {NotificationFilter} notificationFilter the notification filter configured + * @param {Object} routing server side routing, set to routing context to turn on server side routing (> 4.1) + * @return {RequestMessage} new HELLO message. + */ + static hello5x2 (userAgent, notificationFilter = null, routing = null) { + const metadata = { user_agent: userAgent } + + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + } + + if (routing) { + metadata.routing = routing + } + + return new RequestMessage( + HELLO, + [metadata], + () => `HELLO ${json.stringify(metadata)}` + ) + } + /** * Create a new LOGON message. * @@ -194,10 +225,11 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {NotificationFilter} notificationFilter the notification filter * @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, notificationFilter } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) return new RequestMessage( BEGIN, [metadata], @@ -235,9 +267,9 @@ export default class RequestMessage { static runWithMetadata ( query, parameters, - { bookmarks, txConfig, database, mode, impersonatedUser } = {} + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {} ) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) return new RequestMessage( RUN, [query, parameters, metadata], @@ -348,9 +380,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 {notificationFilter} notificationFilter the notification filter * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -370,6 +403,15 @@ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) if (mode === ACCESS_MODE_READ) { metadata.mode = READ_MODE } + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + } return metadata } diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 390eedb7e..8b270519f 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -86,6 +86,7 @@ export function createChannelConnection ( config.disableLosslessIntegers, serversideRouting, chunker, + config.notificationFilter, createProtocol ) @@ -119,6 +120,7 @@ export default class ChannelConnection extends Connection { disableLosslessIntegers = false, serversideRouting = null, chunker, // to be removed, + notificationFilter, protocolSupplier ) { super(errorHandler) @@ -134,6 +136,7 @@ export default class ChannelConnection extends Connection { this._chunker = chunker this._log = createConnectionLogger(this, log) this._serversideRouting = serversideRouting + this._notificationFilter = notificationFilter // connection from the database, returned in response for HELLO message and might not be available this._dbConnectionId = null @@ -187,6 +190,7 @@ export default class ChannelConnection extends Connection { this._protocol.initialize({ userAgent, authToken, + notificationFilter: this._notificationFilter, onError: err => reject(err), onComplete: metadata => { if (metadata) { diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap new file mode 100644 index 000000000..75f545b99 --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x2.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV5x2 .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 BoltProtocolV5x2 .packable() should pack not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV5x2 .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 BoltProtocolV5x2 .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 BoltProtocolV5x2 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x2 .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/behaviour/index.js b/packages/bolt-connection/test/bolt/behaviour/index.js new file mode 100644 index 000000000..6149bfb61 --- /dev/null +++ b/packages/bolt-connection/test/bolt/behaviour/index.js @@ -0,0 +1,20 @@ +/** + * 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. + */ + +export * as notificationFilterBehaviour from './notification-filter' diff --git a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js new file mode 100644 index 000000000..22e9659d8 --- /dev/null +++ b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js @@ -0,0 +1,281 @@ +/** + * 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 { + internal, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel +} from 'neo4j-driver-core' +import RequestMessage from '../../../src/bolt/request-message' +import { LoginObserver } from '../../../src/bolt/stream-observers' + +import utils from '../../test-utils' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks } +} = internal + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldNotSupportNotificationFilterOnInitialize (createProtocol) { + describe('initialize', () => { + function verifyInitialize (notificationFilter) { + verifyNotificationFilterNotSupportedError( + createProtocol, + notificationFilter, + protocol => protocol.initialize({ notificationFilter })) + } + + it.each( + notificationFilterSetFixture() + )('should throw error when notificationsFilter=%o is set (%o)', (notificationFilter) => { + verifyInitialize({ + notificationFilter + }) + }) + }) +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldNotSupportNotificationFilterOnBeginTransaction (createProtocol) { + describe('beginTransaction', () => { + function verifyBeginTransaction (notificationFilter) { + verifyNotificationFilterNotSupportedError( + createProtocol, + notificationFilter, + protocol => protocol.beginTransaction({ notificationFilter })) + } + + it.each( + notificationFilterSetFixture() + )('should throw error when notificationsFilter=%o is set', (notificationFilter) => { + verifyBeginTransaction(notificationFilter) + }) + }) +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldNotSupportNotificationFilterOnRun (createProtocol) { + describe('beginTransaction', () => { + function verifyRun (notificationFilter) { + verifyNotificationFilterNotSupportedError( + createProtocol, + notificationFilter, + protocol => protocol.run('query', {}, { notificationFilter })) + } + + it.each( + notificationFilterSetFixture() + )('should throw error when notificationsFilter=%o is set', (notificationFilter) => { + verifyRun(notificationFilter) + }) + }) +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportNotificationFilterOnInitialize (createProtocol) { + it.each( + notificationFilterFixture() + )('should send notificationsFilter=%o on initialize', (notificationFilter) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = createProtocol(recorder) + utils.spyProtocolWrite(protocol) + const userAgent = 'js-driver-123' + const authToken = { type: 'none' } + + const observer = protocol.initialize({ userAgent, authToken, notificationFilter }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x2(userAgent, notificationFilter) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + verifyObserversAndFlushes(protocol, observer) + }) + + function verifyObserversAndFlushes (protocol, observer) { + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + } +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportNotificationFilterOnBeginTransaction (createProtocol) { + it.each( + notificationFilterFixture() + )('should send notificationsFilter=%o on begin a transaction', (notificationFilter) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = createProtocol(recorder) + utils.spyProtocolWrite(protocol) + + 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 observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + notificationFilter, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, notificationFilter }) + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportNotificationFilterOnRun (createProtocol) { + it.each( + notificationFilterFixture() + )('should send notificationsFilter=%o on run', (notificationFilter) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = createProtocol(recorder) + utils.spyProtocolWrite(protocol) + + 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 query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + notificationFilter + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + notificationFilter + }) + ) + + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) +} + +export function notificationFilterFixture () { + return [ + undefined, + null, + ...notificationFilterSetFixture() + ] +} + +/** + * + * @returns {Array} Return the list of notification features used in test + */ +function notificationFilterSetFixture () { + const minimumSeverityLevelSet = Object.values(notificationFilterMinimumSeverityLevel) + const disabledCategories = Object.values(notificationFilterDisabledCategory) + const disabledCategoriesSet = [...disabledCategories.keys()] + .map(length => disabledCategories.slice(0, length + 1)) + + /** Polyfill flatMap for Node10 tests */ + if (!minimumSeverityLevelSet.flatMap) { + minimumSeverityLevelSet.flatMap = function (callback, thisArg) { + return minimumSeverityLevelSet.concat.apply([], minimumSeverityLevelSet.map(callback, thisArg)) + } + } + + return [ + {}, + ...minimumSeverityLevelSet.map(minimumSeverityLevel => ({ minimumSeverityLevel })), + ...disabledCategoriesSet.map(disabledCategories => ({ disabledCategories })), + ...minimumSeverityLevelSet.flatMap( + minimumSeverityLevel => disabledCategories.map( + disabledCategories => ({ minimumSeverityLevel, disabledCategories }))) + ] +} + +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + * @param {string[]} notificationFilter The notification filters. + * @param {function(protocol: BoltProtocolV1)} fn + */ +function verifyNotificationFilterNotSupportedError (createProtocol, notificationFilter, fn) { + const recorder = new utils.MessageRecordingConnection() + const protocol = createProtocol(recorder) + + expect(() => fn(protocol)).toThrowError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to Neo4j 5.7.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${JSON.stringify(notificationFilter)}.` + ) +} 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 1ec6c840e..cae56c8d6 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -38,6 +38,7 @@ import utils from '../test-utils' import { LoginObserver } from '../../src/bolt/stream-observers' import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -635,4 +636,14 @@ describe('#unit BoltProtocolV1', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV1(recorder, null, false, undefined, undefined, () => {}) + } }) 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 70843b424..651fc4c95 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v2.test.js @@ -37,6 +37,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' describe('#unit BoltProtocolV2', () => { beforeEach(() => { @@ -501,4 +502,14 @@ describe('#unit BoltProtocolV2', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV2(recorder, null, false, undefined, undefined, () => {}) + } }) 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 40adfb621..8902ffe9e 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -43,6 +43,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' const { bookmarks: { Bookmarks }, @@ -706,6 +707,16 @@ describe('#unit BoltProtocolV3', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV3(recorder, null, false, undefined, undefined, () => {}) + } }) class SpiedBoltProtocolV3 extends BoltProtocolV3 { 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 b84e78fc8..e37f96c74 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js @@ -43,6 +43,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -598,6 +599,16 @@ describe('#unit BoltProtocolV4x0', () => { expect(protocol.supportsReAuth).toBe(false) }) }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV4x0(recorder, null, false, undefined, undefined, () => {}) + } }) class SpiedBoltProtocolV4x0 extends BoltProtocolV4x0 { 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 7b1d05e26..c921f6a17 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js @@ -37,6 +37,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' const { txConfig: { TxConfig }, @@ -498,4 +499,14 @@ describe('#unit BoltProtocolV4x1', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV4x1(recorder, null, false, undefined, undefined, () => {}) + } }) 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 9df65f4cd..6e16b1dee 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js @@ -37,6 +37,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' +import { notificationFilterBehaviour } from './behaviour' const { txConfig: { TxConfig }, @@ -497,4 +498,14 @@ describe('#unit BoltProtocolV4x2', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV4x2(recorder, null, false, undefined, undefined, () => {}) + } }) 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 9bcce10d4..698c8dd19 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -40,6 +40,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' import fc from 'fast-check' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -1130,4 +1131,14 @@ describe('#unit BoltProtocolV4x3', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV4x3(recorder, null, false, undefined, undefined, () => {}) + } }) 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 f541c869e..8c4ed5209 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js @@ -40,6 +40,7 @@ import { import { alloc } from '../../src/channel' import { structure } from '../../src/packstream' import fc from 'fast-check' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -1163,4 +1164,14 @@ describe('#unit BoltProtocolV4x4', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV4x4(recorder, null, false, undefined, undefined, () => {}) + } }) 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 72d603215..3fcdeb935 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -40,6 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -1062,4 +1063,14 @@ describe('#unit BoltProtocolV5x0', () => { expect(() => fn(protocol)).toThrowError(message) } }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV5x0(recorder, null, false, undefined, undefined, () => {}) + } }) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js index afce3f0a3..8c472ba42 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x1.test.js @@ -40,6 +40,7 @@ import { } from 'neo4j-driver-core' import { alloc } from '../../src/channel' +import { notificationFilterBehaviour } from './behaviour' const WRITE = 'WRITE' @@ -1089,4 +1090,14 @@ describe('#unit BoltProtocolV5x1', () => { expect(unpacked).toEqual(struct) }) }) + + describe('Bolt v5.2', () => { + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldNotSupportNotificationFilterOnRun(newProtocol) + }) + + function newProtocol (recorder) { + return new BoltProtocolV5x1(recorder, null, false, undefined, undefined, () => {}) + } }) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js new file mode 100644 index 000000000..d4a1e10da --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x2.test.js @@ -0,0 +1,1103 @@ +/** + * 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 BoltProtocolV5x2 from '../../src/bolt/bolt-protocol-v5x2' +import RequestMessage from '../../src/bolt/request-message' +import { v2, structure } from '../../src/packstream' +import utils from '../test-utils' +import { LoginObserver, 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' +import { notificationFilterBehaviour } from './behaviour' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV5x2', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x2(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 BoltProtocolV5x2(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 BoltProtocolV5x2(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 BoltProtocolV5x2(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('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 BoltProtocolV5x2(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 BoltProtocolV5x2(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('should return correct bolt version number', () => { + const protocol = new BoltProtocolV5x2(null, null, false) + + expect(protocol.version).toBe(5.2) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x2(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 BoltProtocolV5x2(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(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x1(clientName) + ) + expect(protocol.messages[1]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers.length).toBe(2) + + // hello observer + const helloObserver = protocol.observers[0] + expect(helloObserver).toBeInstanceOf(LoginObserver) + expect(helloObserver).not.toBe(observer) + + // login observer + const loginObserver = protocol.observers[1] + expect(loginObserver).toBeInstanceOf(LoginObserver) + expect(loginObserver).toBe(observer) + + expect(protocol.flushes).toEqual([false, true]) + }) + + it.each( + [true, false] + )('should logon to the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x2(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.logon({ authToken, flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logon(authToken) + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + it.each( + [true, false] + )('should logoff from the server [flush=%s]', (flush) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x2(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.logoff({ flush }) + + protocol.verifyMessageCount(1) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.logoff() + ) + + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([flush]) + }) + + 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 BoltProtocolV5x2(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 BoltProtocolV5x2(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 BoltProtocolV5x2(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]) + }) + + it('should support logoff', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x2(recorder, null, false) + + expect(protocol.supportsReAuth).toBe(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 BoltProtocolV5x2(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('notificationFilter', () => { + notificationFilterBehaviour.shouldSupportNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldSupportNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldSupportNotificationFilterOnRun(newProtocol) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV5x2(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 BoltProtocolV5x2(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV5x2(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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 BoltProtocolV5x2( + 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 newProtocol (recorder) { + return new BoltProtocolV5x2(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index 49273b1ca..1d550e7c8 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -31,6 +31,8 @@ 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' +import BoltProtocolV5x2 from '../../src/bolt/bolt-protocol-v5x2' const { logger: { Logger } @@ -44,17 +46,17 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x1to5x0 = '00 01 01 05' + const protocolVersion5x2to5x0 = '00 02 02 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion5x1to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x2to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) - it('should handle a successful handshake without reaining buffer', done => { + it('should handle a successful handshake without remaining buffer', done => { const { channel, handshakePromise } = subject() const expectedProtocolVersion = 4.3 @@ -360,7 +362,8 @@ describe('#unit Bolt', () => { v(4.3, BoltProtocolV4x3), v(4.4, BoltProtocolV4x4), v(5.0, BoltProtocolV5x0), - v(5.1, BoltProtocolV5x0) + v(5.1, BoltProtocolV5x1), + v(5.2, BoltProtocolV5x2) ] 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..7f86f9d09 100644 --- a/packages/bolt-connection/test/bolt/request-message.test.js +++ b/packages/bolt-connection/test/bolt/request-message.test.js @@ -19,6 +19,7 @@ import RequestMessage from '../../src/bolt/request-message' import { internal, int, json } from 'neo4j-driver-core' +import { notificationFilterBehaviour } from './behaviour' const { bookmarks: { Bookmarks }, @@ -429,4 +430,123 @@ describe('#unit RequestMessage', () => { }) }) }) + + describe('BoltV5.2', () => { + it.each( + notificationFilterFixtures() + )('should create HELLO message where notificationFilters=%o', (notificationFilter, expectedNotificationFilter) => { + const userAgent = 'my-driver/1.0.2' + const message = RequestMessage.hello5x2(userAgent, notificationFilter) + + const expectedFields = { user_agent: userAgent, ...expectedNotificationFilter } + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + expectedFields + ]) + expect(message.toString()).toEqual( + `HELLO ${json.stringify(expectedFields)}` + ) + }) + + it.each( + notificationFilterFixtures() + )('should create BEGIN message where notificationFilters=%o', (notificationFilter, expectedNotificationFilter) => { + ;[READ, WRITE].forEach(mode => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx10' + ]) + const impersonatedUser = 'the impostor' + const txConfig = new TxConfig({ timeout: 42, metadata: { key: 42 } }) + + const message = RequestMessage.begin({ bookmarks, txConfig, mode, impersonatedUser, notificationFilter }) + + const expectedMode = {} + if (mode === READ) { + expectedMode.mode = 'r' + } + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(42), + tx_metadata: { key: 42 }, + imp_user: impersonatedUser, + ...expectedMode, + ...expectedNotificationFilter + } + + expect(message.signature).toEqual(0x11) + expect(message.fields).toEqual([expectedMetadata]) + expect(message.toString()).toEqual( + `BEGIN ${json.stringify(expectedMetadata)}` + ) + }) + }) + + it.each( + notificationFilterFixtures() + )('should create RUN message where notificationFilters=%o', (notificationFilter, expectedNotificationFilter) => { + ;[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 impersonatedUser = 'the impostor' + + const message = RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + mode, + impersonatedUser, + notificationFilter + }) + + const expectedMode = {} + if (mode === READ) { + expectedMode.mode = 'r' + } + + const expectedMetadata = { + bookmarks: bookmarks.values(), + tx_timeout: int(999), + tx_metadata: { a: 'a', b: 'b' }, + imp_user: impersonatedUser, + ...expectedMode, + ...expectedNotificationFilter + } + + expect(message.signature).toEqual(0x10) + expect(message.fields).toEqual([query, parameters, expectedMetadata]) + expect(message.toString()).toEqual( + `RUN ${query} ${json.stringify(parameters)} ${json.stringify( + expectedMetadata + )}` + ) + }) + }) + + function notificationFilterFixtures () { + return notificationFilterBehaviour.notificationFilterFixture() + .map(notificationFilter => { + const expectedNotificationFilter = {} + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + expectedNotificationFilter.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + expectedNotificationFilter.notifications_disabled_categories = notificationFilter.disabledCategories + } + } + return [notificationFilter, expectedNotificationFilter] + }) + } + }) }) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 21696bd27..18200eea5 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -19,6 +19,7 @@ import ChannelConnection from '../../src/connection/connection-channel' import { int, internal, newError } from 'neo4j-driver-core' +import { notificationFilterBehaviour } from '../bolt/behaviour' const { serverAddress: { ServerAddress }, @@ -124,6 +125,30 @@ describe('ChannelConnection', () => { ) } ) + + it.each( + notificationFilterBehaviour.notificationFilterFixture() + )( + 'should send notificationFilter=%o to initialize ', + async (notificationFilter) => { + const channel = { + setupReceiveTimeout: jest.fn().mockName('setupReceiveTimeout') + } + const protocol = { + initialize: jest.fn(observer => + observer.onComplete({}) + ) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ channel, protocolSupplier, notificationFilter }) + + await connection.connect('userAgent', {}) + + const call = protocol.initialize.mock.calls[0][0] + + expect(call.notificationFilter).toBe(notificationFilter) + } + ) }) describe('._handleFatalError()', () => { @@ -522,6 +547,7 @@ describe('ChannelConnection', () => { disableLosslessIntegers, serversideRouting, chuncker, + notificationFilter, protocolSupplier }) { address = address || ServerAddress.fromUrl('bolt://localhost') @@ -534,6 +560,7 @@ describe('ChannelConnection', () => { disableLosslessIntegers, serversideRouting, chuncker, + notificationFilter, protocolSupplier ) } diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 11129d090..a3fcb2dc4 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -46,6 +46,7 @@ import EagerResult from './result-eager' import resultTransformers, { ResultTransformer } from './result-transformers' import QueryExecutor from './internal/query-executor' import { newError } from './error' +import NotificationFilter from './notification-filter' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -94,6 +95,7 @@ type CreateSession = (args: { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -103,6 +105,7 @@ interface DriverConfig { trust?: TrustStrategy fetchSize?: number logging?: LoggingConfig + notificationFilter?: NotificationFilter } /** @@ -117,6 +120,7 @@ class SessionConfig { impersonatedUser?: string fetchSize?: number bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter /** * @constructor @@ -235,6 +239,70 @@ class SessionConfig { * @since 5.0 */ this.bookmarkManager = undefined + + /** + * Configure filter for {@link Notification} objects returned in {@link ResultSummary#notifications}. + * + * This configuration enables filter notifications by: + * + * * the minimum severity level ({@link NotificationFilterMinimumSeverityLevel}) + * * disabling notification categories ({@link NotificationFilterDisabledCategory}) + * + * + * Disabling notifications can be done by defining the minimum severity level to 'OFF'. + * Default values can be use by omitting the configuration. + * + * @example + * // enabling warning notification, but disabling `HINT` and `DEPRECATION` notifications. + * const session = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.WARNING, // or 'WARNING + * disabledCategories: [ + * neo4j.notificationFilterDisabledCategory.HINT, // or 'HINT' + * neo4j.notificationFilterDisabledCategory.DEPRECATION // or 'DEPRECATION' + * ] + * } + * }) + * + * @example + * // disabling notifications for a session + * const session = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.OFF // or 'OFF' + * } + * }) + * + * @example + * // using default values configured in the driver + * const sessionWithDefaultValues = driver.session({ database: 'neo4j' }) + * // or driver.session({ database: 'neo4j', notificationFilter: undefined }) + * + * // using default minimum severity level, but disabling 'HINT' and 'UNRECOGNIZED' + * // notification categories + * const sessionWithDefaultSeverityLevel = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * disabledCategories: [ + * neo4j.notificationFilterDisabledCategory.HINT, // or 'HINT' + * neo4j.notificationFilterDisabledCategory.UNRECOGNIZED // or 'UNRECOGNIZED' + * ] + * } + * }) + * + * // using default disabled categories, but configuring minimum severity level to 'WARNING' + * const sessionWithDefaultSeverityLevel = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.WARNING // or 'WARNING' + * } + * }) + * + * @type {NotificationFilter|undefined} + * @since 5.7 + */ + this.notificationFilter = undefined } } @@ -627,7 +695,8 @@ class Driver { database = '', impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -637,7 +706,8 @@ class Driver { impersonatedUser, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), - bookmarkManager + bookmarkManager, + notificationFilter }) } @@ -675,7 +745,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -684,6 +755,7 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() @@ -700,7 +772,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }) } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ab6adccc..5532f83f0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,8 +64,18 @@ import ResultSummary, { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + NotificationSeverityLevel, + NotificationCategory, + notificationCategory, + notificationSeverityLevel } from './result-summary' +import NotificationFilter, { + notificationFilterDisabledCategory, + NotificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel, + NotificationFilterMinimumSeverityLevel +} from './notification-filter' import Result, { QueryResult, ResultObserver } from './result' import EagerResult from './result-eager' import ConnectionProvider from './connection-provider' @@ -154,7 +164,11 @@ const forExport = { auth, bookmarkManager, routing, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export { @@ -217,7 +231,11 @@ export { auth, bookmarkManager, routing, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export type { @@ -232,7 +250,12 @@ export type { SessionConfig, QueryConfig, RoutingControl, - ResultTransformer + ResultTransformer, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel } export default forExport diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index 1b9519387..d42d1da1c 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -35,6 +35,7 @@ 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 +const BOLT_PROTOCOL_V5_2: number = 5.2 export { FETCH_ALL, @@ -52,5 +53,6 @@ export { BOLT_PROTOCOL_V4_3, BOLT_PROTOCOL_V4_4, BOLT_PROTOCOL_V5_0, - BOLT_PROTOCOL_V5_1 + BOLT_PROTOCOL_V5_1, + BOLT_PROTOCOL_V5_2 } diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts new file mode 100644 index 000000000..cd5e21b9e --- /dev/null +++ b/packages/core/src/notification-filter.ts @@ -0,0 +1,104 @@ +/** + * 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 ExcludeUnknown = Exclude +type OFF = 'OFF' +type EnumRecord = { [key in T]: key } + +type NotificationFilterMinimumSeverityLevel = ExcludeUnknown | OFF +/** + * @typedef {'WARNING' | 'INFORMATION' | 'OFF'} NotificationFilterMinimumSeverityLevel + */ +/** + * Constants that represents the minimum Severity level in the {@link NotificationFilter} + */ +const notificationFilterMinimumSeverityLevel: EnumRecord = { + OFF: 'OFF', + WARNING: 'WARNING', + INFORMATION: 'INFORMATION' +} +Object.freeze(notificationFilterMinimumSeverityLevel) + +type NotificationFilterDisabledCategory = ExcludeUnknown +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'DEPRECATION' | 'GENERIC' } NotificationFilterDisabledCategory + */ +/** + * Constants that represents the disabled categories in the {@link NotificationFilter} + */ +const notificationFilterDisabledCategory: EnumRecord = { + HINT: 'HINT', + UNRECOGNIZED: 'UNRECOGNIZED', + UNSUPPORTED: 'UNSUPPORTED', + PERFORMANCE: 'PERFORMANCE', + DEPRECATION: 'DEPRECATION', + GENERIC: 'GENERIC' +} +Object.freeze(notificationFilterDisabledCategory) + +/** + * The notification filter object which can be configured in + * the session and driver creation. + * + * Values not defined are interpreted as default. + * + * @interface + */ +class NotificationFilter { + minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel + disabledCategories?: NotificationFilterDisabledCategory[] + + /** + * @constructor + * @private + */ + constructor () { + /** + * The minimum level of all notifications to receive. + * + * @public + * @type {?NotificationFilterMinimumSeverityLevel} + */ + this.minimumSeverityLevel = undefined + + /** + * Categories the user would like to opt-out of receiving. + * @type {?NotificationFilterDisabledCategory[]} + */ + this.disabledCategories = undefined + + throw new Error('Not implemented') + } +} + +export default NotificationFilter + +export { + notificationFilterMinimumSeverityLevel, + notificationFilterDisabledCategory +} + +export type { + NotificationFilterMinimumSeverityLevel, + NotificationFilterDisabledCategory +} diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 1431fefa2..674990ec3 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' | 'GENERIC' | '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 a78498e18..d29509bf9 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 _notificationFilter?: NotificationFilter /** * @constructor * @protected @@ -84,6 +86,7 @@ class Session { * @param {boolean} args.reactive - Whether this session should create reactive streams * @param {number} args.fetchSize - Defines how many records is pulled in each pulling batch * @param {string} args.impersonatedUser - The username which the user wants to impersonate for the duration of the session. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this session. */ constructor ({ mode, @@ -94,7 +97,8 @@ class Session { reactive, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilter }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -105,6 +109,7 @@ class Session { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }) { this._mode = mode this._database = database @@ -142,6 +147,7 @@ class Session { this._highRecordWatermark = calculatedWatermaks.high this._results = [] this._bookmarkManager = bookmarkManager + this._notificationFilter = notificationFilter } /** @@ -181,7 +187,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilter: this._notificationFilter }) }) this._results.push(result) @@ -298,7 +305,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilter: this._notificationFilter }) tx._begin(() => this._bookmarks(), txConfig) return tx diff --git a/packages/core/src/transaction-promise.ts b/packages/core/src/transaction-promise.ts index 04798195c..30bab3d65 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. @@ -47,14 +48,16 @@ class TransactionPromise extends Transaction implements Promise { /** * @constructor - * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. - * @param {function()} onClose - Function to be called when transaction is committed or rolled back. - * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. - * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the connection + * @param {object} args + * @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from. + * @param {function()} args.onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced. + * @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the connection * is not yet released. - * @param {boolean} reactive whether this transaction generates reactive streams - * @param {number} fetchSize - the record fetch size in each pulling batch. - * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {boolean} args.reactive whether this transaction generates reactive streams + * @param {number} args.fetchSize - the record fetch size in each pulling batch. + * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. */ constructor ({ connectionHolder, @@ -65,7 +68,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }: { connectionHolder: ConnectionHolder onClose: () => void @@ -76,6 +80,7 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilter?: NotificationFilter }) { super({ connectionHolder, @@ -86,7 +91,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }) } diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 3bee6d480..ca0c95884 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,19 +62,22 @@ class Transaction { private _bookmarks: Bookmarks private readonly _activePromise: Promise private _acceptActive: () => void + private readonly _notificationFilter?: NotificationFilter /** * @constructor - * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. - * @param {function()} onClose - Function to be called when transaction is committed or rolled back. - * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. - * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the conneciton + * @param {object} args + * @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from. + * @param {function()} args.onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced. + * @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the conneciton * is not yet released. - * @param {boolean} reactive whether this transaction generates reactive streams - * @param {number} fetchSize - the record fetch size in each pulling batch. - * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. - * @param {number} highRecordWatermark - The high watermark for the record buffer. - * @param {number} lowRecordWatermark - The low watermark for the record buffer. + * @param {boolean} args.reactive whether this transaction generates reactive streams + * @param {number} args.fetchSize - the record fetch size in each pulling batch. + * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {number} args.highRecordWatermark - The high watermark for the record buffer. + * @param {number} args.lowRecordWatermark - The low watermark for the record buffer. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. */ constructor ({ connectionHolder, @@ -84,7 +88,8 @@ class Transaction { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }: { connectionHolder: ConnectionHolder onClose: () => void @@ -95,6 +100,7 @@ class Transaction { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilter?: NotificationFilter }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -110,6 +116,7 @@ class Transaction { this._lowRecordWatermak = lowRecordWatermark this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() + this._notificationFilter = notificationFilter this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -138,6 +145,7 @@ class Transaction { mode: this._connectionHolder.mode(), database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, + notificationFilter: this._notificationFilter, 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 d78148a04..44de0c37c 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, EagerResult, newError, Result, ResultSummary, ServerInfo, Session } from '../src' +import { bookmarkManager, ConnectionProvider, EagerResult, newError, NotificationFilter, Result, ResultSummary, ServerInfo, Session } from '../src' import Driver, { QueryConfig, READ, routing } from '../src/driver' import { Bookmarks } from '../src/internal/bookmarks' import { Logger } from '../src/internal/logger' @@ -26,6 +26,7 @@ import { ConfiguredCustomResolver } from '../src/internal/resolver' import { LogLevel } from '../src/types' import resultTransformers from '../src/result-transformers' import Record, { Dict } from '../src/record' +import { validNotificationFilters } from './utils/notification-filters.fixtures' describe('Driver', () => { let driver: Driver | null @@ -165,6 +166,30 @@ describe('Driver', () => { } }) }) + + describe('when set config.notificationFilters', () => { + it.each( + validNotificationFilters() + )('should send valid "notificationFilters" to the session', async (notificationFilter?: NotificationFilter) => { + const driver = new Driver( + META_INFO, + { ...CONFIG }, + mockCreateConnectonProvider(connectionProvider), + createSession + ) + + const session = driver.session({ notificationFilter }) + + try { + expect(createSession).toBeCalledWith(expect.objectContaining({ + notificationFilter + })) + } finally { + await session.close() + await driver.close() + } + }) + }) }) it.each([ @@ -511,6 +536,33 @@ describe('Driver', () => { }) }) + describe('constructor', () => { + describe('when set config.notificationFilters', () => { + it.each( + validNotificationFilters() + )('should send valid "notificationFilters" to the connection provider', async (notificationFilter?: NotificationFilter) => { + const createConnectionProviderMock = jest.fn(mockCreateConnectonProvider(connectionProvider)) + const driver = new Driver( + META_INFO, + { notificationFilter }, + createConnectionProviderMock, + createSession + ) + + driver._getOrCreateConnectionProvider() + + expect(createConnectionProviderMock).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ notificationFilter }), + expect.any(Object), + expect.any(Object) + ) + + await driver.close() + }) + }) + }) + function mockCreateConnectonProvider (connectionProvider: ConnectionProvider) { return ( id: number, diff --git a/packages/core/test/notification-filter.test.ts b/packages/core/test/notification-filter.test.ts new file mode 100644 index 000000000..a904e1f2d --- /dev/null +++ b/packages/core/test/notification-filter.test.ts @@ -0,0 +1,83 @@ +/** + * 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 { + NotificationFilterDisabledCategory, + notificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel, + notificationFilterMinimumSeverityLevel +} from '../src/notification-filter' + +describe('notificationFilterMinimumSeverityLevel', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationFilterMinimumSeverityLevel)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationFilterMinimumSeverityLevel', () => { + for (const [, value] of Object.entries(notificationFilterMinimumSeverityLevel)) { + const assignableValue: NotificationFilterMinimumSeverityLevel = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidNotificationsSeverityLevels())('should have %s as key', (minimumSeverityLevel) => { + const keys = Object.keys(notificationFilterMinimumSeverityLevel) + expect(keys.includes(minimumSeverityLevel)).toBe(true) + }) +}) + +describe('notificationFilterDisabledCategory', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationFilterDisabledCategory)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationFilterDisabledCategory', () => { + for (const [, value] of Object.entries(notificationFilterDisabledCategory)) { + const assignableValue: NotificationFilterDisabledCategory = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidNotificationsCategories())('should have %s as key', (category) => { + const keys = Object.keys(notificationFilterDisabledCategory) + expect(keys.includes(category)).toBe(true) + }) +}) + +function getValidNotificationsSeverityLevels (): NotificationFilterMinimumSeverityLevel[] { + return [ + 'OFF', + 'INFORMATION', + 'WARNING' + ] +} + +function getValidNotificationsCategories (): NotificationFilterDisabledCategory[] { + return [ + 'HINT', + 'DEPRECATION', + 'GENERIC', + 'PERFORMANCE', + 'UNRECOGNIZED', + 'UNSUPPORTED' + ] +} 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 58795e4f4..d554468ec 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -16,11 +16,12 @@ * 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 } from '../src' import { bookmarks } from '../src/internal' import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants' import ManagedTransaction from '../src/transaction-managed' import FakeConnection from './utils/connection.fake' +import { validNotificationFilters } from './utils/notification-filters.fixtures' describe('session', () => { const systemBookmarks = ['sys:bm01', 'sys:bm02'] @@ -486,6 +487,27 @@ describe('session', () => { expect(updateBookmarksSpy).not.toBeCalled() }) + + it.each( + validNotificationFilters() + )('should call run query with notificationFilter', async (notificationFilter?: NotificationFilter) => { + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j', + notificationFilter + }) + + await session.beginTransaction() + + expect(connection.seenBeginTransaction[0][0]).toEqual( + expect.objectContaining({ + notificationFilter + }) + ) + }) }) describe.each([ @@ -792,6 +814,27 @@ describe('session', () => { expect(updateBookmarksSpy).not.toBeCalled() }) + + it.each( + validNotificationFilters() + )('should call run query with notificationFilter', async (notificationFilter?: NotificationFilter) => { + const connection = newFakeConnection() + + const { session } = setupSession({ + connection, + beginTx: false, + database: 'neo4j', + notificationFilter + }) + + await session.run('query') + + expect(connection.seenProtocolOptions[0]).toEqual( + expect.objectContaining({ + notificationFilter + }) + ) + }) }) }) @@ -843,7 +886,8 @@ function setupSession ({ fetchSize = 1000, database = '', lastBookmarks = bookmarks.Bookmarks.empty(), - bookmarkManager + bookmarkManager, + notificationFilter }: { connection: Connection beginTx?: boolean @@ -851,6 +895,7 @@ function setupSession ({ lastBookmarks?: bookmarks.Bookmarks database?: string bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }): { session: Session, connectionProvider: ConnectionProvider } { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = jest.fn(async () => await Promise.resolve(connection)) @@ -864,7 +909,8 @@ function setupSession ({ config: {}, reactive: false, bookmarks: lastBookmarks, - bookmarkManager + bookmarkManager, + notificationFilter }) if (beginTx) { diff --git a/packages/core/test/transaction.test.ts b/packages/core/test/transaction.test.ts index d61459ff3..0c8a23bda 100644 --- a/packages/core/test/transaction.test.ts +++ b/packages/core/test/transaction.test.ts @@ -17,11 +17,12 @@ * limitations under the License. */ -import { ConnectionProvider, newError, Transaction, TransactionPromise } from '../src' +import { ConnectionProvider, newError, 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' import FakeConnection from './utils/connection.fake' +import { validNotificationFilters } from './utils/notification-filters.fixtures' testTx('Transaction', newRegularTransaction) @@ -375,6 +376,25 @@ function testTx (transactionName: string, newTransaction: expect(connection.seenBeginTransaction.length).toEqual(1) expect(connection.seenQueries.length).toEqual(1) }) + + it.each( + validNotificationFilters() + )('should call not run query with notificationFilter', async (notificationFilter?: NotificationFilter) => { + const connection = newFakeConnection() + const tx = newTransaction({ + connection, + notificationFilter + }) + + tx._begin(async () => Bookmarks.empty(), TxConfig.empty()) + + await tx.run('RETURN 1') + expect(connection.seenProtocolOptions[0]).not.toEqual( + expect.objectContaining({ + notificationFilter + }) + ) + }) }) describe('.close()', () => { @@ -467,6 +487,7 @@ type TransactionFactory = (_: { fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number + notificationFilter?: NotificationFilter }) => T function newTransactionPromise ({ @@ -474,13 +495,15 @@ function newTransactionPromise ({ fetchSize = 1000, highRecordWatermark = 700, lowRecordWatermark = 300, - errorResolvingConnection = undefined + errorResolvingConnection = undefined, + notificationFilter }: { connection?: FakeConnection fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number errorResolvingConnection?: Error + notificationFilter?: NotificationFilter }): TransactionPromise { const connectionProvider = new ConnectionProvider() // @ts-expect-error @@ -504,7 +527,8 @@ function newTransactionPromise ({ fetchSize, impersonatedUser: '', highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }) return transaction @@ -514,12 +538,14 @@ function newRegularTransaction ({ connection, fetchSize = 1000, highRecordWatermark = 700, - lowRecordWatermark = 300 + lowRecordWatermark = 300, + notificationFilter }: { connection: FakeConnection fetchSize?: number highRecordWatermark?: number lowRecordWatermark?: number + notificationFilter?: NotificationFilter }): Transaction { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = async () => await Promise.resolve(connection) @@ -537,7 +563,8 @@ function newRegularTransaction ({ fetchSize, impersonatedUser: '', highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }) return transaction diff --git a/packages/core/test/utils/notification-filters.fixtures.ts b/packages/core/test/utils/notification-filters.fixtures.ts new file mode 100644 index 000000000..1c0f89cbc --- /dev/null +++ b/packages/core/test/utils/notification-filters.fixtures.ts @@ -0,0 +1,47 @@ +/** + * 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 { NotificationFilter, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel } from '../../src' + +export function validNotificationFilters (): Array { + return [ + undefined, + { + minimumSeverityLevel: 'OFF' + }, + { + minimumSeverityLevel: notificationFilterMinimumSeverityLevel.INFORMATION + }, + { + disabledCategories: [] + }, + { + disabledCategories: ['DEPRECATION'] + }, + { + disabledCategories: [notificationFilterDisabledCategory.GENERIC, notificationFilterDisabledCategory.PERFORMANCE] + }, + { + disabledCategories: [notificationFilterDisabledCategory.GENERIC, 'PERFORMANCE'] + }, + { + minimumSeverityLevel: notificationFilterMinimumSeverityLevel.INFORMATION, + disabledCategories: [notificationFilterDisabledCategory.GENERIC, notificationFilterDisabledCategory.PERFORMANCE] + } + ] +} 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..6fe79efd0 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,24 @@ function assertImpersonatedUserIsEmpty (impersonatedUser, onProtocolError = () = } } -export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty } +/** + * Asserts that the passed-in notificationFilter is empty + * @param {NotificationFilter} notificationFilter + * @param {function (err:Error)} onProtocolError Called when it does have notificationFilter user set + * @param {any} observer + */ +function assertNotificationFilterIsEmpty (notificationFilter, onProtocolError = () => {}, observer) { + if (notificationFilter !== undefined) { + const error = newError( + 'Driver is connected to a database that does not support user notification filters. ' + + 'Please upgrade to Neo4j 5.7.0 or later in order to use this functionality. ' + + `Trying to set notifications to ${json.stringify(notificationFilter)}.` + ) + // 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, assertNotificationFilterIsEmpty } 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 7874be285..bafc8d74e 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, + assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' // eslint-disable-next-line no-unused-vars import { Chunker } from '../channel/index.js' @@ -169,16 +170,20 @@ export default class BoltProtocol { * @param {Object} param * @param {string} param.userAgent the user agent. * @param {Object} param.authToken the authentication token. + * @param {NotificationFilter} param.notificationFilter the notification filter. * @param {function(err: Error)} param.onError the callback to invoke on error. * @param {function()} param.onComplete the callback to invoke on completion. * @returns {StreamObserver} the stream observer that monitors the corresponding server response. */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + this.write(RequestMessage.init(userAgent, authToken), observer, true) return observer @@ -254,6 +259,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 {NotificationFilter} param.notificationFilter the notification filter. * @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. @@ -266,6 +272,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -280,6 +287,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -362,6 +370,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 {NotificationFilter} param.notificationFilter the notification filter. * @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. @@ -381,6 +390,7 @@ export default class BoltProtocol { database, mode, impersonatedUser, + notificationFilter, beforeKeys, afterKeys, beforeError, @@ -410,6 +420,8 @@ export default class BoltProtocol { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write(RequestMessage.run(query, parameters), observer, false) this.write(RequestMessage.pullAll(), observer, flush) 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 f9826485c..42b9d6788 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, assertNotificationFilterIsEmpty } 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, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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, + notificationFilter, mode, beforeError, afterError, @@ -108,6 +112,8 @@ export default class BoltProtocol extends BoltProtocolV2 { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.begin({ bookmarks, txConfig, mode }), @@ -166,6 +172,7 @@ export default class BoltProtocol extends BoltProtocolV2 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeKeys, afterKeys, @@ -194,6 +201,8 @@ export default class BoltProtocol extends BoltProtocolV2 { assertDatabaseIsEmpty(database, this._onProtocolError, observer) // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.runWithMetadata(query, parameters, { 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..884c09615 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, assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import { ResultStreamObserver, ProcedureRouteObserver @@ -56,6 +56,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeError, afterError, @@ -73,6 +74,8 @@ export default class BoltProtocol extends BoltProtocolV3 { // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) this.write( RequestMessage.begin({ bookmarks, txConfig, database, mode }), @@ -91,6 +94,7 @@ export default class BoltProtocol extends BoltProtocolV3 { txConfig, database, impersonatedUser, + notificationFilter, mode, beforeKeys, afterKeys, @@ -123,6 +127,8 @@ export default class BoltProtocol extends BoltProtocolV3 { // passing impersonated user on this protocol version throws an error assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) const flushRun = reactive this.write( 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 d3e813602..8ab31730e 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 @@ -20,6 +20,7 @@ import BoltProtocolV4 from './bolt-protocol-v4x0.js' import RequestMessage from './request-message.js' import { LoginObserver } from './stream-observers.js' import { internal } from '../../core/index.ts' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import transformersFactories from './bolt-protocol-v4x1.transformer.js' import Transformer from './transformer.js' @@ -72,12 +73,15 @@ export default class BoltProtocol extends BoltProtocolV4 { return this._transformer } - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 68d50bad7..dcee97cb5 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 @@ -19,6 +19,7 @@ import BoltProtocolV42 from './bolt-protocol-v4x2.js' import RequestMessage from './request-message.js' import { LoginObserver, RouteObserver } from './stream-observers.js' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import transformersFactories from './bolt-protocol-v4x3.transformer.js' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' @@ -80,14 +81,15 @@ export default class BoltProtocol extends BoltProtocolV42 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filter. + * @param {function(error)} args.onError On error callback + * @param {function(onComplte)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => { @@ -98,6 +100,9 @@ export default class BoltProtocol extends BoltProtocolV42 { } }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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..774b21348 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 @@ -21,6 +21,7 @@ import BoltProtocolV43 from './bolt-protocol-v4x3.js' import { internal } from '../../core/index.ts' import RequestMessage from './request-message.js' import { RouteObserver, ResultStreamObserver } from './stream-observers.js' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import transformersFactories from './bolt-protocol-v4x4.transformer.js' import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' @@ -87,6 +88,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilter, beforeKeys, afterKeys, beforeError, @@ -116,6 +118,9 @@ export default class BoltProtocol extends BoltProtocolV43 { lowRecordWatermark }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + const flushRun = reactive this.write( RequestMessage.runWithMetadata(query, parameters, { @@ -142,6 +147,7 @@ export default class BoltProtocol extends BoltProtocolV43 { database, mode, impersonatedUser, + notificationFilter, beforeError, afterError, beforeComplete, @@ -156,6 +162,9 @@ export default class BoltProtocol extends BoltProtocolV43 { }) observer.prepareToHandleSingleResponse() + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 d29f73044..cf4330563 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 @@ -18,6 +18,7 @@ */ import BoltProtocolV44 from './bolt-protocol-v4x4.js' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import transformersFactories from './bolt-protocol-v5x0.transformer.js' import Transformer from './transformer.js' import RequestMessage from './request-message.js' @@ -44,19 +45,23 @@ export default class BoltProtocol extends BoltProtocolV44 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filter. + * @param {function(error)} args.onError On error callback + * @param {function(onComplte)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), onCompleted: metadata => this._onLoginCompleted(metadata, authToken, onComplete) }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, 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 index 4dda70917..4c6dba634 100644 --- 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 @@ -18,6 +18,7 @@ */ import BoltProtocolV5x0 from './bolt-protocol-v5x0.js' +import { assertNotificationFilterIsEmpty } from './bolt-protocol-util.js' import transformersFactories from './bolt-protocol-v5x1.transformer.js' import Transformer from './transformer.js' import RequestMessage from './request-message.js' @@ -48,14 +49,15 @@ export default class BoltProtocol extends BoltProtocolV5x0 { /** * 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(onComplte)} param0.onComplete On complete callback + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filters. + * @param {function(error)} args.onError On error callback + * @param {function(onComplete)} args.onComplete On complete callback * @returns {LoginObserver} The Login observer */ - initialize ({ userAgent, authToken, onError, onComplete } = {}) { + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { const state = {} const observer = new LoginObserver({ onError: error => this._onLoginError(error, onError), @@ -65,6 +67,9 @@ export default class BoltProtocol extends BoltProtocolV5x0 { } }) + // passing notification filter on this protocol version throws an error + assertNotificationFilterIsEmpty(notificationFilter, this._onProtocolError, observer) + this.write( RequestMessage.hello5x1(userAgent, this._serversideRouting), observer, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.js new file mode 100644 index 000000000..9a77a3a10 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.js @@ -0,0 +1,172 @@ +/** + * 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 './bolt-protocol-v5x1.js' + +import transformersFactories from './bolt-protocol-v5x2.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_2, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x1 { + get version () { + return BOLT_PROTOCOL_V5_2 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + get supportsReAuth () { + return true + } + + /** + * Initialize a connection with the server + * + * @param {Object} args The params + * @param {string} args.userAgent The user agent + * @param {any} args.authToken The auth token + * @param {NotificationFilter} args.notificationFilter The notification filters. + * @param {function(error)} args.onError On error callback + * @param {function(onComplete)} args.onComplete On complete callback + * @returns {LoginObserver} The Login observer + */ + initialize ({ userAgent, authToken, notificationFilter, onError, onComplete } = {}) { + const state = {} + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => { + state.metadata = metadata + return this._onLoginCompleted(metadata) + } + }) + + this.write( + RequestMessage.hello5x2(userAgent, notificationFilter, this._serversideRouting), + observer, + false + ) + + return this.logon({ + authToken, + onComplete: metadata => onComplete({ ...metadata, ...state.metadata }), + onError, + flush: true + }) + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilter, + 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, notificationFilter }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + notificationFilter, + 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, + notificationFilter + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.transformer.js new file mode 100644 index 000000000..9cbb0c2e2 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x2.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 v5x1 from './bolt-protocol-v5x1.transformer.js' + +export default { + ...v5x1 +} 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 45c33eef2..95cbbbb8a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -28,6 +28,7 @@ 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' +import BoltProtocolV5x2 from './bolt-protocol-v5x2.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -202,6 +203,16 @@ function createProtocol ( onProtocolError, serversideRouting ) + case 5.2: + return new BoltProtocolV5x2( + 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 f8c0de714..ef7ff76f1 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, 1), version(5, 0)], + [version(5, 2), 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 fc9d6a07d..574cdee80 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 @@ -160,6 +160,37 @@ export default class RequestMessage { ) } + /** + * Create a new HELLO message. + * @param {string} userAgent the user agent. + * @param {NotificationFilter} notificationFilter the notification filter configured + * @param {Object} routing server side routing, set to routing context to turn on server side routing (> 4.1) + * @return {RequestMessage} new HELLO message. + */ + static hello5x2 (userAgent, notificationFilter = null, routing = null) { + const metadata = { user_agent: userAgent } + + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + } + + if (routing) { + metadata.routing = routing + } + + return new RequestMessage( + HELLO, + [metadata], + () => `HELLO ${json.stringify(metadata)}` + ) + } + /** * Create a new LOGON message. * @@ -194,10 +225,11 @@ export default class RequestMessage { * @param {string} database the database name. * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user. + * @param {NotificationFilter} notificationFilter the notification filter * @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, notificationFilter } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) return new RequestMessage( BEGIN, [metadata], @@ -235,9 +267,9 @@ export default class RequestMessage { static runWithMetadata ( query, parameters, - { bookmarks, txConfig, database, mode, impersonatedUser } = {} + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {} ) { - const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) return new RequestMessage( RUN, [query, parameters, metadata], @@ -348,9 +380,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 {notificationFilter} notificationFilter the notification filter * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -370,6 +403,15 @@ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) if (mode === ACCESS_MODE_READ) { metadata.mode = READ_MODE } + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + } return metadata } 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 b676085a0..79dd0e4b9 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 @@ -86,6 +86,7 @@ export function createChannelConnection ( config.disableLosslessIntegers, serversideRouting, chunker, + config.notificationFilter, createProtocol ) @@ -119,6 +120,7 @@ export default class ChannelConnection extends Connection { disableLosslessIntegers = false, serversideRouting = null, chunker, // to be removed, + notificationFilter, protocolSupplier ) { super(errorHandler) @@ -134,6 +136,7 @@ export default class ChannelConnection extends Connection { this._chunker = chunker this._log = createConnectionLogger(this, log) this._serversideRouting = serversideRouting + this._notificationFilter = notificationFilter // connection from the database, returned in response for HELLO message and might not be available this._dbConnectionId = null @@ -187,6 +190,7 @@ export default class ChannelConnection extends Connection { this._protocol.initialize({ userAgent, authToken, + notificationFilter: this._notificationFilter, onError: err => reject(err), onComplete: metadata => { if (metadata) { diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index d79341a71..aa61ee18e 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -46,6 +46,7 @@ import EagerResult from './result-eager.ts' import resultTransformers, { ResultTransformer } from './result-transformers.ts' import QueryExecutor from './internal/query-executor.ts' import { newError } from './error.ts' +import NotificationFilter from './notification-filter.ts' const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour @@ -94,6 +95,7 @@ type CreateSession = (args: { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -103,6 +105,7 @@ interface DriverConfig { trust?: TrustStrategy fetchSize?: number logging?: LoggingConfig + notificationFilter?: NotificationFilter } /** @@ -117,6 +120,7 @@ class SessionConfig { impersonatedUser?: string fetchSize?: number bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter /** * @constructor @@ -235,6 +239,70 @@ class SessionConfig { * @since 5.0 */ this.bookmarkManager = undefined + + /** + * Configure filter for {@link Notification} objects returned in {@link ResultSummary#notifications}. + * + * This configuration enables filter notifications by: + * + * * the minimum severity level ({@link NotificationFilterMinimumSeverityLevel}) + * * disabling notification categories ({@link NotificationFilterDisabledCategory}) + * + * + * Disabling notifications can be done by defining the minimum severity level to 'OFF'. + * Default values can be use by omitting the configuration. + * + * @example + * // enabling warning notification, but disabling `HINT` and `DEPRECATION` notifications. + * const session = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.WARNING, // or 'WARNING + * disabledCategories: [ + * neo4j.notificationFilterDisabledCategory.HINT, // or 'HINT' + * neo4j.notificationFilterDisabledCategory.DEPRECATION // or 'DEPRECATION' + * ] + * } + * }) + * + * @example + * // disabling notifications for a session + * const session = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.OFF // or 'OFF' + * } + * }) + * + * @example + * // using default values configured in the driver + * const sessionWithDefaultValues = driver.session({ database: 'neo4j' }) + * // or driver.session({ database: 'neo4j', notificationFilter: undefined }) + * + * // using default minimum severity level, but disabling 'HINT' and 'UNRECOGNIZED' + * // notification categories + * const sessionWithDefaultSeverityLevel = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * disabledCategories: [ + * neo4j.notificationFilterDisabledCategory.HINT, // or 'HINT' + * neo4j.notificationFilterDisabledCategory.UNRECOGNIZED // or 'UNRECOGNIZED' + * ] + * } + * }) + * + * // using default disabled categories, but configuring minimum severity level to 'WARNING' + * const sessionWithDefaultSeverityLevel = driver.session({ + * database: 'neo4j', + * notificationFilter: { + * minimumSeverityLevel: neo4j.notificationFilterMinimumSeverityLevel.WARNING // or 'WARNING' + * } + * }) + * + * @type {NotificationFilter|undefined} + * @since 5.7 + */ + this.notificationFilter = undefined } } @@ -627,7 +695,8 @@ class Driver { database = '', impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -637,7 +706,8 @@ class Driver { impersonatedUser, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), - bookmarkManager + bookmarkManager, + notificationFilter }) } @@ -675,7 +745,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -684,6 +755,7 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() @@ -700,7 +772,8 @@ class Driver { reactive, impersonatedUser, fetchSize, - bookmarkManager + bookmarkManager, + notificationFilter }) } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 0e15fcdc7..12352d6fd 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -64,8 +64,18 @@ import ResultSummary, { Plan, ProfiledPlan, QueryStatistics, - Stats + Stats, + NotificationSeverityLevel, + NotificationCategory, + notificationCategory, + notificationSeverityLevel } from './result-summary.ts' +import NotificationFilter, { + notificationFilterDisabledCategory, + NotificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel, + NotificationFilterMinimumSeverityLevel +} from './notification-filter.ts' import Result, { QueryResult, ResultObserver } from './result.ts' import EagerResult from './result-eager.ts' import ConnectionProvider from './connection-provider.ts' @@ -154,7 +164,11 @@ const forExport = { auth, bookmarkManager, routing, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export { @@ -217,7 +231,11 @@ export { auth, bookmarkManager, routing, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export type { @@ -232,7 +250,12 @@ export type { SessionConfig, QueryConfig, RoutingControl, - ResultTransformer + ResultTransformer, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel } 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 1b9519387..d42d1da1c 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -35,6 +35,7 @@ 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 +const BOLT_PROTOCOL_V5_2: number = 5.2 export { FETCH_ALL, @@ -52,5 +53,6 @@ export { BOLT_PROTOCOL_V4_3, BOLT_PROTOCOL_V4_4, BOLT_PROTOCOL_V5_0, - BOLT_PROTOCOL_V5_1 + BOLT_PROTOCOL_V5_1, + BOLT_PROTOCOL_V5_2 } 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..6f02d926c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -0,0 +1,104 @@ +/** + * 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 ExcludeUnknown = Exclude +type OFF = 'OFF' +type EnumRecord = { [key in T]: key } + +type NotificationFilterMinimumSeverityLevel = ExcludeUnknown | OFF +/** + * @typedef {'WARNING' | 'INFORMATION' | 'OFF'} NotificationFilterMinimumSeverityLevel + */ +/** + * Constants that represents the minimum Severity level in the {@link NotificationFilter} + */ +const notificationFilterMinimumSeverityLevel: EnumRecord = { + OFF: 'OFF', + WARNING: 'WARNING', + INFORMATION: 'INFORMATION' +} +Object.freeze(notificationFilterMinimumSeverityLevel) + +type NotificationFilterDisabledCategory = ExcludeUnknown +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'DEPRECATION' | 'GENERIC' } NotificationFilterDisabledCategory + */ +/** + * Constants that represents the disabled categories in the {@link NotificationFilter} + */ +const notificationFilterDisabledCategory: EnumRecord = { + HINT: 'HINT', + UNRECOGNIZED: 'UNRECOGNIZED', + UNSUPPORTED: 'UNSUPPORTED', + PERFORMANCE: 'PERFORMANCE', + DEPRECATION: 'DEPRECATION', + GENERIC: 'GENERIC' +} +Object.freeze(notificationFilterDisabledCategory) + +/** + * The notification filter object which can be configured in + * the session and driver creation. + * + * Values not defined are interpreted as default. + * + * @interface + */ +class NotificationFilter { + minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel + disabledCategories?: NotificationFilterDisabledCategory[] + + /** + * @constructor + * @private + */ + constructor () { + /** + * The minimum level of all notifications to receive. + * + * @public + * @type {?NotificationFilterMinimumSeverityLevel} + */ + this.minimumSeverityLevel = undefined + + /** + * Categories the user would like to opt-out of receiving. + * @type {?NotificationFilterDisabledCategory[]} + */ + this.disabledCategories = undefined + + throw new Error('Not implemented') + } +} + +export default NotificationFilter + +export { + notificationFilterMinimumSeverityLevel, + notificationFilterDisabledCategory +} + +export type { + NotificationFilterMinimumSeverityLevel, + NotificationFilterDisabledCategory +} diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index 771075922..c16803598 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' | 'GENERIC' | '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 ddc49cb98..26fea9162 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 _notificationFilter?: NotificationFilter /** * @constructor * @protected @@ -84,6 +86,7 @@ class Session { * @param {boolean} args.reactive - Whether this session should create reactive streams * @param {number} args.fetchSize - Defines how many records is pulled in each pulling batch * @param {string} args.impersonatedUser - The username which the user wants to impersonate for the duration of the session. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this session. */ constructor ({ mode, @@ -94,7 +97,8 @@ class Session { reactive, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilter }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -105,6 +109,7 @@ class Session { fetchSize: number impersonatedUser?: string bookmarkManager?: BookmarkManager + notificationFilter?: NotificationFilter }) { this._mode = mode this._database = database @@ -142,6 +147,7 @@ class Session { this._highRecordWatermark = calculatedWatermaks.high this._results = [] this._bookmarkManager = bookmarkManager + this._notificationFilter = notificationFilter } /** @@ -181,7 +187,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilter: this._notificationFilter }) }) this._results.push(result) @@ -298,7 +305,8 @@ class Session { reactive: this._reactive, fetchSize: this._fetchSize, lowRecordWatermark: this._lowRecordWatermark, - highRecordWatermark: this._highRecordWatermark + highRecordWatermark: this._highRecordWatermark, + notificationFilter: this._notificationFilter }) tx._begin(() => this._bookmarks(), txConfig) return tx diff --git a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts index 157588735..b5a72a403 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. @@ -47,14 +48,16 @@ class TransactionPromise extends Transaction implements Promise { /** * @constructor - * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. - * @param {function()} onClose - Function to be called when transaction is committed or rolled back. - * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. - * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the connection + * @param {object} args + * @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from. + * @param {function()} args.onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced. + * @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the connection * is not yet released. - * @param {boolean} reactive whether this transaction generates reactive streams - * @param {number} fetchSize - the record fetch size in each pulling batch. - * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {boolean} args.reactive whether this transaction generates reactive streams + * @param {number} args.fetchSize - the record fetch size in each pulling batch. + * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. */ constructor ({ connectionHolder, @@ -65,7 +68,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }: { connectionHolder: ConnectionHolder onClose: () => void @@ -76,6 +80,7 @@ class TransactionPromise extends Transaction implements Promise { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilter?: NotificationFilter }) { super({ connectionHolder, @@ -86,7 +91,8 @@ class TransactionPromise extends Transaction implements Promise { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }) } diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts index 5e4b98b96..91c786de2 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,19 +62,22 @@ class Transaction { private _bookmarks: Bookmarks private readonly _activePromise: Promise private _acceptActive: () => void + private readonly _notificationFilter?: NotificationFilter /** * @constructor - * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. - * @param {function()} onClose - Function to be called when transaction is committed or rolled back. - * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. - * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the conneciton + * @param {object} args + * @param {ConnectionHolder} args.connectionHolder - the connection holder to get connection from. + * @param {function()} args.onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} args.onBookmarks callback invoked when new bookmark is produced. + * @param {function()} args.onConnection - Function to be called when a connection is obtained to ensure the conneciton * is not yet released. - * @param {boolean} reactive whether this transaction generates reactive streams - * @param {number} fetchSize - the record fetch size in each pulling batch. - * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. - * @param {number} highRecordWatermark - The high watermark for the record buffer. - * @param {number} lowRecordWatermark - The low watermark for the record buffer. + * @param {boolean} args.reactive whether this transaction generates reactive streams + * @param {number} args.fetchSize - the record fetch size in each pulling batch. + * @param {string} args.impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {number} args.highRecordWatermark - The high watermark for the record buffer. + * @param {number} args.lowRecordWatermark - The low watermark for the record buffer. + * @param {NotificationFilter} args.notificationFilter - The notification filter used for this transaction. */ constructor ({ connectionHolder, @@ -84,7 +88,8 @@ class Transaction { fetchSize, impersonatedUser, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + notificationFilter }: { connectionHolder: ConnectionHolder onClose: () => void @@ -95,6 +100,7 @@ class Transaction { impersonatedUser?: string highRecordWatermark: number lowRecordWatermark: number + notificationFilter?: NotificationFilter }) { this._connectionHolder = connectionHolder this._reactive = reactive @@ -110,6 +116,7 @@ class Transaction { this._lowRecordWatermak = lowRecordWatermark this._highRecordWatermark = highRecordWatermark this._bookmarks = Bookmarks.empty() + this._notificationFilter = notificationFilter this._acceptActive = () => { } // satisfy DenoJS this._activePromise = new Promise((resolve, reject) => { this._acceptActive = resolve @@ -138,6 +145,7 @@ class Transaction { mode: this._connectionHolder.mode(), database: this._connectionHolder.database(), impersonatedUser: this._impersonatedUser, + notificationFilter: this._notificationFilter, 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 9065e7f72..633060a56 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -84,7 +84,16 @@ import { RoutingControl, routing, resultTransformers, - ResultTransformer + ResultTransformer, + notificationCategory, + notificationSeverityLevel, + NotificationSeverityLevel, + NotificationCategory, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -229,7 +238,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#notificationFilter for usage instructions. + * notificationFilter: undefined, + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -498,7 +511,11 @@ const forExport = { ConnectionProvider, Connection, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export { @@ -560,7 +577,11 @@ export { ConnectionProvider, Connection, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export type { QueryResult, @@ -576,6 +597,11 @@ export type { SessionConfig, QueryConfig, RoutingControl, - ResultTransformer + ResultTransformer, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel } export default forExport diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 56442d200..e64639855 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -84,7 +84,16 @@ import { RoutingControl, routing, resultTransformers, - ResultTransformer + ResultTransformer, + notificationCategory, + notificationSeverityLevel, + NotificationSeverityLevel, + NotificationCategory, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -228,7 +237,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#notificationFilter for usage instructions. + * notificationFilter: undefined, + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -497,7 +510,11 @@ const forExport = { ConnectionProvider, Connection, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export { @@ -559,7 +576,11 @@ export { ConnectionProvider, Connection, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export type { QueryResult, @@ -575,6 +596,11 @@ export type { SessionConfig, QueryConfig, RoutingControl, - ResultTransformer + ResultTransformer, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel } 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 ebb9bffd3..a5ebbc4aa 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -409,4 +409,39 @@ describe('index', () => { expect(neo4j.routing.WRITERS).toBeDefined() expect(neo4j.routing.READERS).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 notificationFilterMinimumSeverityLevel', () => { + expect(neo4j.notificationFilterMinimumSeverityLevel).toBeDefined() + expect(neo4j.notificationFilterMinimumSeverityLevel.WARNING).toBeDefined() + expect(neo4j.notificationFilterMinimumSeverityLevel.INFORMATION).toBeDefined() + expect(neo4j.notificationFilterMinimumSeverityLevel.OFF).toBeDefined() + }) + + it('should export notificationFilterDisabledCategory', () => { + expect(neo4j.notificationFilterDisabledCategory).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.HINT).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.UNRECOGNIZED).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.UNSUPPORTED).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.PERFORMANCE).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.DEPRECATION).toBeDefined() + expect(neo4j.notificationFilterDisabledCategory.GENERIC).toBeDefined() + }) }) diff --git a/packages/neo4j-driver/src/driver.js b/packages/neo4j-driver/src/driver.js index 491c43f02..f91638af4 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, + notificationFilter } = {}) { 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, + notificationFilter }), config: this._config }) diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 3ccd05733..954bff531 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -69,7 +69,11 @@ import { ManagedTransaction, bookmarkManager, routing, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -208,7 +212,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#notificationFilter for usage instructions. + * notificationFilter: undefined, + * + * // Optionally override the default user agent name. * userAgent: USER_AGENT * } * @@ -484,7 +492,11 @@ const forExport = { LocalDateTime, DateTime, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export { @@ -547,6 +559,10 @@ export { LocalDateTime, DateTime, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export default forExport diff --git a/packages/neo4j-driver/test/types/index.test.ts b/packages/neo4j-driver/test/types/index.test.ts index 2fd1f50a9..b7b611ce4 100644 --- a/packages/neo4j-driver/test/types/index.test.ts +++ b/packages/neo4j-driver/test/types/index.test.ts @@ -36,7 +36,15 @@ import { isPathSegment, isRelationship, isUnboundRelationship, - RoutingControl + RoutingControl, + notificationSeverityLevel, + NotificationSeverityLevel, + notificationCategory, + NotificationCategory, + notificationFilterMinimumSeverityLevel, + NotificationFilterMinimumSeverityLevel, + NotificationFilterDisabledCategory, + notificationFilterDisabledCategory } from '../../types/index' import Driver from '../../types/driver' @@ -111,3 +119,45 @@ const neo4jIsPath: boolean = isPath({}) const neo4jIsPathSegment: boolean = isPathSegment({}) const neo4jIsRelationship: boolean = isRelationship({}) const neo4jIsUnboundRelationship: boolean = isUnboundRelationship({}) + +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 offNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.OFF +const warningNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.WARNING +const infoNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.INFORMATION +const offNotificationFilterMinimumSeverityLevel: NotificationFilterMinimumSeverityLevel = notificationFilterMinimumSeverityLevel.OFF +const warningNotificationFilterMinimumSeverityLevel: NotificationFilterMinimumSeverityLevel = notificationFilterMinimumSeverityLevel.WARNING +const infoNotificationFilterMinimumSeverityLevel: NotificationFilterMinimumSeverityLevel = notificationFilterMinimumSeverityLevel.INFORMATION + +const hintDisabledCategoryString: string = notificationFilterDisabledCategory.HINT +const deprecationDisabledCategoryString: string = notificationFilterDisabledCategory.DEPRECATION +const performanceDisabledCategoryString: string = notificationFilterDisabledCategory.PERFORMANCE +const genericDisabledCategoryString: string = notificationFilterDisabledCategory.GENERIC +const unrecognizedDisabledCategoryString: string = notificationFilterDisabledCategory.UNRECOGNIZED +const unsupportedDisabledCategoryString: string = notificationFilterDisabledCategory.UNSUPPORTED +const hintDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.HINT +const deprecationDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.DEPRECATION +const performanceDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.PERFORMANCE +const genericDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.GENERIC +const unrecognizedDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.UNRECOGNIZED +const unsupportedDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.UNSUPPORTED diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index badb51bcd..e0dfa1865 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -74,7 +74,16 @@ import { RoutingControl, routing, resultTransformers, - ResultTransformer + ResultTransformer, + notificationCategory, + notificationSeverityLevel, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } from 'neo4j-driver-core' import { AuthToken, @@ -252,6 +261,10 @@ declare const forExport: { isUnboundRelationship: typeof isUnboundRelationship bookmarkManager: typeof bookmarkManager resultTransformers: typeof resultTransformers + notificationCategory: typeof notificationCategory + notificationSeverityLevel: typeof notificationSeverityLevel + notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory + notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel } export { @@ -321,7 +334,11 @@ export { isRelationship, isUnboundRelationship, bookmarkManager, - resultTransformers + resultTransformers, + notificationCategory, + notificationSeverityLevel, + notificationFilterDisabledCategory, + notificationFilterMinimumSeverityLevel } export type { @@ -330,7 +347,12 @@ export type { SessionConfig, QueryConfig, RoutingControl, - ResultTransformer + ResultTransformer, + NotificationCategory, + NotificationSeverityLevel, + NotificationFilter, + NotificationFilterDisabledCategory, + NotificationFilterMinimumSeverityLevel } export default forExport diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index bd270cf3e..99181a219 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -17,11 +17,14 @@ const features = [ 'Feature:Bolt:4.4', 'Feature:Bolt:5.0', 'Feature:Bolt:5.1', + 'Feature:Bolt:5.2', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', + 'Feature:API:Driver:NotificationsConfig', 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', + 'Feature:API:Session:NotificationsConfig', '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 b48ff1e28..1d0762856 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -46,6 +46,13 @@ export function NewSession (neo4j, context, data, wire) { return } } + let notificationFilter + if ('notificationsMinSeverity' in data || 'notificationsDisabledCategories' in data) { + notificationFilter = { + minimumSeverityLevel: data.notificationsMinSeverity, + disabledCategories: data.notificationsDisabledCategories + } + } const driver = context.getDriver(driverId) const session = driver.rxSession({ defaultAccessMode: accessMode, @@ -53,7 +60,8 @@ export function NewSession (neo4j, context, data, wire) { database, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilter }) 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 82d6f2e30..295033053 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -39,6 +39,7 @@ export function NewDriver (neo4j, context, data, wire) { authToken.parameters ) } + const resolver = resolverRegistered ? address => new Promise((resolve, reject) => { @@ -83,6 +84,12 @@ export function NewDriver (neo4j, context, data, wire) { if ('maxTxRetryTimeMs' in data) { config.maxTransactionRetryTime = data.maxTxRetryTimeMs } + if ('notificationsMinSeverity' in data || 'notificationsDisabledCategories' in data) { + config.notificationFilter = { + minimumSeverityLevel: data.notificationsMinSeverity, + disabledCategories: data.notificationsDisabledCategories + } + } let driver try { driver = neo4j.driver(uri, parsedAuthToken, config) @@ -126,6 +133,13 @@ export function NewSession (neo4j, context, data, wire) { return } } + let notificationFilter + if ('notificationsMinSeverity' in data || 'notificationsDisabledCategories' in data) { + notificationFilter = { + minimumSeverityLevel: data.notificationsMinSeverity, + disabledCategories: data.notificationsDisabledCategories + } + } const driver = context.getDriver(driverId) const session = driver.session({ defaultAccessMode: accessMode, @@ -133,7 +147,8 @@ export function NewSession (neo4j, context, data, wire) { database, fetchSize, impersonatedUser, - bookmarkManager + bookmarkManager, + notificationFilter }) const id = context.addSession(session) wire.writeResponse(responses.Session({ id })) diff --git a/packages/testkit-backend/src/summary-binder.js b/packages/testkit-backend/src/summary-binder.js index d7221a3ea..62af6c1d0 100644 --- a/packages/testkit-backend/src/summary-binder.js +++ b/packages/testkit-backend/src/summary-binder.js @@ -42,6 +42,7 @@ function mapProfile (profile, child = false, binder) { function mapNotification (notification) { return { ...notification, + rawCategory: notification.rawCategory || '', position: Object.keys(notification.position).length !== 0 ? notification.position : undefined } }