From 8d2ee881a5733bfe94e7f35e3f95dcede64a95a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 16 May 2024 16:24:40 +0200 Subject: [PATCH 01/43] Implement filtering GqlStatusObject in Bot layer The bolt protocol 5.5 should support a slightly different way of configuring notifications filtering. This new configuration change the name of categories to classification to be aligned with the definitions on Gql standard. This changes doesn't includes add the protocol to handshake and the proper configuration on the driver side. --- .../src/bolt/bolt-protocol-v5x5.js | 166 +++ .../bolt/bolt-protocol-v5x5.transformer.js | 22 + .../src/bolt/request-message.js | 143 +- .../bolt-protocol-v5x5.test.js.snap | 61 + .../bolt/behaviour/notification-filter.js | 144 +- .../test/bolt/bolt-protocol-v5x5.test.js | 1157 +++++++++++++++++ .../test/bolt/request-message.test.js | 231 ++++ packages/core/src/internal/constants.ts | 2 + packages/core/src/notification-filter.ts | 19 + .../bolt/bolt-protocol-v5x5.js | 166 +++ .../bolt/bolt-protocol-v5x5.transformer.js | 22 + .../bolt-connection/bolt/request-message.js | 143 +- .../lib/core/internal/constants.ts | 2 + .../lib/core/notification-filter.ts | 20 + 14 files changed, 2249 insertions(+), 49 deletions(-) create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v5x5.transformer.js create mode 100644 packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x5.test.js.snap create mode 100644 packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.transformer.js diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js new file mode 100644 index 000000000..6295884de --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js @@ -0,0 +1,166 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 BoltProtocolV5x4 from './bolt-protocol-v5x4' + +import transformersFactories from './bolt-protocol-v5x5.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_5, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x4 { + get version () { + return BOLT_PROTOCOL_V5_5 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Initialize a connection with the server + * + * @param {Object} 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, boltAgent, 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.hello5x5(userAgent, boltAgent, 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.begin5x5({ 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, + 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.runWithMetadata5x5(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-v5x5.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.transformer.js new file mode 100644 index 000000000..8b5456f0b --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.transformer.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 v5x3 from './bolt-protocol-v5x3.transformer' + +export default { + ...v5x3 +} diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index c5d166b01..1d0e20aa6 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -172,15 +172,7 @@ export default class RequestMessage { 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 - } - } + appendLegacyNotificationFilterToMetadata(metadata, notificationFilter) if (routing) { metadata.routing = routing @@ -217,16 +209,45 @@ export default class RequestMessage { } } - if (notificationFilter) { - if (notificationFilter.minimumSeverityLevel) { - metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel - } + appendLegacyNotificationFilterToMetadata(metadata, notificationFilter) + + if (routing) { + metadata.routing = routing + } + + return new RequestMessage( + HELLO, + [metadata], + () => `HELLO ${json.stringify(metadata)}` + ) + } + + /** + * Create a new HELLO message. + * @param {string} userAgent the user agent. + * @param {string} boltAgent the bolt 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 hello5x5 (userAgent, boltAgent, notificationFilter = null, routing = null) { + const metadata = { } - if (notificationFilter.disabledCategories) { - metadata.notifications_disabled_categories = notificationFilter.disabledCategories + if (userAgent) { + metadata.user_agent = userAgent + } + + if (boltAgent) { + metadata.bolt_agent = { + product: boltAgent.product, + platform: boltAgent.platform, + language: boltAgent.language, + language_details: boltAgent.languageDetails } } + appendGqlNotificationFilterToMetadata(metadata, notificationFilter) + if (routing) { metadata.routing = routing } @@ -284,6 +305,27 @@ export default class RequestMessage { ) } + /** + * Create a new BEGIN message. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @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 begin5x5 ({ bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, { + appendNotificationFilter: appendGqlNotificationFilterToMetadata + }) + return new RequestMessage( + BEGIN, + [metadata], + () => `BEGIN ${json.stringify(metadata)}` + ) + } + /** * Get a COMMIT message. * @return {RequestMessage} the COMMIT message. @@ -325,6 +367,33 @@ export default class RequestMessage { ) } + /** + * Create a new RUN message with additional metadata. + * @param {string} query the cypher query. + * @param {Object} parameters the query parameters. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @param {string} database the database name. + * @param {string} mode the access mode. + * @param {string} impersonatedUser the impersonated user. + * @return {RequestMessage} new RUN message with additional metadata. + */ + static runWithMetadata5x5 ( + query, + parameters, + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {} + ) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, { + appendNotificationFilter: appendGqlNotificationFilterToMetadata + }) + return new RequestMessage( + RUN, + [query, parameters, metadata], + () => + `RUN ${query} ${json.stringify(parameters)} ${json.stringify(metadata)}` + ) + } + /** * Get a GOODBYE message. * @return {RequestMessage} the GOODBYE message. @@ -439,7 +508,7 @@ export default class RequestMessage { * @param {notificationFilter} notificationFilter the notification filter * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -459,15 +528,9 @@ 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 - } - } + const appendNotificationFilter = functions.appendNotificationFilter ?? appendLegacyNotificationFilterToMetadata + appendNotificationFilter(metadata, notificationFilter) return metadata } @@ -485,6 +548,38 @@ function buildStreamMetadata (stmtId, n) { return metadata } +function appendLegacyNotificationFilterToMetadata (metadata, notificationFilter) { + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + + if (notificationFilter.disabledClassifications) { + metadata.notifications_disabled_categories = notificationFilter.disabledClassifications + } + } +} + +function appendGqlNotificationFilterToMetadata (metadata, notificationFilter) { + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_classifications = notificationFilter.disabledCategories + } + + if (notificationFilter.disabledClassifications) { + metadata.notifications_disabled_classifications = notificationFilter.disabledClassifications + } + } +} + // constants for messages that never change const PULL_ALL_MESSAGE = new RequestMessage(PULL_ALL, [], () => 'PULL_ALL') const RESET_MESSAGE = new RequestMessage(RESET, [], () => 'RESET') diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x5.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x5.test.js.snap new file mode 100644 index 000000000..fb80b2d02 --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x5.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV5x5 .packable() should resultant function not pack graph types (Node) 1`] = `"It is not allowed to pass nodes in query parameters, given: (c:a {a:"b"})"`; + +exports[`#unit BoltProtocolV5x5 .packable() should resultant function not pack graph types (Path) 1`] = `"It is not allowed to pass paths in query parameters, given: [object Object]"`; + +exports[`#unit BoltProtocolV5x5 .packable() should resultant function not pack graph types (Relationship) 1`] = `"It is not allowed to pass relationships in query parameters, given: (e)-[:a {b:"c"}]->(f)"`; + +exports[`#unit BoltProtocolV5x5 .packable() should resultant function not pack graph types (UnboundRelationship) 1`] = `"It is not allowed to pass unbound relationships in query parameters, given: -[:a {b:"c"}]->"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x5 .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/notification-filter.js b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js index 800dd768a..3004b8403 100644 --- a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js +++ b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js @@ -138,6 +138,50 @@ export function shouldSupportNotificationFilterOnInitialize (createProtocol) { } } +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportGqlNotificationFilterOnInitialize (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.hello5x5(userAgent, undefined, 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 */ @@ -178,6 +222,46 @@ export function shouldSupportNotificationFilterOnBeginTransaction (createProtoco }) } +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportGqlNotificationFilterOnBeginTransaction (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.begin5x5({ 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 */ @@ -227,6 +311,55 @@ export function shouldSupportNotificationFilterOnRun (createProtocol) { }) } +/** + * @param {function(recorder:MessageRecorder):BoltProtocolV1} createProtocol The protocol factory + */ +export function shouldSupportGqlNotificationFilterOnRun (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.runWithMetadata5x5(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, @@ -242,8 +375,12 @@ export function notificationFilterFixture () { function notificationFilterSetFixture () { const minimumSeverityLevelSet = Object.values(notificationFilterMinimumSeverityLevel) const disabledCategories = Object.values(notificationFilterDisabledCategory) + // TODO: Fix the type + const disabledClassifications = Object.values(notificationFilterDisabledCategory) const disabledCategoriesSet = [...disabledCategories.keys()] .map(length => disabledCategories.slice(0, length + 1)) + const disabledClassificationsSet = [...disabledCategories.keys()] + .map(length => disabledCategories.slice(0, length + 1)) /** Polyfill flatMap for Node10 tests */ if (!minimumSeverityLevelSet.flatMap) { @@ -256,9 +393,14 @@ function notificationFilterSetFixture () { {}, ...minimumSeverityLevelSet.map(minimumSeverityLevel => ({ minimumSeverityLevel })), ...disabledCategoriesSet.map(disabledCategories => ({ disabledCategories })), + ...disabledClassificationsSet.map(disabledClassifications => ({ disabledClassifications })), ...minimumSeverityLevelSet.flatMap( minimumSeverityLevel => disabledCategories.map( - disabledCategories => ({ minimumSeverityLevel, disabledCategories }))) + disabledCategories => ({ minimumSeverityLevel, disabledCategories }))), + ...minimumSeverityLevelSet.flatMap( + minimumSeverityLevel => disabledClassifications.map( + disabledClassifications => ({ minimumSeverityLevel, disabledClassifications }))) + ] } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js new file mode 100644 index 000000000..d113ea62c --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js @@ -0,0 +1,1157 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 BoltProtocolV5x5 from '../../src/bolt/bolt-protocol-v5x5' +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, telemetryBehaviour } from './behaviour' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks }, + logger: { Logger }, + temporalUtil +} = internal + +describe('#unit BoltProtocolV5x5', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + telemetryBehaviour.protocolSupportsTelemetry(newProtocol) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(null, null, false) + + expect(protocol.version).toBe(5.5) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x5(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 BoltProtocolV5x5(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(clientName, boltAgent) + ) + 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([ + 'javascript-driver/5.5.0', + '', + undefined, + null + ])('should always use the user agent set by the user', (userAgent) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x5(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent, boltAgent, authToken }) + + protocol.verifyMessageCount(2) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello5x3(userAgent, boltAgent) + ) + 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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(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 BoltProtocolV5x5(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('notificationFilter', () => { + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnInitialize(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnBeginTransaction(newProtocol) + notificationFilterBehaviour.shouldSupportGqlNotificationFilterOnRun(newProtocol) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV5x5(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 BoltProtocolV5x5(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV5x5(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 resultant function not pack graph types (%s)', (_, graphType) => { + const protocol = new BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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') + ], + [ + 'DateTimeWithZoneId / Midnight', + new structure.Structure(0x69, [ + 1685397950, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2023, 5, 30, 0, 5, 50, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ] + ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x5( + 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 BoltProtocolV5x5( + 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 BoltProtocolV5x5(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/bolt-connection/test/bolt/request-message.test.js b/packages/bolt-connection/test/bolt/request-message.test.js index d644a39bd..b425163d6 100644 --- a/packages/bolt-connection/test/bolt/request-message.test.js +++ b/packages/bolt-connection/test/bolt/request-message.test.js @@ -646,6 +646,212 @@ describe('#unit RequestMessage', () => { }) }) + describe('BoltV5.5', () => { + describe('hello5x5', () => { + it.each( + gqlNotificationFilterFixtures() + )('should create HELLO message where notificationFilters=%o', (notificationFilter, expectedNotificationFilter) => { + const userAgent = 'my-driver/1.0.2' + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + + const expectedFields = { + user_agent: userAgent, + bolt_agent: { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + language_details: 'Node/16.0.1 (v8 1.7.0)' + }, + ...expectedNotificationFilter + } + + const message = RequestMessage.hello5x5(userAgent, boltAgent, notificationFilter) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + expectedFields + ]) + expect(message.toString()).toEqual( + `HELLO ${json.stringify(expectedFields)}` + ) + }) + + it('should create HELLO with NodeJS Bolt Agent', () => { + const userAgent = 'my-driver/1.0.2' + const boltAgent = { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + languageDetails: 'Node/16.0.1 (v8 1.7.0)' + } + + const expectedFields = { + user_agent: userAgent, + bolt_agent: { + product: 'neo4j-javascript/5.6', + platform: 'netbsd 1.1.1; Some arch', + language_details: 'Node/16.0.1 (v8 1.7.0)' + } + } + + const message = RequestMessage.hello5x5(userAgent, boltAgent) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + expectedFields + ]) + expect(message.toString()).toEqual( + `HELLO ${json.stringify(expectedFields)}` + ) + }) + + it('should create HELLO with Browser Bolt Agent', () => { + const userAgent = 'my-driver/1.0.2' + + const boltAgent = { + product: 'neo4j-javascript/5.3', + platform: 'Macintosh; Intel Mac OS X 10_15_7' + } + + const expectedFields = { + user_agent: userAgent, + bolt_agent: { + product: 'neo4j-javascript/5.3', + platform: 'Macintosh; Intel Mac OS X 10_15_7' + } + } + + const message = RequestMessage.hello5x5(userAgent, boltAgent) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + expectedFields + ]) + expect(message.toString()).toEqual( + `HELLO ${json.stringify(expectedFields)}` + ) + }) + + it('should create HELLO with Deno Bolt Agent', () => { + const userAgent = 'my-driver/1.0.2' + + const boltAgent = { + product: 'neo4j-javascript/5.3', + platform: 'macos 14.1; myArch', + languageDetails: 'Deno/1.19.1 (v8 8.1.39)' + } + + const expectedFields = { + user_agent: userAgent, + bolt_agent: { + product: 'neo4j-javascript/5.3', + platform: 'macos 14.1; myArch', + language_details: 'Deno/1.19.1 (v8 8.1.39)' + } + } + + const message = RequestMessage.hello5x5(userAgent, boltAgent) + + expect(message.signature).toEqual(0x01) + expect(message.fields).toEqual([ + expectedFields + ]) + expect(message.toString()).toEqual( + `HELLO ${json.stringify(expectedFields)}` + ) + }) + }) + + describe('begin5x5', () => { + it.each( + gqlNotificationFilterFixtures() + )('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.begin5x5({ 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)}` + ) + }) + }) + }) + + describe('run5x5', () => { + it.each( + gqlNotificationFilterFixtures() + )('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.runWithMetadata5x5(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 => { @@ -658,6 +864,31 @@ describe('#unit RequestMessage', () => { if (notificationFilter.disabledCategories) { expectedNotificationFilter.notifications_disabled_categories = notificationFilter.disabledCategories } + + if (notificationFilter.disabledClassifications) { + expectedNotificationFilter.notifications_disabled_categories = notificationFilter.disabledClassifications + } + } + return [notificationFilter, expectedNotificationFilter] + }) + } + + function gqlNotificationFilterFixtures () { + return notificationFilterBehaviour.notificationFilterFixture() + .map(notificationFilter => { + const expectedNotificationFilter = {} + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + expectedNotificationFilter.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + expectedNotificationFilter.notifications_disabled_classifications = notificationFilter.disabledCategories + } + + if (notificationFilter.disabledClassifications) { + expectedNotificationFilter.notifications_disabled_classifications = notificationFilter.disabledClassifications + } } return [notificationFilter, expectedNotificationFilter] }) diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index 9855854b9..e9cf978bf 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -36,6 +36,7 @@ const BOLT_PROTOCOL_V5_1: number = 5.1 const BOLT_PROTOCOL_V5_2: number = 5.2 const BOLT_PROTOCOL_V5_3: number = 5.3 const BOLT_PROTOCOL_V5_4: number = 5.4 +const BOLT_PROTOCOL_V5_5: number = 5.5 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -68,5 +69,6 @@ export { BOLT_PROTOCOL_V5_2, BOLT_PROTOCOL_V5_3, BOLT_PROTOCOL_V5_4, + BOLT_PROTOCOL_V5_5, TELEMETRY_APIS } diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts index fcd1dd761..d338ca590 100644 --- a/packages/core/src/notification-filter.ts +++ b/packages/core/src/notification-filter.ts @@ -67,6 +67,8 @@ Object.freeze(notificationFilterDisabledCategory) class NotificationFilter { minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel disabledCategories?: NotificationFilterDisabledCategory[] + // TODO: Fix the type + disabledClassifications?: NotificationFilterDisabledCategory[] /** * @constructor @@ -83,10 +85,27 @@ class NotificationFilter { /** * Categories the user would like to opt-out of receiving. + * + * + * This property is equivalent to {@link NotificationFilter#disabledClassifications} + * and it should not be enabled at same time. + * * @type {?NotificationFilterDisabledCategory[]} */ this.disabledCategories = undefined + /** + * Classifications the user would like to opt-out of receiving. + * + * This property is equivalent to {@link NotificationFilter#disabledCategories} + * and it should not be enabled at same time. + * + * // TODO: Fix the type + * @type {?NotificationFilterDisabledCategory[]} + * @experimental + */ + this.disabledClassifications = undefined + throw new Error('Not implemented') } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js new file mode 100644 index 000000000..a02f35e1c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js @@ -0,0 +1,166 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 BoltProtocolV5x4 from './bolt-protocol-v5x4.js' + +import transformersFactories from './bolt-protocol-v5x5.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_5, FETCH_ALL } +} = internal + +export default class BoltProtocol extends BoltProtocolV5x4 { + get version () { + return BOLT_PROTOCOL_V5_5 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Initialize a connection with the server + * + * @param {Object} 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, boltAgent, 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.hello5x5(userAgent, boltAgent, 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.begin5x5({ 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, + 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.runWithMetadata5x5(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-v5x5.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.transformer.js new file mode 100644 index 000000000..5967460db --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.transformer.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 v5x3 from './bolt-protocol-v5x3.transformer.js' + +export default { + ...v5x3 +} 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 cc164ccce..eee7f09a7 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 @@ -172,15 +172,7 @@ export default class RequestMessage { 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 - } - } + appendLegacyNotificationFilterToMetadata(metadata, notificationFilter) if (routing) { metadata.routing = routing @@ -217,16 +209,45 @@ export default class RequestMessage { } } - if (notificationFilter) { - if (notificationFilter.minimumSeverityLevel) { - metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel - } + appendLegacyNotificationFilterToMetadata(metadata, notificationFilter) + + if (routing) { + metadata.routing = routing + } + + return new RequestMessage( + HELLO, + [metadata], + () => `HELLO ${json.stringify(metadata)}` + ) + } + + /** + * Create a new HELLO message. + * @param {string} userAgent the user agent. + * @param {string} boltAgent the bolt 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 hello5x5 (userAgent, boltAgent, notificationFilter = null, routing = null) { + const metadata = { } - if (notificationFilter.disabledCategories) { - metadata.notifications_disabled_categories = notificationFilter.disabledCategories + if (userAgent) { + metadata.user_agent = userAgent + } + + if (boltAgent) { + metadata.bolt_agent = { + product: boltAgent.product, + platform: boltAgent.platform, + language: boltAgent.language, + language_details: boltAgent.languageDetails } } + appendGqlNotificationFilterToMetadata(metadata, notificationFilter) + if (routing) { metadata.routing = routing } @@ -284,6 +305,27 @@ export default class RequestMessage { ) } + /** + * Create a new BEGIN message. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @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 begin5x5 ({ bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, { + appendNotificationFilter: appendGqlNotificationFilterToMetadata + }) + return new RequestMessage( + BEGIN, + [metadata], + () => `BEGIN ${json.stringify(metadata)}` + ) + } + /** * Get a COMMIT message. * @return {RequestMessage} the COMMIT message. @@ -325,6 +367,33 @@ export default class RequestMessage { ) } + /** + * Create a new RUN message with additional metadata. + * @param {string} query the cypher query. + * @param {Object} parameters the query parameters. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @param {string} database the database name. + * @param {string} mode the access mode. + * @param {string} impersonatedUser the impersonated user. + * @return {RequestMessage} new RUN message with additional metadata. + */ + static runWithMetadata5x5 ( + query, + parameters, + { bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter } = {} + ) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, { + appendNotificationFilter: appendGqlNotificationFilterToMetadata + }) + return new RequestMessage( + RUN, + [query, parameters, metadata], + () => + `RUN ${query} ${json.stringify(parameters)} ${json.stringify(metadata)}` + ) + } + /** * Get a GOODBYE message. * @return {RequestMessage} the GOODBYE message. @@ -439,7 +508,7 @@ export default class RequestMessage { * @param {notificationFilter} notificationFilter the notification filter * @return {Object} a metadata object. */ -function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter) { +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { const metadata = {} if (!bookmarks.isEmpty()) { metadata.bookmarks = bookmarks.values() @@ -459,15 +528,9 @@ 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 - } - } + const appendNotificationFilter = functions.appendNotificationFilter ?? appendLegacyNotificationFilterToMetadata + appendNotificationFilter(metadata, notificationFilter) return metadata } @@ -485,6 +548,38 @@ function buildStreamMetadata (stmtId, n) { return metadata } +function appendLegacyNotificationFilterToMetadata (metadata, notificationFilter) { + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_categories = notificationFilter.disabledCategories + } + + if (notificationFilter.disabledClassifications) { + metadata.notifications_disabled_categories = notificationFilter.disabledClassifications + } + } +} + +function appendGqlNotificationFilterToMetadata (metadata, notificationFilter) { + if (notificationFilter) { + if (notificationFilter.minimumSeverityLevel) { + metadata.notifications_minimum_severity = notificationFilter.minimumSeverityLevel + } + + if (notificationFilter.disabledCategories) { + metadata.notifications_disabled_classifications = notificationFilter.disabledCategories + } + + if (notificationFilter.disabledClassifications) { + metadata.notifications_disabled_classifications = notificationFilter.disabledClassifications + } + } +} + // constants for messages that never change const PULL_ALL_MESSAGE = new RequestMessage(PULL_ALL, [], () => 'PULL_ALL') const RESET_MESSAGE = new RequestMessage(RESET, [], () => 'RESET') diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index 9855854b9..e9cf978bf 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -36,6 +36,7 @@ const BOLT_PROTOCOL_V5_1: number = 5.1 const BOLT_PROTOCOL_V5_2: number = 5.2 const BOLT_PROTOCOL_V5_3: number = 5.3 const BOLT_PROTOCOL_V5_4: number = 5.4 +const BOLT_PROTOCOL_V5_5: number = 5.5 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -68,5 +69,6 @@ export { BOLT_PROTOCOL_V5_2, BOLT_PROTOCOL_V5_3, BOLT_PROTOCOL_V5_4, + BOLT_PROTOCOL_V5_5, TELEMETRY_APIS } diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index bf265eafb..185c48a92 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -67,6 +67,8 @@ Object.freeze(notificationFilterDisabledCategory) class NotificationFilter { minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel disabledCategories?: NotificationFilterDisabledCategory[] + // TODO: Fix the type + disabledClassifications?: NotificationFilterDisabledCategory[] /** * @constructor @@ -83,10 +85,28 @@ class NotificationFilter { /** * Categories the user would like to opt-out of receiving. + * + * + * This property is equivalent to {@link NotificationFilter#disabledClassifications} + * and it should not be enabled at same time. + * * @type {?NotificationFilterDisabledCategory[]} */ this.disabledCategories = undefined + + /** + * Classifications the user would like to opt-out of receiving. + * + * This property is equivalent to {@link NotificationFilter#disabledCategories} + * and it should not be enabled at same time. + * + * // TODO: Fix the type + * @type {?NotificationFilterDisabledCategory[]} + * @experimental + */ + this.disabledClassifications = undefined + throw new Error('Not implemented') } } From a80c3039c550612121827131f18ab9603a108626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 16 May 2024 16:55:44 +0200 Subject: [PATCH 02/43] Sync deno --- .../lib/core/notification-filter.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index 185c48a92..a1576f987 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -85,25 +85,24 @@ class NotificationFilter { /** * Categories the user would like to opt-out of receiving. - * - * + * + * * This property is equivalent to {@link NotificationFilter#disabledClassifications} - * and it should not be enabled at same time. - * + * and it should not be enabled at same time. + * * @type {?NotificationFilterDisabledCategory[]} */ this.disabledCategories = undefined - /** * Classifications the user would like to opt-out of receiving. - * + * * This property is equivalent to {@link NotificationFilter#disabledCategories} - * and it should not be enabled at same time. - * + * and it should not be enabled at same time. + * * // TODO: Fix the type * @type {?NotificationFilterDisabledCategory[]} - * @experimental + * @experimental */ this.disabledClassifications = undefined From 28917a81f742a5a0334e5130b1d41071611b522e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 16 May 2024 17:00:51 +0200 Subject: [PATCH 03/43] Add Bolt 5.5 to handshake --- packages/bolt-connection/src/bolt/create.js | 9 +++++++++ packages/bolt-connection/src/bolt/handshake.js | 2 +- packages/bolt-connection/test/bolt/index.test.js | 8 +++++--- .../neo4j-driver-deno/lib/bolt-connection/bolt/create.js | 9 +++++++++ .../lib/bolt-connection/bolt/handshake.js | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index 1e0fbc9cd..ed1c502e7 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -29,6 +29,7 @@ import BoltProtocolV5x1 from './bolt-protocol-v5x1' import BoltProtocolV5x2 from './bolt-protocol-v5x2' import BoltProtocolV5x3 from './bolt-protocol-v5x3' import BoltProtocolV5x4 from './bolt-protocol-v5x4' +import BoltProtocolV5x5 from './bolt-protocol-v5x5' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -229,6 +230,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.5: + return new BoltProtocolV5x5(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 92425826e..ae7aaa913 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -76,7 +76,7 @@ function parseNegotiatedResponse (buffer, log) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - [version(5, 4), version(5, 0)], + [version(5, 5), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index 909bef52e..043c37e1f 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -33,6 +33,7 @@ import BoltProtocolV5x1 from '../../src/bolt/bolt-protocol-v5x1' import BoltProtocolV5x2 from '../../src/bolt/bolt-protocol-v5x2' import BoltProtocolV5x3 from '../../src/bolt/bolt-protocol-v5x3' import BoltProtocolV5x4 from '../../src/bolt/bolt-protocol-v5x4' +import BoltProtocolV5x5 from '../../src/bolt/bolt-protocol-v5x5' const { logger: { Logger } @@ -46,13 +47,13 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x4to5x0 = '00 04 04 05' + const protocolVersion5x5to5x0 = '00 05 05 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion5x4to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x5to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) @@ -390,7 +391,8 @@ describe('#unit Bolt', () => { v(5.1, BoltProtocolV5x1), v(5.2, BoltProtocolV5x2), v(5.3, BoltProtocolV5x3), - v(5.4, BoltProtocolV5x4) + v(5.4, BoltProtocolV5x4), + v(5.5, BoltProtocolV5x5) ] availableProtocols.forEach(lambda) 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 82cbfae31..3e1050afe 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -29,6 +29,7 @@ import BoltProtocolV5x1 from './bolt-protocol-v5x1.js' import BoltProtocolV5x2 from './bolt-protocol-v5x2.js' import BoltProtocolV5x3 from './bolt-protocol-v5x3.js' import BoltProtocolV5x4 from './bolt-protocol-v5x4.js' +import BoltProtocolV5x5 from './bolt-protocol-v5x5.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -229,6 +230,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.5: + return new BoltProtocolV5x5(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 ade55d3cb..de214d2c1 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, log) { */ function newHandshakeBuffer () { return createHandshakeMessage([ - [version(5, 4), version(5, 0)], + [version(5, 5), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) From 6d3ed0fe92d216f1ff4274a19acea025c50dd839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 17 May 2024 17:48:27 +0200 Subject: [PATCH 04/43] Implement filtering in core and exposing it throug api --- packages/core/src/driver.ts | 5 ++ packages/core/src/index.ts | 10 +++ packages/core/src/notification-filter.ts | 25 +++++-- packages/core/src/result-summary.ts | 70 +++++++++++++++++- packages/core/test/driver.test.ts | 17 ++++- .../core/test/notification-filter.test.ts | 26 +++++++ packages/core/test/result-summary.test.ts | 27 ++++++- .../utils/notification-filters.fixtures.ts | 49 +++++++++++++ packages/neo4j-driver-deno/lib/core/driver.ts | 5 ++ packages/neo4j-driver-deno/lib/core/index.ts | 10 +++ .../lib/core/notification-filter.ts | 25 +++++-- .../lib/core/result-summary.ts | 71 ++++++++++++++++++- packages/neo4j-driver-deno/lib/mod.ts | 10 +++ packages/neo4j-driver-lite/src/index.ts | 10 +++ packages/neo4j-driver/src/index.js | 4 ++ .../neo4j-driver/test/types/index.test.ts | 36 ++++++++++ packages/neo4j-driver/types/index.d.ts | 10 +++ 17 files changed, 392 insertions(+), 18 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index e045204cb..9233812fe 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -901,6 +901,11 @@ function validateConfig (config: any, log: Logger): any { 'where a new connection is created while it is acquired' ) } + + if (config.notificationFilter?.disabledCategories != null && config.notificationFilter?.disabledClassifications != null) { + throw new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.') + } + return config } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index af9677287..b92c5a492 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,13 +64,17 @@ import ResultSummary, { QueryStatistics, Stats, NotificationSeverityLevel, + NotificationClassification, NotificationCategory, notificationCategory, + notificationClassification, notificationSeverityLevel } from './result-summary' import NotificationFilter, { notificationFilterDisabledCategory, NotificationFilterDisabledCategory, + notificationFilterDisabledClassification, + NotificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, NotificationFilterMinimumSeverityLevel } from './notification-filter' @@ -168,8 +172,10 @@ const forExport = { routing, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, resolveCertificateProvider @@ -239,8 +245,10 @@ export { routing, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, resolveCertificateProvider @@ -265,9 +273,11 @@ export type { RecordShape, ResultTransformer, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, ClientCertificate, ClientCertificateProvider, diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts index d338ca590..e5b3c5fc0 100644 --- a/packages/core/src/notification-filter.ts +++ b/packages/core/src/notification-filter.ts @@ -16,6 +16,7 @@ */ import { NotificationCategory, + NotificationClassification, NotificationSeverityLevel } from './result-summary' @@ -56,6 +57,17 @@ const notificationFilterDisabledCategory: EnumRecord +/** + * @typedef {NotificationFilterDisabledCategory} NotificationFilterDisabledClassification + * @experimental + */ +/** + * Constants that represents the disabled classifications in the {@link NotificationFilter} + * @experimental + */ +const notificationFilterDisabledClassification: EnumRecord = notificationFilterDisabledCategory + /** * The notification filter object which can be configured in * the session and driver creation. @@ -67,8 +79,7 @@ Object.freeze(notificationFilterDisabledCategory) class NotificationFilter { minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel disabledCategories?: NotificationFilterDisabledCategory[] - // TODO: Fix the type - disabledClassifications?: NotificationFilterDisabledCategory[] + disabledClassifications?: NotificationFilterDisabledClassification[] /** * @constructor @@ -100,8 +111,8 @@ class NotificationFilter { * This property is equivalent to {@link NotificationFilter#disabledCategories} * and it should not be enabled at same time. * - * // TODO: Fix the type - * @type {?NotificationFilterDisabledCategory[]} + * + * @type {?NotificationFilterDisabledClassification[]} * @experimental */ this.disabledClassifications = undefined @@ -114,10 +125,12 @@ export default NotificationFilter export { notificationFilterMinimumSeverityLevel, - notificationFilterDisabledCategory + notificationFilterDisabledCategory, + notificationFilterDisabledClassification } export type { NotificationFilterMinimumSeverityLevel, - NotificationFilterDisabledCategory + NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification } diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index c909cae1f..fb79a3cef 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -30,6 +30,7 @@ class ResultSummary { plan: Plan | false profile: ProfiledPlan | false notifications: Notification[] + gqlStatusObjects: GqlStatusObject[] server: ServerInfo resultConsumedAfter: T resultAvailableAfter: T @@ -107,6 +108,13 @@ class ResultSummary { */ this.notifications = this._buildNotifications(metadata.notifications) + /** + * @type {Array} + * @public + * @experimental + */ + this.gqlStatusObjects = [] + /** * The basic information of the server where the result is obtained from. * @type {ServerInfo} @@ -456,6 +464,17 @@ const notificationCategory: { [key in NotificationCategory]: key } = { Object.freeze(notificationCategory) const categories = Object.values(notificationCategory) +type NotificationClassification = NotificationCategory +/** + * @typedef {NotificationCategory} NotificationClassification + * @experimental + */ +/** + * Constants that represents the Classification in the {@link GqlStatusObject} + * @experimental + */ +const notificationClassification = notificationCategory + /** * Class for Cypher notifications * @access public @@ -600,6 +619,51 @@ class Notification { } } +/** + * Representation for GqlStatusObject found when executing a query. + *

+ * This object represents a status of query execution. + * This status is a superset of {@link Notification}. + * + * @experimental + */ +class GqlStatusObject { + /** + * The GQLSTATUS + */ + getGqlStatus (): String { + return '' + } + + /** + * Retrieve the severity from the diagnostic record. + */ + getSeverity (): NotificationSeverityLevel { + return notificationSeverityLevel.UNKNOWN + } + + /** + * Retrieve the severity from the diagnostic record as string. + */ + getRawSeverity (): String { + return '' + } + + /** + * Retrieve the classification from the diagnostic record. + */ + getClassification (): NotificationClassification { + return notificationClassification.UNKNOWN + } + + /** + * Retrieve the classification from the diagnostic record as string + */ + getRawClassification (): String { + return '' + } +} + /** * Class for exposing server info from a result. * @access public @@ -685,12 +749,14 @@ export { QueryStatistics, Stats, notificationSeverityLevel, - notificationCategory + notificationCategory, + notificationClassification } export type { NotificationPosition, NotificationSeverityLevel, - NotificationCategory + NotificationCategory, + NotificationClassification } export default ResultSummary diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index f4e810efb..e81f4e423 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -24,7 +24,7 @@ import { ConfiguredCustomResolver } from '../src/internal/resolver' import { LogLevel } from '../src/types' import resultTransformers from '../src/result-transformers' import Record, { RecordShape } from '../src/record' -import { validNotificationFilters } from './utils/notification-filters.fixtures' +import { invalidNotificationFilters, validNotificationFilters } from './utils/notification-filters.fixtures' describe('Driver', () => { let driver: Driver | null @@ -603,6 +603,21 @@ describe('Driver', () => { await driver.close() }) + + it.each( + invalidNotificationFilters() + )('should fail on invalid notification filters', async (notificationFilter?: NotificationFilter) => { + const createConnectionProviderMock = jest.fn(mockCreateConnectonProvider(connectionProvider)) + + expect(() => new Driver( + META_INFO, + { notificationFilter }, + createConnectionProviderMock, + createSession + )).toThrow(new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.')) + + expect(createConnectionProviderMock).not.toHaveBeenCalled() + }) }) describe('config', () => { diff --git a/packages/core/test/notification-filter.test.ts b/packages/core/test/notification-filter.test.ts index c0f65661e..c629428ed 100644 --- a/packages/core/test/notification-filter.test.ts +++ b/packages/core/test/notification-filter.test.ts @@ -17,6 +17,8 @@ import { NotificationFilterDisabledCategory, notificationFilterDisabledCategory, + NotificationFilterDisabledClassification, + notificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, notificationFilterMinimumSeverityLevel } from '../src/notification-filter' @@ -61,6 +63,30 @@ describe('notificationFilterDisabledCategory', () => { }) }) +describe('notificationFilterDisabledClassification', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationFilterDisabledClassification)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationFilterDisabledClassification', () => { + for (const [, value] of Object.entries(notificationFilterDisabledClassification)) { + const assignableValue: NotificationFilterDisabledClassification = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidNotificationsCategories())('should have %s as key', (category) => { + const keys = Object.keys(notificationFilterDisabledCategory) + expect(keys.includes(category)).toBe(true) + }) + + it('should be notificationFilterDisabledCategory', () => { + expect(notificationFilterDisabledClassification).toBe(notificationFilterDisabledCategory) + }) +}) + function getValidNotificationsSeverityLevels (): NotificationFilterMinimumSeverityLevel[] { return [ 'OFF', diff --git a/packages/core/test/result-summary.test.ts b/packages/core/test/result-summary.test.ts index f9cfa4aad..ef817a575 100644 --- a/packages/core/test/result-summary.test.ts +++ b/packages/core/test/result-summary.test.ts @@ -25,7 +25,8 @@ import { notificationCategory, ProfiledPlan, QueryStatistics, - Stats + Stats, + notificationClassification } from '../src/result-summary' import fc from 'fast-check' @@ -162,6 +163,30 @@ describe('notificationCategory', () => { }) }) +describe('notificationClassification', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationClassification)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationClassification', () => { + for (const [, value] of Object.entries(notificationClassification)) { + const assignableValue: NotificationCategory = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidCategories())('should have %s as key', (category) => { + const keys = Object.keys(notificationClassification) + expect(keys.includes(category)).toBe(true) + }) + + it('should be notificationCategory', () => { + expect(notificationClassification).toBe(notificationCategory) + }) +}) + describe('ProfilePlan', () => { describe.each([ 'dbHits', diff --git a/packages/core/test/utils/notification-filters.fixtures.ts b/packages/core/test/utils/notification-filters.fixtures.ts index 508d1125d..3d36e33ae 100644 --- a/packages/core/test/utils/notification-filters.fixtures.ts +++ b/packages/core/test/utils/notification-filters.fixtures.ts @@ -15,6 +15,7 @@ * limitations under the License. */ import { NotificationFilter, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel } from '../../src' +import { notificationFilterDisabledClassification } from '../../src/notification-filter' export function validNotificationFilters (): Array { return [ @@ -40,6 +41,54 @@ export function validNotificationFilters (): Array { + return [ + { + disabledCategories: [], + disabledClassifications: [] + }, + { + disabledCategories: [notificationFilterDisabledCategory.GENERIC, notificationFilterDisabledCategory.PERFORMANCE], + disabledClassifications: [] + }, + { + disabledCategories: [], + disabledClassifications: [notificationFilterDisabledClassification.GENERIC, notificationFilterDisabledClassification.PERFORMANCE] + }, + { + minimumSeverityLevel: notificationFilterMinimumSeverityLevel.INFORMATION, + disabledCategories: [], + disabledClassifications: [] + }, + { + minimumSeverityLevel: notificationFilterMinimumSeverityLevel.INFORMATION, + disabledCategories: [notificationFilterDisabledCategory.GENERIC, notificationFilterDisabledCategory.PERFORMANCE], + disabledClassifications: [] + }, + { + minimumSeverityLevel: notificationFilterMinimumSeverityLevel.INFORMATION, + disabledCategories: [], + disabledClassifications: [notificationFilterDisabledClassification.GENERIC, notificationFilterDisabledClassification.PERFORMANCE] } ] } diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 3f7a81b36..40b9d4a8a 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -901,6 +901,11 @@ function validateConfig (config: any, log: Logger): any { 'where a new connection is created while it is acquired' ) } + + if (config.notificationFilter?.disabledCategories != null && config.notificationFilter?.disabledClassifications != null) { + throw new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.') + } + return config } diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index ad3dd1f76..74dedc154 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -64,13 +64,17 @@ import ResultSummary, { QueryStatistics, Stats, NotificationSeverityLevel, + NotificationClassification, NotificationCategory, notificationCategory, + notificationClassification, notificationSeverityLevel } from './result-summary.ts' import NotificationFilter, { notificationFilterDisabledCategory, NotificationFilterDisabledCategory, + notificationFilterDisabledClassification, + NotificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, NotificationFilterMinimumSeverityLevel } from './notification-filter.ts' @@ -168,8 +172,10 @@ const forExport = { routing, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, resolveCertificateProvider @@ -239,8 +245,10 @@ export { routing, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders, resolveCertificateProvider @@ -265,9 +273,11 @@ export type { RecordShape, ResultTransformer, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, ClientCertificate, ClientCertificateProvider, diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index a1576f987..4431f1ec2 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -16,6 +16,7 @@ */ import { NotificationCategory, + NotificationClassification, NotificationSeverityLevel } from './result-summary.ts' @@ -56,6 +57,17 @@ const notificationFilterDisabledCategory: EnumRecord +/** + * @typedef {NotificationFilterDisabledCategory} NotificationFilterDisabledClassification + * @experimental + */ +/** + * Constants that represents the disabled classifications in the {@link NotificationFilter} + * @experimental + */ +const notificationFilterDisabledClassification: EnumRecord = notificationFilterDisabledCategory + /** * The notification filter object which can be configured in * the session and driver creation. @@ -67,8 +79,7 @@ Object.freeze(notificationFilterDisabledCategory) class NotificationFilter { minimumSeverityLevel?: NotificationFilterMinimumSeverityLevel disabledCategories?: NotificationFilterDisabledCategory[] - // TODO: Fix the type - disabledClassifications?: NotificationFilterDisabledCategory[] + disabledClassifications?: NotificationFilterDisabledClassification[] /** * @constructor @@ -100,8 +111,8 @@ class NotificationFilter { * This property is equivalent to {@link NotificationFilter#disabledCategories} * and it should not be enabled at same time. * - * // TODO: Fix the type - * @type {?NotificationFilterDisabledCategory[]} + * + * @type {?NotificationFilterDisabledClassification[]} * @experimental */ this.disabledClassifications = undefined @@ -114,10 +125,12 @@ export default NotificationFilter export { notificationFilterMinimumSeverityLevel, - notificationFilterDisabledCategory + notificationFilterDisabledCategory, + notificationFilterDisabledClassification } export type { NotificationFilterMinimumSeverityLevel, - NotificationFilterDisabledCategory + NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification } diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index 7fffd9ee5..9f3105db8 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -30,6 +30,7 @@ class ResultSummary { plan: Plan | false profile: ProfiledPlan | false notifications: Notification[] + gqlStatusObjects: GqlStatusObject[] server: ServerInfo resultConsumedAfter: T resultAvailableAfter: T @@ -107,6 +108,13 @@ class ResultSummary { */ this.notifications = this._buildNotifications(metadata.notifications) + /** + * @type {Array} + * @public + * @experimental + */ + this.gqlStatusObjects = [] + /** * The basic information of the server where the result is obtained from. * @type {ServerInfo} @@ -456,6 +464,18 @@ const notificationCategory: { [key in NotificationCategory]: key } = { Object.freeze(notificationCategory) const categories = Object.values(notificationCategory) + +type NotificationClassification = NotificationCategory +/** + * @typedef {NotificationCategory} NotificationClassification + * @experimental + */ +/** + * Constants that represents the Classification in the {@link GqlStatusObject} + * @experimental + */ +const notificationClassification = notificationCategory + /** * Class for Cypher notifications * @access public @@ -600,6 +620,51 @@ class Notification { } } +/** + * Representation for GqlStatusObject found when executing a query. + *

+ * This object represents a status of query execution. + * This status is a superset of {@link Notification}. + * + * @experimental + */ +class GqlStatusObject { + /** + * The GQLSTATUS + */ + getGqlStatus(): String { + return "" + } + + /** + * Retrieve the severity from the diagnostic record. + */ + getSeverity(): NotificationSeverityLevel { + return notificationSeverityLevel.UNKNOWN + } + + /** + * Retrieve the severity from the diagnostic record as string. + */ + getRawSeverity(): String { + return ""; + } + + /** + * Retrieve the classification from the diagnostic record. + */ + getClassification(): NotificationClassification { + return notificationClassification.UNKNOWN + } + + /** + * Retrieve the classification from the diagnostic record as string + */ + getRawClassification(): String { + return "" + } +} + /** * Class for exposing server info from a result. * @access public @@ -685,12 +750,14 @@ export { QueryStatistics, Stats, notificationSeverityLevel, - notificationCategory + notificationCategory, + notificationClassification } export type { NotificationPosition, NotificationSeverityLevel, - NotificationCategory + NotificationCategory, + NotificationClassification } export default ResultSummary diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index b8e652221..d0cd9ab3c 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -59,9 +59,13 @@ import { Notification, notificationCategory, NotificationCategory, + notificationClassification, + NotificationClassification, NotificationFilter, NotificationFilterDisabledCategory, notificationFilterDisabledCategory, + NotificationFilterDisabledClassification, + notificationFilterDisabledClassification, AuthTokenManager, AuthTokenAndExpiration, staticAuthTokenManager, @@ -429,8 +433,10 @@ const forExport = { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders } @@ -497,8 +503,10 @@ export { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders } @@ -522,9 +530,11 @@ export type { RecordShape, ResultTransformer, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, ClientCertificate, ClientCertificateProvider, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index a9b440367..cff40ae76 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -59,9 +59,13 @@ import { Notification, notificationCategory, NotificationCategory, + notificationClassification, + NotificationClassification, NotificationFilter, NotificationFilterDisabledCategory, notificationFilterDisabledCategory, + NotificationFilterDisabledClassification, + notificationFilterDisabledClassification, AuthTokenManager, AuthTokenAndExpiration, staticAuthTokenManager, @@ -428,8 +432,10 @@ const forExport = { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders } @@ -496,8 +502,10 @@ export { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders } @@ -521,9 +529,11 @@ export type { RecordShape, ResultTransformer, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, ClientCertificate, ClientCertificateProvider, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index e0b2b4aa4..4d75befa5 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -70,8 +70,10 @@ import { routing, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, staticAuthTokenManager, clientCertificateProviders, @@ -464,8 +466,10 @@ export { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, clientCertificateProviders } diff --git a/packages/neo4j-driver/test/types/index.test.ts b/packages/neo4j-driver/test/types/index.test.ts index bbc4dcf35..2dcf2dd86 100644 --- a/packages/neo4j-driver/test/types/index.test.ts +++ b/packages/neo4j-driver/test/types/index.test.ts @@ -39,10 +39,14 @@ import { NotificationSeverityLevel, notificationCategory, NotificationCategory, + notificationClassification, + NotificationClassification, notificationFilterMinimumSeverityLevel, NotificationFilterMinimumSeverityLevel, NotificationFilterDisabledCategory, notificationFilterDisabledCategory, + NotificationFilterDisabledClassification, + notificationFilterDisabledClassification, authTokenManagers } from '../../types/index' @@ -159,6 +163,25 @@ const unrecognizedCategory: NotificationCategory = notificationCategory.UNRECOGN const unsupportedCategory: NotificationCategory = notificationCategory.UNSUPPORTED const unknownCategory: NotificationCategory = notificationCategory.UNKNOWN +const hintClassificationString: string = notificationClassification.HINT +const deprecationClassificationString: string = notificationClassification.DEPRECATION +const performanceClassificationString: string = notificationClassification.PERFORMANCE +const topologyClassificationString: string = notificationClassification.TOPOLOGY +const securityClassificationString: string = notificationClassification.SECURITY +const genericClassificationString: string = notificationClassification.GENERIC +const unrecognizedClassificationString: string = notificationClassification.UNRECOGNIZED +const unsupportedClassificationString: string = notificationClassification.UNSUPPORTED +const unknownClassificationString: string = notificationClassification.UNKNOWN +const hintClassification: NotificationClassification = notificationClassification.HINT +const deprecationClassification: NotificationClassification = notificationClassification.DEPRECATION +const performanceClassification: NotificationClassification = notificationClassification.PERFORMANCE +const topologyClassification: NotificationClassification = notificationClassification.TOPOLOGY +const securityClassification: NotificationClassification = notificationClassification.SECURITY +const genericClassification: NotificationClassification = notificationClassification.GENERIC +const unrecognizedClassification: NotificationClassification = notificationClassification.UNRECOGNIZED +const unsupportedClassification: NotificationClassification = notificationClassification.UNSUPPORTED +const unknownClassification: NotificationClassification = notificationClassification.UNKNOWN + const offNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.OFF const warningNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.WARNING const infoNotificationFilterMinimumSeverityLevelString: string = notificationFilterMinimumSeverityLevel.INFORMATION @@ -178,3 +201,16 @@ const performanceDisabledCategory: NotificationFilterDisabledCategory = notifica const genericDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.GENERIC const unrecognizedDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.UNRECOGNIZED const unsupportedDisabledCategory: NotificationFilterDisabledCategory = notificationFilterDisabledCategory.UNSUPPORTED + +const hintDisabledClassificationString: string = notificationFilterDisabledClassification.HINT +const deprecationDisabledClassificationString: string = notificationFilterDisabledClassification.DEPRECATION +const performanceDisabledClassificationString: string = notificationFilterDisabledClassification.PERFORMANCE +const genericDisabledClassificationString: string = notificationFilterDisabledClassification.GENERIC +const unrecognizedDisabledClassificationString: string = notificationFilterDisabledClassification.UNRECOGNIZED +const unsupportedDisabledClassificationString: string = notificationFilterDisabledClassification.UNSUPPORTED +const hintDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.HINT +const deprecationDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.DEPRECATION +const performanceDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.PERFORMANCE +const genericDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.GENERIC +const unrecognizedDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.UNRECOGNIZED +const unsupportedDisabledClassification: NotificationFilterDisabledClassification = notificationFilterDisabledClassification.UNSUPPORTED diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 18c9a5cbf..ac7efbef1 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -77,13 +77,17 @@ import { resultTransformers, ResultTransformer, notificationCategory, + notificationClassification, notificationSeverityLevel, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, AuthTokenManager, AuthTokenAndExpiration, @@ -284,8 +288,10 @@ declare const forExport: { bookmarkManager: typeof bookmarkManager resultTransformers: typeof resultTransformers notificationCategory: typeof notificationCategory + notificationClassification: typeof notificationClassification notificationSeverityLevel: typeof notificationSeverityLevel notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory + notificationFilterDisabledClassification: typeof notificationFilterDisabledClassification notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel logging: typeof logging clientCertificateProviders: typeof clientCertificateProviders @@ -361,8 +367,10 @@ export { bookmarkManager, resultTransformers, notificationCategory, + notificationClassification, notificationSeverityLevel, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel, logging, clientCertificateProviders @@ -378,9 +386,11 @@ export type { RecordShape, ResultTransformer, NotificationCategory, + NotificationClassification, NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, + NotificationFilterDisabledClassification, NotificationFilterMinimumSeverityLevel, AuthTokenManager, AuthTokenAndExpiration, From 24f6225c522c64f88e3f65bf47ef29173a64d9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 17 May 2024 18:00:32 +0200 Subject: [PATCH 05/43] Fix docs --- packages/core/src/notification-filter.ts | 2 ++ packages/core/src/result-summary.ts | 1 + .../lib/core/notification-filter.ts | 4 +++- .../lib/core/result-summary.ts | 24 +++++++++---------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts index e5b3c5fc0..558cd06b4 100644 --- a/packages/core/src/notification-filter.ts +++ b/packages/core/src/notification-filter.ts @@ -64,6 +64,8 @@ type NotificationFilterDisabledClassification = ExcludeUnknown = notificationFilterDisabledCategory diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index fb79a3cef..4114f26b9 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -471,6 +471,7 @@ type NotificationClassification = NotificationCategory */ /** * Constants that represents the Classification in the {@link GqlStatusObject} + * @type {notificationCategory} * @experimental */ const notificationClassification = notificationCategory diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index 4431f1ec2..3472ef464 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -64,6 +64,8 @@ type NotificationFilterDisabledClassification = ExcludeUnknown = notificationFilterDisabledCategory @@ -111,7 +113,7 @@ class NotificationFilter { * This property is equivalent to {@link NotificationFilter#disabledCategories} * and it should not be enabled at same time. * - * + * * @type {?NotificationFilterDisabledClassification[]} * @experimental */ diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index 9f3105db8..f729d6554 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -464,7 +464,6 @@ const notificationCategory: { [key in NotificationCategory]: key } = { Object.freeze(notificationCategory) const categories = Object.values(notificationCategory) - type NotificationClassification = NotificationCategory /** * @typedef {NotificationCategory} NotificationClassification @@ -472,6 +471,7 @@ type NotificationClassification = NotificationCategory */ /** * Constants that represents the Classification in the {@link GqlStatusObject} + * @type {notificationCategory} * @experimental */ const notificationClassification = notificationCategory @@ -624,44 +624,44 @@ class Notification { * Representation for GqlStatusObject found when executing a query. *

* This object represents a status of query execution. - * This status is a superset of {@link Notification}. - * + * This status is a superset of {@link Notification}. + * * @experimental */ class GqlStatusObject { /** * The GQLSTATUS */ - getGqlStatus(): String { - return "" + getGqlStatus (): String { + return '' } /** * Retrieve the severity from the diagnostic record. */ - getSeverity(): NotificationSeverityLevel { + getSeverity (): NotificationSeverityLevel { return notificationSeverityLevel.UNKNOWN - } + } /** * Retrieve the severity from the diagnostic record as string. */ - getRawSeverity(): String { - return ""; + getRawSeverity (): String { + return '' } /** * Retrieve the classification from the diagnostic record. */ - getClassification(): NotificationClassification { + getClassification (): NotificationClassification { return notificationClassification.UNKNOWN } /** * Retrieve the classification from the diagnostic record as string */ - getRawClassification(): String { - return "" + getRawClassification (): String { + return '' } } From 951a6c87a805c84767feacba5302f67dea701134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 17 May 2024 18:03:35 +0200 Subject: [PATCH 06/43] Sync deno --- packages/neo4j-driver-deno/lib/core/notification-filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index 3472ef464..d55cbfb6c 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -64,7 +64,7 @@ type NotificationFilterDisabledClassification = ExcludeUnknown Date: Mon, 20 May 2024 09:27:58 +0200 Subject: [PATCH 07/43] Extract Notification to their own file --- packages/core/src/index.ts | 9 +- packages/core/src/internal/util.ts | 20 +- packages/core/src/notification-filter.ts | 2 +- packages/core/src/notification.ts | 280 ++++++++++++++++++ packages/core/src/result-summary.ts | 278 +---------------- packages/core/test/notification.test.ts | 173 +++++++++++ packages/core/test/result-summary.test.ts | 156 +--------- packages/neo4j-driver-deno/lib/core/index.ts | 9 +- .../lib/core/internal/util.ts | 20 +- .../lib/core/notification-filter.ts | 2 +- .../lib/core/notification.ts | 280 ++++++++++++++++++ .../lib/core/result-summary.ts | 278 +---------------- 12 files changed, 798 insertions(+), 709 deletions(-) create mode 100644 packages/core/src/notification.ts create mode 100644 packages/core/test/notification.test.ts create mode 100644 packages/neo4j-driver-deno/lib/core/notification.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b92c5a492..6cef3ad89 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -57,19 +57,20 @@ import { isPoint, Point } from './spatial-types' import ResultSummary, { queryType, ServerInfo, - Notification, - NotificationPosition, Plan, ProfiledPlan, QueryStatistics, - Stats, + Stats +} from './result-summary' +import Notification, { + NotificationPosition, NotificationSeverityLevel, NotificationClassification, NotificationCategory, notificationCategory, notificationClassification, notificationSeverityLevel -} from './result-summary' +} from './notification' import NotificationFilter, { notificationFilterDisabledCategory, NotificationFilterDisabledCategory, diff --git a/packages/core/src/internal/util.ts b/packages/core/src/internal/util.ts index 6007065e8..4151fd17d 100644 --- a/packages/core/src/internal/util.ts +++ b/packages/core/src/internal/util.ts @@ -15,7 +15,7 @@ * limitations under the License. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import Integer, { isInt } from '../integer' +import Integer, { isInt, int } from '../integer' import { NumberOrInteger } from '../graph-types' import { EncryptionLevel } from '../types' import { stringify } from '../json' @@ -259,6 +259,23 @@ function equals (a: unknown, b: unknown): boolean { return false } +/** + * Converts (Integer | bigint) to number. + * + * @private + * @param {NumberOrInteger} value The number or integer + * @returns {number} The number + */ +function toNumber (value: NumberOrInteger): number { + if (value instanceof Integer) { + return value.toNumber() + } else if (typeof value === 'bigint') { + return int(value).toNumber() + } else { + return value + } +} + export { isEmptyObjectOrNull, isObject, @@ -268,6 +285,7 @@ export { assertNumber, assertNumberOrInteger, assertValidDate, + toNumber, validateQueryAndParameters, equals, ENCRYPTION_ON, diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts index 558cd06b4..070be9573 100644 --- a/packages/core/src/notification-filter.ts +++ b/packages/core/src/notification-filter.ts @@ -18,7 +18,7 @@ import { NotificationCategory, NotificationClassification, NotificationSeverityLevel -} from './result-summary' +} from './notification' type ExcludeUnknown = Exclude type OFF = 'OFF' diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts new file mode 100644 index 000000000..19c25de9b --- /dev/null +++ b/packages/core/src/notification.ts @@ -0,0 +1,280 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 { util } from './internal' + +interface NotificationPosition { + offset?: number + line?: number + 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' | +'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | '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', + TOPOLOGY: 'TOPOLOGY', + SECURITY: 'SECURITY', + GENERIC: 'GENERIC', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationCategory) +const categories = Object.values(notificationCategory) + +type NotificationClassification = NotificationCategory +/** + * @typedef {NotificationCategory} NotificationClassification + * @experimental + */ +/** + * Constants that represents the Classification in the {@link GqlStatusObject} + * @type {notificationCategory} + * @experimental + */ +const notificationClassification = notificationCategory + +/** + * Class for Cypher notifications + * @access public + */ +class Notification { + code: string + title: string + description: string + severity: string + position: NotificationPosition | {} + severityLevel: NotificationSeverityLevel + category: NotificationCategory + rawSeverityLevel: string + rawCategory?: string + + /** + * Create a Notification instance + * @constructor + * @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 { + if (pos == null) { + return {} + } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return { + offset: util.toNumber(pos.offset!), + line: util.toNumber(pos.line!), + column: util.toNumber(pos.column!) + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + } +} + +/** + * Representation for GqlStatusObject found when executing a query. + *

+ * This object represents a status of query execution. + * This status is a superset of {@link Notification}. + * + * @experimental + */ +class GqlStatusObject { + /** + * The GQLSTATUS + */ + getGqlStatus (): String { + return '' + } + + /** + * Retrieve the severity from the diagnostic record. + */ + getSeverity (): NotificationSeverityLevel { + return notificationSeverityLevel.UNKNOWN + } + + /** + * Retrieve the severity from the diagnostic record as string. + */ + getRawSeverity (): String { + return '' + } + + /** + * Retrieve the classification from the diagnostic record. + */ + getClassification (): NotificationClassification { + return notificationClassification.UNKNOWN + } + + /** + * Retrieve the classification from the diagnostic record as string + */ + getRawClassification (): String { + return '' + } +} + +export default Notification + +export { + notificationSeverityLevel, + notificationCategory, + notificationClassification, + Notification, + GqlStatusObject +} + +export type { + NotificationPosition, + NotificationSeverityLevel, + NotificationCategory, + NotificationClassification +} diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 4114f26b9..00a452075 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -15,8 +15,10 @@ * limitations under the License. */ -import Integer, { int } from './integer' +import Integer from './integer' import { NumberOrInteger } from './graph-types' +import { util } from './internal' +import Notification, { GqlStatusObject } from './notification' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -366,9 +368,9 @@ class QueryStatistics { // To camelCase const camelCaseIndex = index.replace(/(-\w)/g, m => m[1].toUpperCase()) if (camelCaseIndex in this._stats) { - this._stats[camelCaseIndex] = intValue(statistics[index]) + this._stats[camelCaseIndex] = util.toNumber(statistics[index]) } else if (camelCaseIndex === 'systemUpdates') { - this._systemUpdates = intValue(statistics[index]) + this._systemUpdates = util.toNumber(statistics[index]) } else if (camelCaseIndex === 'containsSystemUpdates') { this._containsSystemUpdates = statistics[index] } else if (camelCaseIndex === 'containsUpdates') { @@ -419,252 +421,6 @@ class QueryStatistics { } } -interface NotificationPosition { - offset?: number - line?: number - 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' | -'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' -/** - * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | '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', - TOPOLOGY: 'TOPOLOGY', - SECURITY: 'SECURITY', - GENERIC: 'GENERIC', - UNKNOWN: 'UNKNOWN' -} - -Object.freeze(notificationCategory) -const categories = Object.values(notificationCategory) - -type NotificationClassification = NotificationCategory -/** - * @typedef {NotificationCategory} NotificationClassification - * @experimental - */ -/** - * Constants that represents the Classification in the {@link GqlStatusObject} - * @type {notificationCategory} - * @experimental - */ -const notificationClassification = notificationCategory - -/** - * Class for Cypher notifications - * @access public - */ -class Notification { - code: string - title: string - description: string - severity: string - position: NotificationPosition | {} - severityLevel: NotificationSeverityLevel - category: NotificationCategory - rawSeverityLevel: string - rawCategory?: string - - /** - * Create a Notification instance - * @constructor - * @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 { - if (pos == null) { - return {} - } - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - return { - offset: intValue(pos.offset!), - line: intValue(pos.line!), - column: intValue(pos.column!) - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } -} - -/** - * Representation for GqlStatusObject found when executing a query. - *

- * This object represents a status of query execution. - * This status is a superset of {@link Notification}. - * - * @experimental - */ -class GqlStatusObject { - /** - * The GQLSTATUS - */ - getGqlStatus (): String { - return '' - } - - /** - * Retrieve the severity from the diagnostic record. - */ - getSeverity (): NotificationSeverityLevel { - return notificationSeverityLevel.UNKNOWN - } - - /** - * Retrieve the severity from the diagnostic record as string. - */ - getRawSeverity (): String { - return '' - } - - /** - * Retrieve the classification from the diagnostic record. - */ - getClassification (): NotificationClassification { - return notificationClassification.UNKNOWN - } - - /** - * Retrieve the classification from the diagnostic record as string - */ - getRawClassification (): String { - return '' - } -} - /** * Class for exposing server info from a result. * @access public @@ -707,16 +463,6 @@ class ServerInfo { } } -function intValue (value: NumberOrInteger): number { - if (value instanceof Integer) { - return value.toNumber() - } else if (typeof value === 'bigint') { - return int(value).toNumber() - } else { - return value - } -} - function valueOrDefault ( key: string, values: { [key: string]: NumberOrInteger } | false, @@ -724,7 +470,7 @@ function valueOrDefault ( ): number { if (values !== false && key in values) { const value = values[key] - return intValue(value) + return util.toNumber(value) } else { return defaultValue } @@ -744,20 +490,10 @@ const queryType = { export { queryType, ServerInfo, - Notification, Plan, ProfiledPlan, QueryStatistics, - Stats, - notificationSeverityLevel, - notificationCategory, - notificationClassification -} -export type { - NotificationPosition, - NotificationSeverityLevel, - NotificationCategory, - NotificationClassification + Stats } export default ResultSummary diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts new file mode 100644 index 000000000..9717fcf04 --- /dev/null +++ b/packages/core/test/notification.test.ts @@ -0,0 +1,173 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 { + Notification, + NotificationSeverityLevel, + NotificationCategory, + notificationSeverityLevel, + notificationCategory, + notificationClassification +} from '../src/notification' + +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) + }) +}) + +describe('notificationClassification', () => { + it('should have keys equals to values', () => { + for (const [key, value] of Object.entries(notificationClassification)) { + expect(key).toEqual(value) + } + }) + + it('should values be assignable to NotificationClassification', () => { + for (const [, value] of Object.entries(notificationClassification)) { + const assignableValue: NotificationCategory = value + expect(assignableValue).toBeDefined() + } + }) + + it.each(getValidCategories())('should have %s as key', (category) => { + const keys = Object.keys(notificationClassification) + expect(keys.includes(category)).toBe(true) + }) + + it('should be notificationCategory', () => { + expect(notificationClassification).toBe(notificationCategory) + }) +}) + +function getValidSeverityLevels (): NotificationSeverityLevel[] { + return [ + 'WARNING', + 'INFORMATION', + 'UNKNOWN' + ] +} + +function getValidCategories (): NotificationCategory[] { + return [ + 'HINT', + 'UNRECOGNIZED', + 'UNSUPPORTED', + 'PERFORMANCE', + 'TOPOLOGY', + 'SECURITY', + 'DEPRECATION', + 'GENERIC', + 'UNKNOWN' + ] +} diff --git a/packages/core/test/result-summary.test.ts b/packages/core/test/result-summary.test.ts index ef817a575..69c28595f 100644 --- a/packages/core/test/result-summary.test.ts +++ b/packages/core/test/result-summary.test.ts @@ -18,15 +18,9 @@ import { int } from '../src' import { ServerInfo, - Notification, - NotificationSeverityLevel, - NotificationCategory, - notificationSeverityLevel, - notificationCategory, ProfiledPlan, QueryStatistics, - Stats, - notificationClassification + Stats } from '../src/result-summary' import fc from 'fast-check' @@ -61,132 +55,6 @@ 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) - }) -}) - -describe('notificationClassification', () => { - it('should have keys equals to values', () => { - for (const [key, value] of Object.entries(notificationClassification)) { - expect(key).toEqual(value) - } - }) - - it('should values be assignable to NotificationClassification', () => { - for (const [, value] of Object.entries(notificationClassification)) { - const assignableValue: NotificationCategory = value - expect(assignableValue).toBeDefined() - } - }) - - it.each(getValidCategories())('should have %s as key', (category) => { - const keys = Object.keys(notificationClassification) - expect(keys.includes(category)).toBe(true) - }) - - it('should be notificationCategory', () => { - expect(notificationClassification).toBe(notificationCategory) - }) -}) - describe('ProfilePlan', () => { describe.each([ 'dbHits', @@ -383,25 +251,3 @@ describe('QueryStatistics', () => { }) }) }) - -function getValidSeverityLevels (): NotificationSeverityLevel[] { - return [ - 'WARNING', - 'INFORMATION', - 'UNKNOWN' - ] -} - -function getValidCategories (): NotificationCategory[] { - return [ - 'HINT', - 'UNRECOGNIZED', - 'UNSUPPORTED', - 'PERFORMANCE', - 'TOPOLOGY', - 'SECURITY', - 'DEPRECATION', - 'GENERIC', - 'UNKNOWN' - ] -} diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 74dedc154..d002d9e9c 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -57,19 +57,20 @@ import { isPoint, Point } from './spatial-types.ts' import ResultSummary, { queryType, ServerInfo, - Notification, - NotificationPosition, Plan, ProfiledPlan, QueryStatistics, - Stats, + Stats +} from './result-summary.ts' +import Notification, { + NotificationPosition, NotificationSeverityLevel, NotificationClassification, NotificationCategory, notificationCategory, notificationClassification, notificationSeverityLevel -} from './result-summary.ts' +} from './notification.ts' import NotificationFilter, { notificationFilterDisabledCategory, NotificationFilterDisabledCategory, diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index 37c30e68c..ef007304f 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -15,7 +15,7 @@ * limitations under the License. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import Integer, { isInt } from '../integer.ts' +import Integer, { isInt, int } from '../integer.ts' import { NumberOrInteger } from '../graph-types.ts' import { EncryptionLevel } from '../types.ts' import { stringify } from '../json.ts' @@ -259,6 +259,23 @@ function equals (a: unknown, b: unknown): boolean { return false } +/** + * Converts (Integer | bigint) to number. + * + * @private + * @param {NumberOrInteger} value The number or integer + * @returns {number} The number + */ +function toNumber (value: NumberOrInteger): number { + if (value instanceof Integer) { + return value.toNumber() + } else if (typeof value === 'bigint') { + return int(value).toNumber() + } else { + return value + } +} + export { isEmptyObjectOrNull, isObject, @@ -268,6 +285,7 @@ export { assertNumber, assertNumberOrInteger, assertValidDate, + toNumber, validateQueryAndParameters, equals, ENCRYPTION_ON, diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index d55cbfb6c..ed0ddf501 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -18,7 +18,7 @@ import { NotificationCategory, NotificationClassification, NotificationSeverityLevel -} from './result-summary.ts' +} from './notification.ts' type ExcludeUnknown = Exclude type OFF = 'OFF' diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts new file mode 100644 index 000000000..592b5f490 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -0,0 +1,280 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 { util } from './internal/index.ts' + +interface NotificationPosition { + offset?: number + line?: number + 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' | + 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +/** + * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | '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', + TOPOLOGY: 'TOPOLOGY', + SECURITY: 'SECURITY', + GENERIC: 'GENERIC', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(notificationCategory) +const categories = Object.values(notificationCategory) + +type NotificationClassification = NotificationCategory +/** + * @typedef {NotificationCategory} NotificationClassification + * @experimental + */ +/** + * Constants that represents the Classification in the {@link GqlStatusObject} + * @type {notificationCategory} + * @experimental + */ +const notificationClassification = notificationCategory + +/** + * Class for Cypher notifications + * @access public + */ +class Notification { + code: string + title: string + description: string + severity: string + position: NotificationPosition | {} + severityLevel: NotificationSeverityLevel + category: NotificationCategory + rawSeverityLevel: string + rawCategory?: string + + /** + * Create a Notification instance + * @constructor + * @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 { + if (pos == null) { + return {} + } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return { + offset: util.toNumber(pos.offset!), + line: util.toNumber(pos.line!), + column: util.toNumber(pos.column!) + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + } +} + +/** + * Representation for GqlStatusObject found when executing a query. + *

+ * This object represents a status of query execution. + * This status is a superset of {@link Notification}. + * + * @experimental + */ +class GqlStatusObject { + /** + * The GQLSTATUS + */ + getGqlStatus(): String { + return '' + } + + /** + * Retrieve the severity from the diagnostic record. + */ + getSeverity(): NotificationSeverityLevel { + return notificationSeverityLevel.UNKNOWN + } + + /** + * Retrieve the severity from the diagnostic record as string. + */ + getRawSeverity(): String { + return '' + } + + /** + * Retrieve the classification from the diagnostic record. + */ + getClassification(): NotificationClassification { + return notificationClassification.UNKNOWN + } + + /** + * Retrieve the classification from the diagnostic record as string + */ + getRawClassification(): String { + return '' + } +} + +export default Notification + +export { + notificationSeverityLevel, + notificationCategory, + notificationClassification, + Notification, + GqlStatusObject +} + +export type { + NotificationPosition, + NotificationSeverityLevel, + NotificationCategory, + NotificationClassification +} diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index f729d6554..d721dd407 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -15,8 +15,10 @@ * limitations under the License. */ -import Integer, { int } from './integer.ts' +import Integer from './integer.ts' import { NumberOrInteger } from './graph-types.ts' +import { util } from './internal/index.ts' +import Notification, { GqlStatusObject } from './notification.ts' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -366,9 +368,9 @@ class QueryStatistics { // To camelCase const camelCaseIndex = index.replace(/(-\w)/g, m => m[1].toUpperCase()) if (camelCaseIndex in this._stats) { - this._stats[camelCaseIndex] = intValue(statistics[index]) + this._stats[camelCaseIndex] = util.toNumber(statistics[index]) } else if (camelCaseIndex === 'systemUpdates') { - this._systemUpdates = intValue(statistics[index]) + this._systemUpdates = util.toNumber(statistics[index]) } else if (camelCaseIndex === 'containsSystemUpdates') { this._containsSystemUpdates = statistics[index] } else if (camelCaseIndex === 'containsUpdates') { @@ -419,252 +421,6 @@ class QueryStatistics { } } -interface NotificationPosition { - offset?: number - line?: number - 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' | -'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' -/** - * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | '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', - TOPOLOGY: 'TOPOLOGY', - SECURITY: 'SECURITY', - GENERIC: 'GENERIC', - UNKNOWN: 'UNKNOWN' -} - -Object.freeze(notificationCategory) -const categories = Object.values(notificationCategory) - -type NotificationClassification = NotificationCategory -/** - * @typedef {NotificationCategory} NotificationClassification - * @experimental - */ -/** - * Constants that represents the Classification in the {@link GqlStatusObject} - * @type {notificationCategory} - * @experimental - */ -const notificationClassification = notificationCategory - -/** - * Class for Cypher notifications - * @access public - */ -class Notification { - code: string - title: string - description: string - severity: string - position: NotificationPosition | {} - severityLevel: NotificationSeverityLevel - category: NotificationCategory - rawSeverityLevel: string - rawCategory?: string - - /** - * Create a Notification instance - * @constructor - * @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 { - if (pos == null) { - return {} - } - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - return { - offset: intValue(pos.offset!), - line: intValue(pos.line!), - column: intValue(pos.column!) - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } -} - -/** - * Representation for GqlStatusObject found when executing a query. - *

- * This object represents a status of query execution. - * This status is a superset of {@link Notification}. - * - * @experimental - */ -class GqlStatusObject { - /** - * The GQLSTATUS - */ - getGqlStatus (): String { - return '' - } - - /** - * Retrieve the severity from the diagnostic record. - */ - getSeverity (): NotificationSeverityLevel { - return notificationSeverityLevel.UNKNOWN - } - - /** - * Retrieve the severity from the diagnostic record as string. - */ - getRawSeverity (): String { - return '' - } - - /** - * Retrieve the classification from the diagnostic record. - */ - getClassification (): NotificationClassification { - return notificationClassification.UNKNOWN - } - - /** - * Retrieve the classification from the diagnostic record as string - */ - getRawClassification (): String { - return '' - } -} - /** * Class for exposing server info from a result. * @access public @@ -707,16 +463,6 @@ class ServerInfo { } } -function intValue (value: NumberOrInteger): number { - if (value instanceof Integer) { - return value.toNumber() - } else if (typeof value === 'bigint') { - return int(value).toNumber() - } else { - return value - } -} - function valueOrDefault ( key: string, values: { [key: string]: NumberOrInteger } | false, @@ -724,7 +470,7 @@ function valueOrDefault ( ): number { if (values !== false && key in values) { const value = values[key] - return intValue(value) + return util.toNumber(value) } else { return defaultValue } @@ -744,20 +490,10 @@ const queryType = { export { queryType, ServerInfo, - Notification, Plan, ProfiledPlan, QueryStatistics, - Stats, - notificationSeverityLevel, - notificationCategory, - notificationClassification -} -export type { - NotificationPosition, - NotificationSeverityLevel, - NotificationCategory, - NotificationClassification + Stats } export default ResultSummary From b1dc8d67713074d8d200c45bbab1ce1f1a20a872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 12:18:52 +0200 Subject: [PATCH 08/43] GqlStatusObject construction using native gql objects --- packages/core/src/notification.ts | 200 ++++++-- packages/core/test/notification.test.ts | 212 +++++++++ .../lib/core/internal/util.ts | 4 +- .../lib/core/notification.ts | 448 +++++++++++------- 4 files changed, 648 insertions(+), 216 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 19c25de9b..c731ddc8e 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import * as json from './json' import { util } from './internal' interface NotificationPosition { @@ -128,7 +129,7 @@ class Notification { * @type {NotificationPosition} * @public */ - this.position = Notification._constructPosition(notification.position) + this.position = _constructPosition(notification.position) /** * The severity level @@ -154,9 +155,7 @@ class Notification { * } * } */ - this.severityLevel = severityLevels.includes(notification.severity) - ? notification.severity - : notificationSeverityLevel.UNKNOWN + this.severityLevel = _asEnumerableSeverity(this.severity) /** * The severity level returned by the server without any validation. @@ -190,9 +189,7 @@ class Notification { * } * } */ - this.category = categories.includes(notification.category) - ? notification.category - : notificationCategory.UNKNOWN + this.category = _asEnumerableClassification(notification.category) /** * The category returned by the server without any validation. @@ -202,19 +199,17 @@ class Notification { */ this.rawCategory = notification.category } +} - static _constructPosition (pos: NotificationPosition): NotificationPosition { - if (pos == null) { - return {} - } - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - return { - offset: util.toNumber(pos.offset!), - line: util.toNumber(pos.line!), - column: util.toNumber(pos.column!) - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } +interface DiagnosticRecord { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: string + _position?: object + _status_parameters?: object + [key: string]: unknown } /** @@ -224,42 +219,156 @@ class Notification { * This status is a superset of {@link Notification}. * * @experimental + * @public */ class GqlStatusObject { - /** - * The GQLSTATUS - */ - getGqlStatus (): String { - return '' - } + public readonly gqlStatus: string + public readonly statusDescription: string + public readonly diagnosticRecord: DiagnosticRecord + public readonly position: NotificationPosition + public readonly severity: NotificationSeverityLevel + public readonly rawSeverity?: string + public readonly classification: NotificationClassification + public readonly rawClassification?: string - /** - * Retrieve the severity from the diagnostic record. - */ - getSeverity (): NotificationSeverityLevel { - return notificationSeverityLevel.UNKNOWN - } + constructor (rawGqlStatusObject: any) { + /** + * The GQLSTATUS + * + * @type {string} + * @public + */ + this.gqlStatus = rawGqlStatusObject.gql_status - /** - * Retrieve the severity from the diagnostic record as string. - */ - getRawSeverity (): String { - return '' - } + /** + * The GQLSTATUS description + * + * @type {string} + * @public + */ + this.statusDescription = rawGqlStatusObject.status_description - /** - * Retrieve the classification from the diagnostic record. + /** + * The diagnostic record as it is. + * + * @type {object} + * @public */ - getClassification (): NotificationClassification { - return notificationClassification.UNKNOWN + this.diagnosticRecord = rawGqlStatusObject.diagnostic_record ?? {} + + /** + * The position which the notification had occur. + * + * @type {NotificationPosition} + * @public + */ + this.position = _constructPosition(this.diagnosticRecord._position) + + /** + * The severity + * + * @type {NotificationSeverityLevel} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const gqlStatusObject of summary.gqlStatusObjects) { + * switch(gqlStatusObject.severity) { + * case neo4j.notificationSeverityLevel.INFORMATION: // or simply 'INFORMATION' + * console.info(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationSeverityLevel.WARNING: // or simply 'WARNING' + * console.warn(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationSeverityLevel.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at gqlStatusObject.rawSeverity + * console.log(gqlStatusObject.statusDescription) + * break + * } + * } + */ + this.severity = _asEnumerableSeverity(this.diagnosticRecord._severity) + + /** + * The severity returned in the diagnostic record from the server without any validation. + * + * @type {string | undefined} + * @public + */ + this.rawSeverity = this.diagnosticRecord._severity + + /** + * The classification + * + * @type {NotificationClassification} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const gqlStatusObject of summary.gqlStatusObjects) { + * switch(gqlStatusObject.classification) { + * case neo4j.notificationClassification.QUERY: // or simply 'QUERY' + * console.info(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationClassification.PERFORMANCE: // or simply 'PERFORMANCE' + * console.warn(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationClassification.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawCategory + * console.log(gqlStatusObject.statusDescription) + * break + * } + * } + */ + this.classification = _asEnumerableClassification(this.diagnosticRecord._classification) + + /** + * The category returned by the server without any validation. + * + * @type {string|undefined} + * @public + */ + this.rawClassification = this.diagnosticRecord._classification + Object.freeze(this) } /** - * Retrieve the classification from the diagnostic record as string + * The json string representation of the diagnostic record. + * The goal of this method is provide a serialized object for human inspection. + * + * @type {string} + * @public */ - getRawClassification (): String { - return '' + public get diagnosticRecordAsJsonString (): string { + return json.stringify(this.diagnosticRecord) + } +} + +function _constructPosition (pos: any): NotificationPosition { + if (pos == null) { + return {} } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return { + offset: util.toNumber(pos.offset!), + line: util.toNumber(pos.line!), + column: util.toNumber(pos.column!) + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ +} + +function _asEnumerableSeverity (severity: any): NotificationSeverityLevel { + return severityLevels.includes(severity) + ? severity + : notificationSeverityLevel.UNKNOWN +} + +function _asEnumerableClassification (classification: any): NotificationClassification { + return categories.includes(classification) + ? classification + : notificationClassification.UNKNOWN } export default Notification @@ -276,5 +385,6 @@ export type { NotificationPosition, NotificationSeverityLevel, NotificationCategory, - NotificationClassification + NotificationClassification, + DiagnosticRecord } diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 9717fcf04..36357400e 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -15,8 +15,10 @@ * limitations under the License. */ +import * as json from '../src/json' import { Notification, + GqlStatusObject, NotificationSeverityLevel, NotificationCategory, notificationSeverityLevel, @@ -86,6 +88,216 @@ describe('Notification', () => { }) }) +describe('GqlStatusObject', () => { + describe('constructor', () => { + it('should fill gqlStatus with raw.gql_status', () => { + const gqlStatus = '00001' + const rawGqlStatusObject = { + gql_status: gqlStatus + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.gqlStatus).toBe(gqlStatus) + }) + + it('should fill statusDescription with raw.status_description', () => { + const statusDescription = 'some gql standard status description' + const rawGqlStatusObject = { + status_description: statusDescription + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.statusDescription).toBe(statusDescription) + }) + + it('should fill diagnosticRecord with raw.diagnostic_record', () => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: '', + _classification: '', + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.diagnosticRecord).toBe(diagnosticRecord) + }) + + it('should fill position with values came from raw.diagnostic_record', () => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: '', + _classification: '', + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.position).toEqual(diagnosticRecord._position) + }) + + it.each(getValidSeverityLevels())('should fill severity with values came from raw.diagnostic_record (%s)', (severity) => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: severity, + _classification: '', + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.severity).toEqual(severity) + expect(gqlStatusObject.rawSeverity).toEqual(severity) + }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should fill severity UNKNOWN if the raw.diagnostic_record._severity equals to %s', severity => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: severity, + _classification: '', + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.severity).toEqual(notificationSeverityLevel.UNKNOWN) + expect(gqlStatusObject.rawSeverity).toEqual(severity) + }) + + it.each(getValidCategories())('should fill classification with values came from raw.diagnostic_record (%s)', (classification) => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: '', + _classification: classification, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.classification).toEqual(classification) + expect(gqlStatusObject.rawClassification).toEqual(classification) + }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should fill classification UNKNOWN if the raw.diagnostic_record._classification equals to %s', classification => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: '', + _classification: classification, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.classification).toEqual(notificationClassification.UNKNOWN) + expect(gqlStatusObject.rawClassification).toEqual(classification) + }) + }) + + describe('diagnosticRecordAsJsonString()', () => { + it('should stringify diagnosticRecord', () => { + const diagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: '', + _classification: '', + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + const rawGqlStatusObject = { + diagnostic_record: diagnosticRecord + } + + const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) + + expect(gqlStatusObject.diagnosticRecordAsJsonString).toBe(json.stringify(diagnosticRecord)) + }) + }) +}) + describe('notificationSeverityLevel', () => { it('should have keys equals to values', () => { for (const [key, value] of Object.entries(notificationSeverityLevel)) { diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index ef007304f..be860bf08 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -261,10 +261,10 @@ function equals (a: unknown, b: unknown): boolean { /** * Converts (Integer | bigint) to number. - * + * * @private * @param {NumberOrInteger} value The number or integer - * @returns {number} The number + * @returns {number} The number */ function toNumber (value: NumberOrInteger): number { if (value instanceof Integer) { diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 592b5f490..ed1a0856a 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -14,12 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import * as json from './json.ts' import { util } from './internal/index.ts' interface NotificationPosition { - offset?: number - line?: number - column?: number + offset?: number + line?: number + column?: number } type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' @@ -30,16 +31,16 @@ type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' * Constants that represents the Severity level in the {@link Notification} */ const notificationSeverityLevel: { [key in NotificationSeverityLevel]: key } = { - WARNING: 'WARNING', - INFORMATION: 'INFORMATION', - UNKNOWN: 'UNKNOWN' + WARNING: 'WARNING', + INFORMATION: 'INFORMATION', + UNKNOWN: 'UNKNOWN' } Object.freeze(notificationSeverityLevel) const severityLevels = Object.values(notificationSeverityLevel) type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' | 'PERFORMANCE' | - 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' /** * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' } NotificationCategory */ @@ -47,15 +48,15 @@ type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' | 'PERFORMAN * 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', - TOPOLOGY: 'TOPOLOGY', - SECURITY: 'SECURITY', - GENERIC: 'GENERIC', - UNKNOWN: 'UNKNOWN' + HINT: 'HINT', + UNRECOGNIZED: 'UNRECOGNIZED', + UNSUPPORTED: 'UNSUPPORTED', + PERFORMANCE: 'PERFORMANCE', + DEPRECATION: 'DEPRECATION', + TOPOLOGY: 'TOPOLOGY', + SECURITY: 'SECURITY', + GENERIC: 'GENERIC', + UNKNOWN: 'UNKNOWN' } Object.freeze(notificationCategory) @@ -78,143 +79,137 @@ const notificationClassification = notificationCategory * @access public */ class Notification { - code: string - title: string - description: string - severity: string - position: NotificationPosition | {} - severityLevel: NotificationSeverityLevel - category: NotificationCategory - rawSeverityLevel: string - rawCategory?: string + code: string + title: string + description: string + severity: string + position: NotificationPosition | {} + severityLevel: NotificationSeverityLevel + category: NotificationCategory + rawSeverityLevel: string + rawCategory?: string + /** + * Create a Notification instance + * @constructor + * @param {Object} notification - Object with notification data + */ + constructor (notification: any) { /** - * Create a Notification instance - * @constructor - * @param {Object} notification - Object with notification data + * The code + * @type {string} + * @public */ - 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) + 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 = _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 + * + * @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 = _asEnumerableSeverity(this.severity) - /** - * The severity level returned by the server without any validation. - * - * @type {string} - * @public - */ - this.rawSeverityLevel = notification.severity + /** + * 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 + * + * @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 = _asEnumerableClassification(notification.category) - /** - * The category returned by the server without any validation. - * - * @type {string|undefined} - * @public - */ - this.rawCategory = notification.category - } + /** + * The category returned by the server without any validation. + * + * @type {string|undefined} + * @public + */ + this.rawCategory = notification.category + } +} - static _constructPosition(pos: NotificationPosition): NotificationPosition { - if (pos == null) { - return {} - } - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - return { - offset: util.toNumber(pos.offset!), - line: util.toNumber(pos.line!), - column: util.toNumber(pos.column!) - } - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } +type DiagnosticRecord = { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: string + _position?: object + _status_parameters?: object + [key: string]: unknown } /** @@ -224,42 +219,156 @@ class Notification { * This status is a superset of {@link Notification}. * * @experimental + * @public */ class GqlStatusObject { + public readonly gqlStatus: string + public readonly statusDescription: string + public readonly diagnosticRecord: DiagnosticRecord + public readonly position: NotificationPosition + public readonly severity: NotificationSeverityLevel + public readonly rawSeverity?: string + public readonly classification: NotificationClassification + public readonly rawClassification?: string + + constructor(rawGqlStatusObject: any) { /** * The GQLSTATUS + * + * @type {string} + * @public */ - getGqlStatus(): String { - return '' - } + this.gqlStatus = rawGqlStatusObject.gql_status /** - * Retrieve the severity from the diagnostic record. + * The GQLSTATUS description + * + * @type {string} + * @public */ - getSeverity(): NotificationSeverityLevel { - return notificationSeverityLevel.UNKNOWN - } + this.statusDescription = rawGqlStatusObject.status_description /** - * Retrieve the severity from the diagnostic record as string. + * The diagnostic record as it is. + * + * @type {object} + * @public */ - getRawSeverity(): String { - return '' - } + this.diagnosticRecord = rawGqlStatusObject.diagnostic_record ?? {} /** - * Retrieve the classification from the diagnostic record. - */ - getClassification(): NotificationClassification { - return notificationClassification.UNKNOWN - } + * The position which the notification had occur. + * + * @type {NotificationPosition} + * @public + */ + this.position = _constructPosition(this.diagnosticRecord._position) /** - * Retrieve the classification from the diagnostic record as string + * The severity + * + * @type {NotificationSeverityLevel} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const gqlStatusObject of summary.gqlStatusObjects) { + * switch(gqlStatusObject.severity) { + * case neo4j.notificationSeverityLevel.INFORMATION: // or simply 'INFORMATION' + * console.info(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationSeverityLevel.WARNING: // or simply 'WARNING' + * console.warn(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationSeverityLevel.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at gqlStatusObject.rawSeverity + * console.log(gqlStatusObject.statusDescription) + * break + * } + * } */ - getRawClassification(): String { - return '' - } + this.severity = _asEnumerableSeverity(this.diagnosticRecord._severity) + + /** + * The severity returned in the diagnostic record from the server without any validation. + * + * @type {string | undefined} + * @public + */ + this.rawSeverity = this.diagnosticRecord._severity + + /** + * The classification + * + * @type {NotificationClassification} + * @public + * @example + * const { summary } = await session.run("RETURN 1") + * + * for (const gqlStatusObject of summary.gqlStatusObjects) { + * switch(gqlStatusObject.classification) { + * case neo4j.notificationClassification.QUERY: // or simply 'QUERY' + * console.info(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationClassification.PERFORMANCE: // or simply 'PERFORMANCE' + * console.warn(gqlStatusObject.statusDescription) + * break + * case neo4j.notificationClassification.UNKNOWN: // or simply 'UNKNOWN' + * default: + * // the raw info came from the server could be found at notification.rawCategory + * console.log(gqlStatusObject.statusDescription) + * break + * } + * } + */ + this.classification = _asEnumerableClassification(this.diagnosticRecord._classification) + + /** + * The category returned by the server without any validation. + * + * @type {string|undefined} + * @public + */ + this.rawClassification = this.diagnosticRecord._classification + Object.freeze(this) + } + + /** + * The json string representation of the diagnostic record. + * The goal of this method is provide a serialized object for human inspection. + * + * @type {string} + * @public + */ + public get diagnosticRecordAsJsonString (): string { + return json.stringify(this.diagnosticRecord) + } +} + +function _constructPosition (pos: any): NotificationPosition { + if (pos == null) { + return {} + } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return { + offset: util.toNumber(pos.offset!), + line: util.toNumber(pos.line!), + column: util.toNumber(pos.column!) + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ +} + +function _asEnumerableSeverity (severity: any): NotificationSeverityLevel { + return severityLevels.includes(severity) + ? severity + : notificationSeverityLevel.UNKNOWN +} + +function _asEnumerableClassification(classification: any): NotificationClassification { + return categories.includes(classification) + ? classification + : notificationClassification.UNKNOWN } export default Notification @@ -273,8 +382,9 @@ export { } export type { - NotificationPosition, - NotificationSeverityLevel, - NotificationCategory, - NotificationClassification + NotificationPosition, + NotificationSeverityLevel, + NotificationCategory, + NotificationClassification, + DiagnosticRecord } From 3fd4e0c446fcf5133599ab29941074b69666d9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 12:21:24 +0200 Subject: [PATCH 09/43] Sync deno --- .../lib/core/notification.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index ed1a0856a..8d1e0bb9f 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as json from './json.ts' +import * as json from './json.ts' import { util } from './internal/index.ts' interface NotificationPosition { @@ -198,10 +198,10 @@ class Notification { * @public */ this.rawCategory = notification.category - } + } } -type DiagnosticRecord = { +interface DiagnosticRecord { OPERATION: string OPERATION_CODE: string CURRENT_SCHEMA: string @@ -231,10 +231,10 @@ class GqlStatusObject { public readonly classification: NotificationClassification public readonly rawClassification?: string - constructor(rawGqlStatusObject: any) { + constructor (rawGqlStatusObject: any) { /** * The GQLSTATUS - * + * * @type {string} * @public */ @@ -242,7 +242,7 @@ class GqlStatusObject { /** * The GQLSTATUS description - * + * * @type {string} * @public */ @@ -250,7 +250,7 @@ class GqlStatusObject { /** * The diagnostic record as it is. - * + * * @type {object} * @public */ @@ -337,7 +337,7 @@ class GqlStatusObject { /** * The json string representation of the diagnostic record. * The goal of this method is provide a serialized object for human inspection. - * + * * @type {string} * @public */ @@ -361,14 +361,14 @@ function _constructPosition (pos: any): NotificationPosition { function _asEnumerableSeverity (severity: any): NotificationSeverityLevel { return severityLevels.includes(severity) - ? severity - : notificationSeverityLevel.UNKNOWN + ? severity + : notificationSeverityLevel.UNKNOWN } -function _asEnumerableClassification(classification: any): NotificationClassification { +function _asEnumerableClassification (classification: any): NotificationClassification { return categories.includes(classification) - ? classification - : notificationClassification.UNKNOWN + ? classification + : notificationClassification.UNKNOWN } export default Notification From 2ee427e7efbb43216fb951951aa0ec590be8a48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 13:44:49 +0200 Subject: [PATCH 10/43] Small fix --- packages/core/test/notification.test.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 36357400e..fc5b74d5b 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -23,7 +23,8 @@ import { NotificationCategory, notificationSeverityLevel, notificationCategory, - notificationClassification + notificationClassification, + NotificationClassification } from '../src/notification' describe('Notification', () => { @@ -215,7 +216,7 @@ describe('GqlStatusObject', () => { expect(gqlStatusObject.rawSeverity).toEqual(severity) }) - it.each(getValidCategories())('should fill classification with values came from raw.diagnostic_record (%s)', (classification) => { + it.each(getValidClassifications())('should fill classification with values came from raw.diagnostic_record (%s)', (classification) => { const diagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', @@ -242,10 +243,10 @@ describe('GqlStatusObject', () => { it.each([ 'UNKNOWN', - null, undefined, - 'I_AM_NOT_OKAY', - 'information' + null, + 'DUNNO', + 'deprecation' ])('should fill classification UNKNOWN if the raw.diagnostic_record._classification equals to %s', classification => { const diagnosticRecord = { OPERATION: '', @@ -383,3 +384,17 @@ function getValidCategories (): NotificationCategory[] { 'UNKNOWN' ] } + +function getValidClassifications (): NotificationClassification[] { + return [ + 'HINT', + 'UNRECOGNIZED', + 'UNSUPPORTED', + 'PERFORMANCE', + 'TOPOLOGY', + 'SECURITY', + 'DEPRECATION', + 'GENERIC', + 'UNKNOWN' + ] +} From 4760c860ffe5ba1e638a50ded511690b66055d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 16:08:27 +0200 Subject: [PATCH 11/43] Polyfilling and exposing GqlStatusCode --- packages/core/src/index.ts | 3 + packages/core/src/notification.ts | 30 ++++- packages/core/src/result-summary.ts | 10 ++ packages/core/test/notification.test.ts | 119 +++++++++++++++++- packages/neo4j-driver-deno/lib/core/index.ts | 3 + .../lib/core/notification.ts | 30 ++++- .../lib/core/result-summary.ts | 10 ++ packages/neo4j-driver-deno/lib/mod.ts | 3 + packages/neo4j-driver-lite/src/index.ts | 3 + packages/neo4j-driver/src/index.js | 3 + .../neo4j-driver/test/types/export.test.ts | 1 + packages/neo4j-driver/types/index.d.ts | 3 + 12 files changed, 215 insertions(+), 3 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6cef3ad89..44eaf59d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -67,6 +67,7 @@ import Notification, { NotificationSeverityLevel, NotificationClassification, NotificationCategory, + GqlStatusObject, notificationCategory, notificationClassification, notificationSeverityLevel @@ -153,6 +154,7 @@ const forExport = { queryType, ServerInfo, Notification, + GqlStatusObject, Plan, ProfiledPlan, QueryStatistics, @@ -224,6 +226,7 @@ export { queryType, ServerInfo, Notification, + GqlStatusObject, Plan, ProfiledPlan, QueryStatistics, diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index c731ddc8e..79265445f 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -23,6 +23,15 @@ interface NotificationPosition { column?: number } +type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` + +const unknownGqlStatus: Record = { + WARNING: '01N42', + NO_DATA: '02N42', + INFORMATION: '03N42', + ERROR: '50N42' +} + type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' /** * @typedef {'WARNING' | 'INFORMATION' | 'UNKNOWN'} NotificationSeverityLevel @@ -346,6 +355,24 @@ class GqlStatusObject { } } +function polyfillGqlStatusObject (notification: any): GqlStatusObject { + return new GqlStatusObject({ + gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, + status_description: notification.description, + neo4j_code: notification.code, + title: notification.title, + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: notification.severity, + _classification: notification.category, + _position: notification.position + } + }) +} + function _constructPosition (pos: any): NotificationPosition { if (pos == null) { return {} @@ -378,7 +405,8 @@ export { notificationCategory, notificationClassification, Notification, - GqlStatusObject + GqlStatusObject, + polyfillGqlStatusObject } export type { diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 00a452075..a645793b4 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -111,6 +111,16 @@ class ResultSummary { this.notifications = this._buildNotifications(metadata.notifications) /** + * A list of GqlStatusObjects that arise when executing the query. + * + * The list always contains at least 1 status representing the Success, No Data or Omitted Result. + * This status will be always the first one. + * When discarding records while connected to a non-gql aware server, the driver might not be able to + * tell apart Success and No Data. + * + * All following status are notifications like warnings about problematic queries or other valuable + * information that can be presented in a client. + * * @type {Array} * @public * @experimental diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index fc5b74d5b..ff7383a27 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -24,7 +24,8 @@ import { notificationSeverityLevel, notificationCategory, notificationClassification, - NotificationClassification + NotificationClassification, + polyfillGqlStatusObject } from '../src/notification' describe('Notification', () => { @@ -297,6 +298,122 @@ describe('GqlStatusObject', () => { expect(gqlStatusObject.diagnosticRecordAsJsonString).toBe(json.stringify(diagnosticRecord)) }) }) + + describe('polyfillGqlStatusObject()', () => { + it.each(getValidCategories())('should polyfill severity WARNING', (category) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '01N42', + status_description: rawNotification.description, + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'WARNING', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) + + it.each(getValidCategories())('should polyfill severity INFORMATION', (category) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '03N42', + status_description: rawNotification.description, + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'INFORMATION', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should polyfill UNKNOWN', (severity) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity, + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'UNSUPPORTED' + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '03N42', + status_description: rawNotification.description, + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: severity, + _classification: rawNotification.category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) + }) }) describe('notificationSeverityLevel', () => { diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index d002d9e9c..45ebafe00 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -67,6 +67,7 @@ import Notification, { NotificationSeverityLevel, NotificationClassification, NotificationCategory, + GqlStatusObject, notificationCategory, notificationClassification, notificationSeverityLevel @@ -153,6 +154,7 @@ const forExport = { queryType, ServerInfo, Notification, + GqlStatusObject, Plan, ProfiledPlan, QueryStatistics, @@ -224,6 +226,7 @@ export { queryType, ServerInfo, Notification, + GqlStatusObject, Plan, ProfiledPlan, QueryStatistics, diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 8d1e0bb9f..9511dbd6f 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -23,6 +23,15 @@ interface NotificationPosition { column?: number } +type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` + +const unknownGqlStatus: Record = { + WARNING: '01N42', + NO_DATA: '02N42', + INFORMATION: '03N42', + ERROR: '50N42' +} + type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' /** * @typedef {'WARNING' | 'INFORMATION' | 'UNKNOWN'} NotificationSeverityLevel @@ -346,6 +355,24 @@ class GqlStatusObject { } } +function polyfillGqlStatusObject(notification: any): GqlStatusObject { + return new GqlStatusObject({ + gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, + status_description: notification.description, + neo4j_code: notification.code, + title: notification.title, + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: notification.severity, + _classification: notification.category, + _position: notification.position + } + }) +} + function _constructPosition (pos: any): NotificationPosition { if (pos == null) { return {} @@ -378,7 +405,8 @@ export { notificationCategory, notificationClassification, Notification, - GqlStatusObject + GqlStatusObject, + polyfillGqlStatusObject } export type { diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index d721dd407..861e6b462 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -111,6 +111,16 @@ class ResultSummary { this.notifications = this._buildNotifications(metadata.notifications) /** + * A list of GqlStatusObjects that arise when executing the query. + * + * The list always contains at least 1 status representing the Success, No Data or Omitted Result. + * This status will be always the first one. + * When discarding records while connected to a non-gql aware server, the driver might not be able to + * tell apart Success and No Data. + * + * All following status are notifications like warnings about problematic queries or other valuable + * information that can be presented in a client. + * * @type {Array} * @public * @experimental diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index d0cd9ab3c..41f775d59 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -57,6 +57,7 @@ import { Neo4jError, Node, Notification, + GqlStatusObject, notificationCategory, NotificationCategory, notificationClassification, @@ -416,6 +417,7 @@ const forExport = { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Session, Transaction, @@ -486,6 +488,7 @@ export { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Session, Transaction, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index cff40ae76..2e078aa48 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -57,6 +57,7 @@ import { Neo4jError, Node, Notification, + GqlStatusObject, notificationCategory, NotificationCategory, notificationClassification, @@ -415,6 +416,7 @@ const forExport = { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Session, Transaction, @@ -485,6 +487,7 @@ export { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Session, Transaction, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 4d75befa5..911ad9fcd 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -59,6 +59,7 @@ import { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Result, EagerResult, @@ -379,6 +380,7 @@ const forExport = { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Record, Node, @@ -448,6 +450,7 @@ export { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, Record, Node, diff --git a/packages/neo4j-driver/test/types/export.test.ts b/packages/neo4j-driver/test/types/export.test.ts index 5dd72ac53..9b2f88ac7 100644 --- a/packages/neo4j-driver/test/types/export.test.ts +++ b/packages/neo4j-driver/test/types/export.test.ts @@ -120,6 +120,7 @@ const instanceOfDriverPlan: boolean = dummy instanceof driver.Plan const instanceOfDriverProfiledPlan: boolean = dummy instanceof driver.ProfiledPlan const instanceOfDriverQueryStatistics: boolean = dummy instanceof driver.QueryStatistics const instanceOfDriverNotification: boolean = dummy instanceof driver.Notification +const instanceOfDriverGqlStatusObject: boolean = dummy instanceof driver.GqlStatusObject const instanceOfDriverServerInfo: boolean = dummy instanceof driver.ServerInfo const instanceOfDriverSession: boolean = dummy instanceof driver.Session const instanceOfDriverTransaction: boolean = dummy instanceof driver.Transaction diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index ac7efbef1..659ea07bf 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -55,6 +55,7 @@ import { RecordShape, ResultSummary, Notification, + GqlStatusObject, NotificationPosition, Plan, ProfiledPlan, @@ -257,6 +258,7 @@ declare const forExport: { ProfiledPlan: typeof ProfiledPlan QueryStatistics: typeof QueryStatistics Notification: typeof Notification + GqlStatusObject: typeof GqlStatusObject ServerInfo: typeof ServerInfo NotificationPosition: NotificationPosition Session: typeof Session @@ -336,6 +338,7 @@ export { ProfiledPlan, QueryStatistics, Notification, + GqlStatusObject, ServerInfo, NotificationPosition, Session, From 1db6c6eac3df2f5aadb510cf48b1ba46b295f978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 18:15:09 +0200 Subject: [PATCH 12/43] Create gqlStatusObjects from statuses or notifications --- .../src/bolt/stream-observers.js | 35 +- packages/core/src/notification.ts | 90 +++- packages/core/src/result-summary.ts | 6 +- packages/core/test/notification.test.ts | 390 +++++++++++++++++- .../bolt-connection/bolt/stream-observers.js | 35 +- .../lib/core/notification.ts | 108 ++++- .../lib/core/result-summary.ts | 12 +- 7 files changed, 632 insertions(+), 44 deletions(-) diff --git a/packages/bolt-connection/src/bolt/stream-observers.js b/packages/bolt-connection/src/bolt/stream-observers.js index 98bedc85a..cbf40cc2d 100644 --- a/packages/bolt-connection/src/bolt/stream-observers.js +++ b/packages/bolt-connection/src/bolt/stream-observers.js @@ -30,11 +30,11 @@ const { } = internal const { PROTOCOL_ERROR } = error class StreamObserver { - onNext (rawRecord) {} + onNext (rawRecord) { } - onError (_error) {} + onError (_error) { } - onCompleted (meta) {} + onCompleted (meta) { } } /** @@ -107,6 +107,8 @@ class ResultStreamObserver extends StreamObserver { this._setState(reactive ? _states.READY : _states.READY_STREAMING) this._setupAutoPull() this._paused = false + this._pulled = !reactive + this._haveRecordStreamed = false } /** @@ -137,6 +139,7 @@ class ResultStreamObserver extends StreamObserver { * @param {Array} rawRecord - An array with the raw record */ onNext (rawRecord) { + this._haveRecordStreamed = true const record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) if (this._observers.some(o => o.onNext)) { this._observers.forEach(o => { @@ -248,6 +251,13 @@ class ResultStreamObserver extends StreamObserver { const completionMetadata = Object.assign( this._server ? { server: this._server } : {}, this._meta, + { + stream_summary: { + have_records_streamed: this._haveRecordStreamed, + pulled: this._pulled, + has_keys: this._fieldKeys.length > 0 + } + }, meta ) @@ -392,6 +402,7 @@ class ResultStreamObserver extends StreamObserver { if (this._discard) { this._discardFunction(this._queryId, this) } else { + this._pulled = true this._moreFunction(this._queryId, this._fetchSize, this) } this._setState(_states.STREAMING) @@ -501,7 +512,7 @@ class ResetObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - json.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) @@ -602,9 +613,9 @@ class ProcedureRouteObserver extends StreamObserver { this.onError( newError( 'Illegal response from router. Received ' + - this._records.length + - ' records but expected only one.\n' + - json.stringify(this._records), + this._records.length + + ' records but expected only one.\n' + + json.stringify(this._records), PROTOCOL_ERROR ) ) @@ -637,7 +648,7 @@ class RouteObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - json.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) @@ -678,7 +689,7 @@ const _states = { name: () => { return 'READY_STREAMING' }, - pull: () => {} + pull: () => { } }, READY: { // reactive start state @@ -710,7 +721,7 @@ const _states = { name: () => { return 'STREAMING' }, - pull: () => {} + pull: () => { } }, FAILED: { onError: _error => { @@ -719,13 +730,13 @@ const _states = { name: () => { return 'FAILED' }, - pull: () => {} + pull: () => { } }, SUCCEEDED: { name: () => { return 'SUCCEEDED' }, - pull: () => {} + pull: () => { } } } diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 79265445f..f2672b249 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -355,6 +355,11 @@ class GqlStatusObject { } } +/** + * @private + * @param notification + * @returns {GqlStatusObject} + */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { return new GqlStatusObject({ gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, @@ -373,6 +378,88 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { }) } +const defaultRawDiagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } +} + +Object.freeze(defaultRawDiagnosticRecord) + +/** + * This objects are used for polyfilling the first status on the status list + * + * @private + */ +const staticGqlStatusObjects = { + SUCCESS: new GqlStatusObject({ + gql_status: '00000', + status_description: 'successful completion', + diagnostic_record: defaultRawDiagnosticRecord + }), + NO_DATA: new GqlStatusObject({ + gql_status: '02000', + status_description: 'no data', + diagnostic_record: defaultRawDiagnosticRecord + }), + NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ + gql_status: unknownGqlStatus.NO_DATA, + status_description: 'no data - unknown subcondition', + diagnostic_record: defaultRawDiagnosticRecord + }), + OMITTED_RESULT: new GqlStatusObject({ + gql_status: '00001', + status_description: 'successful completion - omitted', + diagnostic_record: defaultRawDiagnosticRecord + }) +} + +Object.freeze(staticGqlStatusObjects) + +/** + * + * @private + * @param metadata + * @returns + */ +function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ...GqlStatusObject[]] { + function getGqlStatusObjectFromStreamSummary (summary: any): GqlStatusObject { + if (summary?.have_records_streamed === true) { + return staticGqlStatusObjects.SUCCESS + } + + if (summary?.has_keys === false) { + return staticGqlStatusObjects.OMITTED_RESULT + } + + if (summary?.pulled === true) { + return staticGqlStatusObjects.NO_DATA + } + + return staticGqlStatusObjects.NO_DATA_UNKNOWN_SUBCONDITION + } + + if (metadata.statuses != null) { + return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) + } + + return [getGqlStatusObjectFromStreamSummary(metadata.stream_summary), ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? [])] +} + +/** + * + * @private + * @param pos + * @returns {NotificationPosition} + */ function _constructPosition (pos: any): NotificationPosition { if (pos == null) { return {} @@ -406,7 +493,8 @@ export { notificationClassification, Notification, GqlStatusObject, - polyfillGqlStatusObject + polyfillGqlStatusObject, + buildGqlStatusObjectFromMetadata } export type { diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index a645793b4..53ab967a7 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -18,7 +18,7 @@ import Integer from './integer' import { NumberOrInteger } from './graph-types' import { util } from './internal' -import Notification, { GqlStatusObject } from './notification' +import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata } from './notification' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -32,7 +32,7 @@ class ResultSummary { plan: Plan | false profile: ProfiledPlan | false notifications: Notification[] - gqlStatusObjects: GqlStatusObject[] + gqlStatusObjects: [GqlStatusObject, ...GqlStatusObject[]] server: ServerInfo resultConsumedAfter: T resultAvailableAfter: T @@ -125,7 +125,7 @@ class ResultSummary { * @public * @experimental */ - this.gqlStatusObjects = [] + this.gqlStatusObjects = buildGqlStatusObjectFromMetadata(metadata) /** * The basic information of the server where the result is obtained from. diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index ff7383a27..a4e25adc6 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -25,7 +25,8 @@ import { notificationCategory, notificationClassification, NotificationClassification, - polyfillGqlStatusObject + polyfillGqlStatusObject, + buildGqlStatusObjectFromMetadata } from '../src/notification' describe('Notification', () => { @@ -416,6 +417,313 @@ describe('GqlStatusObject', () => { }) }) +describe('buildGqlStatusObjectFromMetadata', () => { + it.each([ + { + statuses: [ + { + gql_status: '00000', + status_description: 'successful completion — omitted', + diagnostic_record: { + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }, + { + gql_status: '01N00', + status_description: 'warning - feature deprecated', + neo4j_code: 'Neo.Some.Warning.Code', + title: 'the title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: 'WARNING', + _classification: 'DEPRECATION' + } + }, + { + gql_status: '03N60', + status_description: 'informational - subquery variable shadowing', + neo4j_code: 'Neo.Some.Informational.Code', + title: 'the title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: 'INFORMATION', + _classification: 'HINT' + } + } + ], + notifications: [{ + severity: 'WARNING', + description: 'Some description', + code: 'Neo.Notification.Warning.Code', + title: 'The title', + category: 'DEPRECATION', + position: { + offset: 10, + line: 13, + column: 123 + } + }] + }, + { + statuses: [ + { + gql_status: '00000', + status_description: 'successful completion — omitted', + diagnostic_record: { + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + } + ], + notifications: [{ + severity: 'WARNING', + description: 'Some description', + code: 'Neo.Notification.Warning.Code', + title: 'The title', + category: 'DEPRECATION', + position: { + offset: 10, + line: 13, + column: 123 + } + }] + }, + { + statuses: [ + { + gql_status: '00000', + status_description: 'successful completion — omitted', + diagnostic_record: { + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + } + ] + }, + { + statuses: [] + }, + { + statuses: [], + notifications: [ + { + severity: 'WARNING', + description: 'Some description', + code: 'Neo.Notification.Warning.Code', + title: 'The title', + category: 'DEPRECATION', + position: { + offset: 10, + line: 13, + column: 123 + } + } + ] + } + ])('should build from statuses when available', (metadata: any) => { + const expectedStatuses = metadata.statuses.map((status: any) => new GqlStatusObject(status)) + + expect(buildGqlStatusObjectFromMetadata(metadata)).toEqual(expectedStatuses) + }) + + it.each([ + // SUCCESS + [ + getSuccessStatusObject(), { + stream_summary: { + have_records_streamed: true + } + } + ], + [ + getSuccessStatusObject(), { + stream_summary: { + have_records_streamed: true + }, + notifications: [] + } + ], + [ + getSuccessStatusObject(), { + stream_summary: { + have_records_streamed: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + // NO DATA + [ + getNoDataStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: true + } + } + ], + [ + getNoDataStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: true + }, + notifications: [] + } + ], + [ + getNoDataStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + // OMITTED RESULT + [ + getOmittedResultStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: false + } + } + ], + [ + getOmittedResultStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: false, + has_keys: false + }, + notifications: [] + } + ], + [ + getOmittedResultStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: false + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + // NO DATA - UNKNOWN SUBCONDITION + [ + getNoDataUnknownSubconditionStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: false, + has_keys: true + } + } + ], + [ + getNoDataUnknownSubconditionStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: false, + has_keys: true + }, + notifications: [] + } + ], + [ + getNoDataUnknownSubconditionStatusObject(), { + stream_summary: { + have_records_streamed: false, + pulled: false, + has_keys: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ] + ])('should build from notifications when not available', (firstGqlObject: GqlStatusObject, metadata: any) => { + const notifications = metadata.notifications != null ? metadata.notifications : [] + const expectedStatuses = [firstGqlObject, ...notifications.map(polyfillGqlStatusObject)] + + expect(buildGqlStatusObjectFromMetadata(metadata)).toEqual(expectedStatuses) + }) +}) + describe('notificationSeverityLevel', () => { it('should have keys equals to values', () => { for (const [key, value] of Object.entries(notificationSeverityLevel)) { @@ -515,3 +823,83 @@ function getValidClassifications (): NotificationClassification[] { 'UNKNOWN' ] } + +function getSuccessStatusObject (): GqlStatusObject { + return new GqlStatusObject({ + gql_status: '00000', + status_description: 'successful completion', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }) +} + +function getNoDataStatusObject (): GqlStatusObject { + return new GqlStatusObject({ + gql_status: '02000', + status_description: 'no data', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }) +} + +function getOmittedResultStatusObject (): GqlStatusObject { + return new GqlStatusObject({ + gql_status: '00001', + status_description: 'successful completion - omitted', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }) +} + +function getNoDataUnknownSubconditionStatusObject (): GqlStatusObject { + return new GqlStatusObject({ + gql_status: '02N42', + status_description: 'no data - unknown subcondition', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js index c4d3b6b79..d3b9fda00 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js @@ -30,11 +30,11 @@ const { } = internal const { PROTOCOL_ERROR } = error class StreamObserver { - onNext (rawRecord) {} + onNext (rawRecord) { } - onError (_error) {} + onError (_error) { } - onCompleted (meta) {} + onCompleted (meta) { } } /** @@ -107,6 +107,8 @@ class ResultStreamObserver extends StreamObserver { this._setState(reactive ? _states.READY : _states.READY_STREAMING) this._setupAutoPull() this._paused = false + this._pulled = !reactive + this._haveRecordStreamed = false } /** @@ -137,6 +139,7 @@ class ResultStreamObserver extends StreamObserver { * @param {Array} rawRecord - An array with the raw record */ onNext (rawRecord) { + this._haveRecordStreamed = true const record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) if (this._observers.some(o => o.onNext)) { this._observers.forEach(o => { @@ -248,6 +251,13 @@ class ResultStreamObserver extends StreamObserver { const completionMetadata = Object.assign( this._server ? { server: this._server } : {}, this._meta, + { + stream_summary: { + have_records_streamed: this._haveRecordStreamed, + pulled: this._pulled, + has_keys: this._fieldKeys.length > 0 + } + }, meta ) @@ -392,6 +402,7 @@ class ResultStreamObserver extends StreamObserver { if (this._discard) { this._discardFunction(this._queryId, this) } else { + this._pulled = true this._moreFunction(this._queryId, this._fetchSize, this) } this._setState(_states.STREAMING) @@ -501,7 +512,7 @@ class ResetObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - json.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) @@ -602,9 +613,9 @@ class ProcedureRouteObserver extends StreamObserver { this.onError( newError( 'Illegal response from router. Received ' + - this._records.length + - ' records but expected only one.\n' + - json.stringify(this._records), + this._records.length + + ' records but expected only one.\n' + + json.stringify(this._records), PROTOCOL_ERROR ) ) @@ -637,7 +648,7 @@ class RouteObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - json.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) @@ -678,7 +689,7 @@ const _states = { name: () => { return 'READY_STREAMING' }, - pull: () => {} + pull: () => { } }, READY: { // reactive start state @@ -710,7 +721,7 @@ const _states = { name: () => { return 'STREAMING' }, - pull: () => {} + pull: () => { } }, FAILED: { onError: _error => { @@ -719,13 +730,13 @@ const _states = { name: () => { return 'FAILED' }, - pull: () => {} + pull: () => { } }, SUCCEEDED: { name: () => { return 'SUCCEEDED' }, - pull: () => {} + pull: () => { } } } diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 9511dbd6f..9df308d28 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -30,7 +30,7 @@ const unknownGqlStatus: Record = { NO_DATA: '02N42', INFORMATION: '03N42', ERROR: '50N42' -} +} type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' /** @@ -49,7 +49,7 @@ Object.freeze(notificationSeverityLevel) const severityLevels = Object.values(notificationSeverityLevel) type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' | 'PERFORMANCE' | -'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' + 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' /** * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' } NotificationCategory */ @@ -103,7 +103,7 @@ class Notification { * @constructor * @param {Object} notification - Object with notification data */ - constructor (notification: any) { + constructor(notification: any) { /** * The code * @type {string} @@ -240,7 +240,7 @@ class GqlStatusObject { public readonly classification: NotificationClassification public readonly rawClassification?: string - constructor (rawGqlStatusObject: any) { + constructor(rawGqlStatusObject: any) { /** * The GQLSTATUS * @@ -350,11 +350,16 @@ class GqlStatusObject { * @type {string} * @public */ - public get diagnosticRecordAsJsonString (): string { + public get diagnosticRecordAsJsonString(): string { return json.stringify(this.diagnosticRecord) } } +/** + * @private + * @param notification + * @returns {GqlStatusObject} + */ function polyfillGqlStatusObject(notification: any): GqlStatusObject { return new GqlStatusObject({ gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, @@ -373,7 +378,91 @@ function polyfillGqlStatusObject(notification: any): GqlStatusObject { }) } -function _constructPosition (pos: any): NotificationPosition { + +const defaultRawDiagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } +} + +Object.freeze(defaultRawDiagnosticRecord) + +/** + * This objects are used for polyfilling the first status on the status list + * + * @private + */ +const staticGqlStatusObjects = { + SUCCESS: new GqlStatusObject({ + gql_status: '00000', + status_description: 'successful completion', + diagnostic_record: defaultRawDiagnosticRecord + }), + NO_DATA: new GqlStatusObject({ + gql_status: '02000', + status_description: 'no data', + diagnostic_record: defaultRawDiagnosticRecord + }), + NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ + gql_status: unknownGqlStatus.NO_DATA, + status_description: 'no data - unknown subcondition', + diagnostic_record: defaultRawDiagnosticRecord + }), + OMITTED_RESULT: new GqlStatusObject({ + gql_status: '00001', + status_description: 'successful completion - omitted', + diagnostic_record: defaultRawDiagnosticRecord + }) +} + +Object.freeze(staticGqlStatusObjects) + + +/** + * + * @private + * @param metadata + * @returns + */ +function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ...GqlStatusObject[]] { + function getGqlStatusObjectFromStreamSummary(summary: any): GqlStatusObject { + if (summary?.have_records_streamed === true) { + return staticGqlStatusObjects.SUCCESS + } + + if (summary?.has_keys === false) { + return staticGqlStatusObjects.OMITTED_RESULT + } + + if (summary?.pulled === true) { + return staticGqlStatusObjects.NO_DATA + } + + return staticGqlStatusObjects.NO_DATA_UNKNOWN_SUBCONDITION + } + + if (metadata.statuses != null) { + return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) + } + + return [getGqlStatusObjectFromStreamSummary(metadata.stream_summary), ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? [])] +} + +/** + * + * @private + * @param pos + * @returns {NotificationPosition} + */ +function _constructPosition(pos: any): NotificationPosition { if (pos == null) { return {} } @@ -386,13 +475,13 @@ function _constructPosition (pos: any): NotificationPosition { /* eslint-enable @typescript-eslint/no-non-null-assertion */ } -function _asEnumerableSeverity (severity: any): NotificationSeverityLevel { +function _asEnumerableSeverity(severity: any): NotificationSeverityLevel { return severityLevels.includes(severity) ? severity : notificationSeverityLevel.UNKNOWN } -function _asEnumerableClassification (classification: any): NotificationClassification { +function _asEnumerableClassification(classification: any): NotificationClassification { return categories.includes(classification) ? classification : notificationClassification.UNKNOWN @@ -406,7 +495,8 @@ export { notificationClassification, Notification, GqlStatusObject, - polyfillGqlStatusObject + polyfillGqlStatusObject, + buildGqlStatusObjectFromMetadata } export type { diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index 861e6b462..dcc55c3a9 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -18,7 +18,7 @@ import Integer from './integer.ts' import { NumberOrInteger } from './graph-types.ts' import { util } from './internal/index.ts' -import Notification, { GqlStatusObject } from './notification.ts' +import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata } from './notification.ts' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -32,7 +32,7 @@ class ResultSummary { plan: Plan | false profile: ProfiledPlan | false notifications: Notification[] - gqlStatusObjects: GqlStatusObject[] + gqlStatusObjects: [GqlStatusObject, ...GqlStatusObject[]] server: ServerInfo resultConsumedAfter: T resultAvailableAfter: T @@ -114,18 +114,18 @@ class ResultSummary { * A list of GqlStatusObjects that arise when executing the query. * * The list always contains at least 1 status representing the Success, No Data or Omitted Result. - * This status will be always the first one. + * This status will be always the first one. * When discarding records while connected to a non-gql aware server, the driver might not be able to * tell apart Success and No Data. * - * All following status are notifications like warnings about problematic queries or other valuable + * All following status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. - * + * * @type {Array} * @public * @experimental */ - this.gqlStatusObjects = [] + this.gqlStatusObjects = buildGqlStatusObjectFromMetadata(metadata) /** * The basic information of the server where the result is obtained from. From 9923c007d02c1aa8e30b3c7bd861c9507f13ec56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 20 May 2024 18:16:19 +0200 Subject: [PATCH 13/43] Sync deno --- .../lib/core/notification.ts | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 9df308d28..fcb998d6c 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -49,7 +49,7 @@ Object.freeze(notificationSeverityLevel) const severityLevels = Object.values(notificationSeverityLevel) type NotificationCategory = 'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' | 'PERFORMANCE' | - 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' +'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' /** * @typedef {'HINT' | 'UNRECOGNIZED' | 'UNSUPPORTED' |'PERFORMANCE' | 'TOPOLOGY' | 'SECURITY' | 'DEPRECATION' | 'GENERIC' | 'UNKNOWN' } NotificationCategory */ @@ -103,7 +103,7 @@ class Notification { * @constructor * @param {Object} notification - Object with notification data */ - constructor(notification: any) { + constructor (notification: any) { /** * The code * @type {string} @@ -240,7 +240,7 @@ class GqlStatusObject { public readonly classification: NotificationClassification public readonly rawClassification?: string - constructor(rawGqlStatusObject: any) { + constructor (rawGqlStatusObject: any) { /** * The GQLSTATUS * @@ -350,17 +350,17 @@ class GqlStatusObject { * @type {string} * @public */ - public get diagnosticRecordAsJsonString(): string { + public get diagnosticRecordAsJsonString (): string { return json.stringify(this.diagnosticRecord) } } /** * @private - * @param notification + * @param notification * @returns {GqlStatusObject} */ -function polyfillGqlStatusObject(notification: any): GqlStatusObject { +function polyfillGqlStatusObject (notification: any): GqlStatusObject { return new GqlStatusObject({ gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, status_description: notification.description, @@ -378,7 +378,6 @@ function polyfillGqlStatusObject(notification: any): GqlStatusObject { }) } - const defaultRawDiagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', @@ -397,7 +396,7 @@ Object.freeze(defaultRawDiagnosticRecord) /** * This objects are used for polyfilling the first status on the status list - * + * * @private */ const staticGqlStatusObjects = { @@ -425,15 +424,14 @@ const staticGqlStatusObjects = { Object.freeze(staticGqlStatusObjects) - /** - * + * * @private - * @param metadata - * @returns + * @param metadata + * @returns */ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ...GqlStatusObject[]] { - function getGqlStatusObjectFromStreamSummary(summary: any): GqlStatusObject { + function getGqlStatusObjectFromStreamSummary (summary: any): GqlStatusObject { if (summary?.have_records_streamed === true) { return staticGqlStatusObjects.SUCCESS } @@ -448,7 +446,7 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return staticGqlStatusObjects.NO_DATA_UNKNOWN_SUBCONDITION } - + if (metadata.statuses != null) { return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) } @@ -457,12 +455,12 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... } /** - * + * * @private - * @param pos - * @returns {NotificationPosition} + * @param pos + * @returns {NotificationPosition} */ -function _constructPosition(pos: any): NotificationPosition { +function _constructPosition (pos: any): NotificationPosition { if (pos == null) { return {} } @@ -475,13 +473,13 @@ function _constructPosition(pos: any): NotificationPosition { /* eslint-enable @typescript-eslint/no-non-null-assertion */ } -function _asEnumerableSeverity(severity: any): NotificationSeverityLevel { +function _asEnumerableSeverity (severity: any): NotificationSeverityLevel { return severityLevels.includes(severity) ? severity : notificationSeverityLevel.UNKNOWN } -function _asEnumerableClassification(classification: any): NotificationClassification { +function _asEnumerableClassification (classification: any): NotificationClassification { return categories.includes(classification) ? classification : notificationClassification.UNKNOWN From 8ff2064e8ddc57e788b000b64b542df901ff17c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 21 May 2024 10:15:05 +0200 Subject: [PATCH 14/43] Polyfill notifications --- packages/core/src/notification.ts | 23 ++ packages/core/test/notification.test.ts | 288 ++++++++++++++---- .../lib/core/notification.ts | 23 ++ 3 files changed, 276 insertions(+), 58 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index f2672b249..eaea82d0e 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -355,6 +355,28 @@ class GqlStatusObject { } } +/** + * + * @private + * @param status + * @returns {Notification|undefined} + */ +function polyfillNotification (status: any): Notification | undefined { + // Non notification status should have neo4j_code + if (status.neo4j_code == null) { + return undefined + } + + return new Notification({ + code: status.neo4j_code, + title: status.title, + description: status.status_description, + severity: status.diagnostic_record?._severity, + category: status.diagnostic_record?._classification, + position: status.diagnostic_record?._position + }) +} + /** * @private * @param notification @@ -494,6 +516,7 @@ export { Notification, GqlStatusObject, polyfillGqlStatusObject, + polyfillNotification, buildGqlStatusObjectFromMetadata } diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index a4e25adc6..5bd8bc55a 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -26,6 +26,7 @@ import { notificationClassification, NotificationClassification, polyfillGqlStatusObject, + polyfillNotification, buildGqlStatusObjectFromMetadata } from '../src/notification' @@ -89,6 +90,151 @@ describe('Notification', () => { expect(notification.rawCategory).toBe(rawCategory) }) }) + + describe('polyfillNotification()', () => { + it.each([ + getSuccessStatus(), + getNoDataStatus(), + getOmittedResultStatus(), + getNoDataUnknownSubconditionStatus() + ])('should return undefined when status is not a notification (%o)', (status: any) => { + expect(polyfillNotification(status)).toBeUndefined() + }) + + it.each(getValidCategories())('should polyfill severity WARNING', (category) => { + const status = { + neo4j_code: 'Neo.Notification.Warning.Code', + gql_status: '01N42', + status_description: 'Description', + title: 'Notification Title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'WARNING', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + } + + const gqlStatusObject = polyfillNotification(status) + + expect(gqlStatusObject).toEqual(new Notification({ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + })) + }) + + it.each(getValidCategories())('should polyfill severity INFORMATION', (category) => { + const status = { + neo4j_code: 'Neo.Notification.Warning.Code', + gql_status: '03N42', + title: 'Notification Title', + status_description: 'Description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'INFORMATION', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + } + + const gqlStatusObject = polyfillNotification(status) + + expect(gqlStatusObject).toEqual(new Notification({ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + })) + }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should polyfill UNKNOWN', (severity) => { + const status = { + neo4j_code: 'Neo.Notification.Warning.Code', + gql_status: '03N42', + title: 'Notification Title', + status_description: 'Description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: severity, + _classification: 'UNSUPPORTED', + _position: { + offset: 1, + line: 2, + column: 3 + }, + _status_parameters: {} + } + } + + const gqlStatusObject = polyfillNotification(status) + + expect(gqlStatusObject).toEqual(new Notification({ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity, + position: { + offset: 1, + line: 2, + column: 3 + }, + category: 'UNSUPPORTED' + })) + }) + + it('should polyfill when diagnostic records is not present', () => { + const status = { + neo4j_code: 'Neo.Notification.Warning.Code', + gql_status: '03N42', + title: 'Notification Title', + status_description: 'Description' + } + + const gqlStatusObject = polyfillNotification(status) + + expect(gqlStatusObject).toEqual(new Notification({ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description' + })) + }) + }) }) describe('GqlStatusObject', () => { @@ -420,52 +566,7 @@ describe('GqlStatusObject', () => { describe('buildGqlStatusObjectFromMetadata', () => { it.each([ { - statuses: [ - { - gql_status: '00000', - status_description: 'successful completion — omitted', - diagnostic_record: { - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } - } - }, - { - gql_status: '01N00', - status_description: 'warning - feature deprecated', - neo4j_code: 'Neo.Some.Warning.Code', - title: 'the title', - diagnostic_record: { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: 'WARNING', - _classification: 'DEPRECATION' - } - }, - { - gql_status: '03N60', - status_description: 'informational - subquery variable shadowing', - neo4j_code: 'Neo.Some.Informational.Code', - title: 'the title', - diagnostic_record: { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: 'INFORMATION', - _classification: 'HINT' - } - } - ], + statuses: getValidStatus(), notifications: [{ severity: 'WARNING', description: 'Some description', @@ -824,8 +925,63 @@ function getValidClassifications (): NotificationClassification[] { ] } -function getSuccessStatusObject (): GqlStatusObject { - return new GqlStatusObject({ +function getValidStatus (): any[] { + return [ + { + gql_status: '00000', + status_description: 'successful completion', + diagnostic_record: { + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + }, + ...getValidNotificationStatus() + ] +} + +function getValidNotificationStatus (): any [] { + return [ + { + gql_status: '01N00', + status_description: 'warning - feature deprecated', + neo4j_code: 'Neo.Some.Warning.Code', + title: 'the title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: 'WARNING', + _classification: 'DEPRECATION' + } + }, + { + gql_status: '03N60', + status_description: 'informational - subquery variable shadowing', + neo4j_code: 'Neo.Some.Informational.Code', + title: 'the title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: 'INFORMATION', + _classification: 'HINT' + } + } + ] +} + +function getSuccessStatus (): any { + return { gql_status: '00000', status_description: 'successful completion', diagnostic_record: { @@ -841,11 +997,15 @@ function getSuccessStatusObject (): GqlStatusObject { column: -1 } } - }) + } } -function getNoDataStatusObject (): GqlStatusObject { - return new GqlStatusObject({ +function getSuccessStatusObject (): GqlStatusObject { + return new GqlStatusObject(getSuccessStatus()) +} + +function getNoDataStatus (): any { + return { gql_status: '02000', status_description: 'no data', diagnostic_record: { @@ -861,11 +1021,15 @@ function getNoDataStatusObject (): GqlStatusObject { column: -1 } } - }) + } } -function getOmittedResultStatusObject (): GqlStatusObject { - return new GqlStatusObject({ +function getNoDataStatusObject (): GqlStatusObject { + return new GqlStatusObject(getNoDataStatus()) +} + +function getOmittedResultStatus (): any { + return { gql_status: '00001', status_description: 'successful completion - omitted', diagnostic_record: { @@ -881,11 +1045,15 @@ function getOmittedResultStatusObject (): GqlStatusObject { column: -1 } } - }) + } } -function getNoDataUnknownSubconditionStatusObject (): GqlStatusObject { - return new GqlStatusObject({ +function getOmittedResultStatusObject (): GqlStatusObject { + return new GqlStatusObject(getOmittedResultStatus()) +} + +function getNoDataUnknownSubconditionStatus (): any { + return { gql_status: '02N42', status_description: 'no data - unknown subcondition', diagnostic_record: { @@ -901,5 +1069,9 @@ function getNoDataUnknownSubconditionStatusObject (): GqlStatusObject { column: -1 } } - }) + } +} + +function getNoDataUnknownSubconditionStatusObject (): GqlStatusObject { + return new GqlStatusObject(getNoDataUnknownSubconditionStatus()) } diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index fcb998d6c..4620e817a 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -355,6 +355,28 @@ class GqlStatusObject { } } +/** + * + * @private + * @param status + * @returns {Notification|undefined} + */ +function polyfillNotification (status: any): Notification | undefined { + // Non notification status should have neo4j_code + if (status.neo4j_code == null) { + return undefined + } + + return new Notification({ + code: status.neo4j_code, + title: status.title, + description: status.status_description, + severity: status.diagnostic_record?._severity, + category: status.diagnostic_record?._classification, + position: status.diagnostic_record?._position + }) +} + /** * @private * @param notification @@ -494,6 +516,7 @@ export { Notification, GqlStatusObject, polyfillGqlStatusObject, + polyfillNotification, buildGqlStatusObjectFromMetadata } From b18b3453f7f7032472c558c02367b2ef387e7d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 21 May 2024 15:39:31 +0200 Subject: [PATCH 15/43] Adjust descriptions and polifylled messages order --- packages/core/src/notification.ts | 44 ++- packages/core/test/notification.test.ts | 353 ++++++++++++++++-- .../lib/core/notification.ts | 44 ++- 3 files changed, 394 insertions(+), 47 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index eaea82d0e..6ba9d0752 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -25,11 +25,23 @@ interface NotificationPosition { type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` -const unknownGqlStatus: Record = { - WARNING: '01N42', - NO_DATA: '02N42', - INFORMATION: '03N42', - ERROR: '50N42' +const unknownGqlStatus: Record = { + WARNING: { + gql_status: '01N42', + status_description: 'warn: warning - unknown warning' + }, + NO_DATA: { + gql_status: '02N42', + status_description: 'note: no data - unknown subcondition' + }, + INFORMATION: { + gql_status: '03N42', + status_description: 'info: informational - unknown notification' + }, + ERROR: { + gql_status: '50N42', + status_description: 'error: general processing exception - unknown error' + } } type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' @@ -383,9 +395,10 @@ function polyfillNotification (status: any): Notification | undefined { * @returns {GqlStatusObject} */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { + const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION return new GqlStatusObject({ - gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, - status_description: notification.description, + gql_status: defaultStatus.gql_status, + status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, title: notification.title, diagnostic_record: { @@ -424,22 +437,21 @@ Object.freeze(defaultRawDiagnosticRecord) const staticGqlStatusObjects = { SUCCESS: new GqlStatusObject({ gql_status: '00000', - status_description: 'successful completion', + status_description: 'note: successful completion', diagnostic_record: defaultRawDiagnosticRecord }), NO_DATA: new GqlStatusObject({ gql_status: '02000', - status_description: 'no data', + status_description: 'note: no data', diagnostic_record: defaultRawDiagnosticRecord }), NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ - gql_status: unknownGqlStatus.NO_DATA, - status_description: 'no data - unknown subcondition', + ...unknownGqlStatus.NO_DATA, diagnostic_record: defaultRawDiagnosticRecord }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', - status_description: 'successful completion - omitted', + status_description: 'note: successful completion - omitted', diagnostic_record: defaultRawDiagnosticRecord }) } @@ -473,7 +485,13 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) } - return [getGqlStatusObjectFromStreamSummary(metadata.stream_summary), ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? [])] + const polyfilledObjects = (metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[] + const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) + const position = clientGenerated.gqlStatus.startsWith('02') + ? 0 + : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 + + return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] } /** diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 5bd8bc55a..b731052b7 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -483,6 +483,41 @@ describe('GqlStatusObject', () => { })) }) + it.each(getValidCategories())('should polyfill severity WARNING and no description', (category) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '01N42', + status_description: 'warn: warning - unknown warning', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'WARNING', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) + it.each(getValidCategories())('should polyfill severity INFORMATION', (category) => { const rawNotification = { code: 'Neo.Notification.Warning.Code', @@ -519,6 +554,41 @@ describe('GqlStatusObject', () => { })) }) + it.each(getValidCategories())('should polyfill severity INFORMATIO and no description', (category) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '03N42', + status_description: 'info: informational - unknown notification', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: 'INFORMATION', + _classification: category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) + it.each([ 'UNKNOWN', null, @@ -560,6 +630,47 @@ describe('GqlStatusObject', () => { } })) }) + + it.each([ + 'UNKNOWN', + null, + undefined, + 'I_AM_NOT_OKAY', + 'information' + ])('should polyfill UNKNOWN and no description', (severity) => { + const rawNotification = { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + severity, + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'UNSUPPORTED' + } + + const gqlStatusObject = polyfillGqlStatusObject(rawNotification) + + expect(gqlStatusObject).toEqual(new GqlStatusObject({ + neo4j_code: rawNotification.code, + gql_status: '03N42', + status_description: 'info: informational - unknown notification', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _severity: severity, + _classification: rawNotification.category, + _position: { + offset: 0, + line: 0, + column: 0 + }, + _status_parameters: {} + } + })) + }) }) }) @@ -661,14 +772,14 @@ describe('buildGqlStatusObjectFromMetadata', () => { it.each([ // SUCCESS [ - getSuccessStatusObject(), { + getSuccessStatusObject(), 0, { stream_summary: { have_records_streamed: true } } ], [ - getSuccessStatusObject(), { + getSuccessStatusObject(), 0, { stream_summary: { have_records_streamed: true }, @@ -676,7 +787,26 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getSuccessStatusObject(), { + getSuccessStatusObject(), 1, { + stream_summary: { + have_records_streamed: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getSuccessStatusObject(), 2, { stream_summary: { have_records_streamed: true }, @@ -691,12 +821,94 @@ describe('buildGqlStatusObjectFromMetadata', () => { column: 0 }, category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getSuccessStatusObject(), 2, { + stream_summary: { + have_records_streamed: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getSuccessStatusObject(), 0, { + stream_summary: { + have_records_streamed: true + }, + notifications: [{ + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' }] } ], // NO DATA [ - getNoDataStatusObject(), { + getNoDataStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: true, @@ -705,7 +917,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getNoDataStatusObject(), { + getNoDataStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: true, @@ -715,7 +927,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getNoDataStatusObject(), { + getNoDataStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: true, @@ -735,9 +947,42 @@ describe('buildGqlStatusObjectFromMetadata', () => { }] } ], + [ + getNoDataStatusObject(), 0, { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: true + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, + { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], // OMITTED RESULT [ - getOmittedResultStatusObject(), { + getOmittedResultStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: true, @@ -746,7 +991,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getOmittedResultStatusObject(), { + getOmittedResultStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: false, @@ -756,7 +1001,28 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getOmittedResultStatusObject(), { + getOmittedResultStatusObject(), 1, { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: false + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getOmittedResultStatusObject(), 2, { stream_summary: { have_records_streamed: false, pulled: true, @@ -773,12 +1039,57 @@ describe('buildGqlStatusObjectFromMetadata', () => { column: 0 }, category: 'TOPOLOGY' + }, + { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getOmittedResultStatusObject(), 1, { + stream_summary: { + have_records_streamed: false, + pulled: true, + has_keys: false + }, + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, + { + code: 'Neo.Notification.Information.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' }] } ], // NO DATA - UNKNOWN SUBCONDITION [ - getNoDataUnknownSubconditionStatusObject(), { + getNoDataUnknownSubconditionStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: false, @@ -787,7 +1098,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getNoDataUnknownSubconditionStatusObject(), { + getNoDataUnknownSubconditionStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: false, @@ -797,7 +1108,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { } ], [ - getNoDataUnknownSubconditionStatusObject(), { + getNoDataUnknownSubconditionStatusObject(), 0, { stream_summary: { have_records_streamed: false, pulled: false, @@ -817,9 +1128,9 @@ describe('buildGqlStatusObjectFromMetadata', () => { }] } ] - ])('should build from notifications when not available', (firstGqlObject: GqlStatusObject, metadata: any) => { + ])('should build from notifications when not available', (filledObject: GqlStatusObject, position: number, metadata: any) => { const notifications = metadata.notifications != null ? metadata.notifications : [] - const expectedStatuses = [firstGqlObject, ...notifications.map(polyfillGqlStatusObject)] + const expectedStatuses = notifications.map(polyfillGqlStatusObject).splice(position, 0, filledObject) expect(buildGqlStatusObjectFromMetadata(metadata)).toEqual(expectedStatuses) }) @@ -929,7 +1240,7 @@ function getValidStatus (): any[] { return [ { gql_status: '00000', - status_description: 'successful completion', + status_description: 'note: successful completion', diagnostic_record: { OPERATION_CODE: '0', CURRENT_SCHEMA: '/', @@ -951,7 +1262,7 @@ function getValidNotificationStatus (): any [] { return [ { gql_status: '01N00', - status_description: 'warning - feature deprecated', + status_description: 'warn: warning - feature deprecated', neo4j_code: 'Neo.Some.Warning.Code', title: 'the title', diagnostic_record: { @@ -965,7 +1276,7 @@ function getValidNotificationStatus (): any [] { }, { gql_status: '03N60', - status_description: 'informational - subquery variable shadowing', + status_description: 'info: informational - subquery variable shadowing', neo4j_code: 'Neo.Some.Informational.Code', title: 'the title', diagnostic_record: { @@ -983,7 +1294,7 @@ function getValidNotificationStatus (): any [] { function getSuccessStatus (): any { return { gql_status: '00000', - status_description: 'successful completion', + status_description: 'note: successful completion', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -1007,7 +1318,7 @@ function getSuccessStatusObject (): GqlStatusObject { function getNoDataStatus (): any { return { gql_status: '02000', - status_description: 'no data', + status_description: 'note: no data', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -1031,7 +1342,7 @@ function getNoDataStatusObject (): GqlStatusObject { function getOmittedResultStatus (): any { return { gql_status: '00001', - status_description: 'successful completion - omitted', + status_description: 'note: successful completion - omitted', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -1055,7 +1366,7 @@ function getOmittedResultStatusObject (): GqlStatusObject { function getNoDataUnknownSubconditionStatus (): any { return { gql_status: '02N42', - status_description: 'no data - unknown subcondition', + status_description: 'note: no data - unknown subcondition', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 4620e817a..02ea44e0d 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -25,11 +25,23 @@ interface NotificationPosition { type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` -const unknownGqlStatus: Record = { - WARNING: '01N42', - NO_DATA: '02N42', - INFORMATION: '03N42', - ERROR: '50N42' +const unknownGqlStatus: Record = { + WARNING: { + gql_status: '01N42', + status_description: 'warn: warning - unknown warning' + }, + NO_DATA: { + gql_status: '02N42', + status_description: 'note: no data - unknown subcondition' + }, + INFORMATION: { + gql_status: '03N42', + status_description: 'info: informational - unknown notification' + }, + ERROR: { + gql_status: '50N42', + status_description: 'error: general processing exception - unknown error' + } } type NotificationSeverityLevel = 'WARNING' | 'INFORMATION' | 'UNKNOWN' @@ -383,9 +395,10 @@ function polyfillNotification (status: any): Notification | undefined { * @returns {GqlStatusObject} */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { + const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION return new GqlStatusObject({ - gql_status: notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION, - status_description: notification.description, + gql_status: defaultStatus.gql_status, + status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, title: notification.title, diagnostic_record: { @@ -424,22 +437,21 @@ Object.freeze(defaultRawDiagnosticRecord) const staticGqlStatusObjects = { SUCCESS: new GqlStatusObject({ gql_status: '00000', - status_description: 'successful completion', + status_description: 'note: successful completion', diagnostic_record: defaultRawDiagnosticRecord }), NO_DATA: new GqlStatusObject({ gql_status: '02000', - status_description: 'no data', + status_description: 'note: no data', diagnostic_record: defaultRawDiagnosticRecord }), NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ - gql_status: unknownGqlStatus.NO_DATA, - status_description: 'no data - unknown subcondition', + ...unknownGqlStatus.NO_DATA, diagnostic_record: defaultRawDiagnosticRecord }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', - status_description: 'successful completion - omitted', + status_description: 'note: successful completion - omitted', diagnostic_record: defaultRawDiagnosticRecord }) } @@ -473,7 +485,13 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) } - return [getGqlStatusObjectFromStreamSummary(metadata.stream_summary), ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? [])] + const polyfilledObjects = (metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[] + const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) + const position = clientGenerated.gqlStatus.startsWith('02') ? + 0 : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 + + + return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] } /** From e0e56087c7c80681e398091725321ab35193a24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 21 May 2024 16:22:24 +0200 Subject: [PATCH 16/43] Build notifications from metadata --- packages/core/src/notification.ts | 21 +- packages/core/src/result-summary.ts | 13 +- packages/core/test/notification.test.ts | 193 +++++++++++++++++- .../lib/core/notification.ts | 27 ++- .../lib/core/result-summary.ts | 13 +- 5 files changed, 238 insertions(+), 29 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 6ba9d0752..f8963baa5 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -494,6 +494,24 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] } +/** + * + * @private + * @param metadata + * @returns + */ +function buildNotificationsFromMetadata (metadata: any): Notification[] { + if (metadata.notifications != null) { + return metadata.notifications.map((n: any) => new Notification(n)) + } + + if (metadata.statuses != null) { + return metadata.statuses.map(polyfillNotification).filter((n: unknown) => n != null) + } + + return [] +} + /** * * @private @@ -535,7 +553,8 @@ export { GqlStatusObject, polyfillGqlStatusObject, polyfillNotification, - buildGqlStatusObjectFromMetadata + buildGqlStatusObjectFromMetadata, + buildNotificationsFromMetadata } export type { diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 53ab967a7..3da4a366a 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -18,7 +18,7 @@ import Integer from './integer' import { NumberOrInteger } from './graph-types' import { util } from './internal' -import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata } from './notification' +import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata, buildNotificationsFromMetadata } from './notification' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -108,7 +108,7 @@ class ResultSummary { * @type {Array} * @public */ - this.notifications = this._buildNotifications(metadata.notifications) + this.notifications = buildNotificationsFromMetadata(metadata) /** * A list of GqlStatusObjects that arise when executing the query. @@ -156,15 +156,6 @@ class ResultSummary { this.database = { name: metadata.db ?? null } } - _buildNotifications (notifications: any[]): Notification[] { - if (notifications == null) { - return [] - } - return notifications.map(function (n: any): Notification { - return new Notification(n) - }) - } - /** * Check if the result summary has a plan * @return {boolean} diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index b731052b7..89d8ebe66 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -27,7 +27,8 @@ import { NotificationClassification, polyfillGqlStatusObject, polyfillNotification, - buildGqlStatusObjectFromMetadata + buildGqlStatusObjectFromMetadata, + buildNotificationsFromMetadata } from '../src/notification' describe('Notification', () => { @@ -554,7 +555,7 @@ describe('GqlStatusObject', () => { })) }) - it.each(getValidCategories())('should polyfill severity INFORMATIO and no description', (category) => { + it.each(getValidCategories())('should polyfill severity INFORMATION and no description', (category) => { const rawNotification = { code: 'Neo.Notification.Warning.Code', title: 'Notification Title', @@ -1136,6 +1137,194 @@ describe('buildGqlStatusObjectFromMetadata', () => { }) }) +describe('buildNotificationsFromMetadata', () => { + it.each([ + [ + { + } + ], + [ + { + notifications: [] + } + ], + [ + { + notifications: [], + statuses: getValidNotificationStatus() + } + ], + [ + { + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + { + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }], + statuses: getValidNotificationStatus() + } + ], + [ + { + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + { + notifications: [{ + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Warning.Code', + title: 'Notification Title', + description: 'Description', + severity: 'WARNING', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ], + [ + getSuccessStatusObject(), 0, { + stream_summary: { + have_records_streamed: true + }, + notifications: [{ + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }, { + code: 'Neo.Notification.Info.Code', + title: 'Notification Title', + description: 'Description', + severity: 'INFORMATION', + position: { + offset: 0, + line: 0, + column: 0 + }, + category: 'TOPOLOGY' + }] + } + ] + + ])('should build from notifications when not available', (metadata: any) => { + const notifications = metadata.notifications != null ? metadata.notifications : [] + const expectedNotifications = notifications.map((notification: any) => new Notification(notification)) + + expect(buildNotificationsFromMetadata(metadata)).toEqual(expectedNotifications) + }) + + it.each([ + { + statuses: getValidStatus() + }, + { + statuses: [ + { + gql_status: '00000', + status_description: 'successful completion — omitted', + diagnostic_record: { + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + } + } + ] + }, + { + statuses: [] + } + ])('should build from statuses when notifications not available', (metadata: any) => { + const expectedNotifications = metadata.statuses.map(polyfillNotification) + .filter((notification: unknown) => notification != null) + + expect(buildNotificationsFromMetadata(metadata)).toEqual(expectedNotifications) + }) +}) + describe('notificationSeverityLevel', () => { it('should have keys equals to values', () => { for (const [key, value] of Object.entries(notificationSeverityLevel)) { diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 02ea44e0d..5ba239267 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -487,13 +487,31 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... const polyfilledObjects = (metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[] const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) - const position = clientGenerated.gqlStatus.startsWith('02') ? - 0 : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 + const position = clientGenerated.gqlStatus.startsWith('02') + ? 0 + : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 - return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] } +/** + * + * @private + * @param metadata + * @returns + */ +function buildNotificationsFromMetadata (metadata: any): Notification[] { + if (metadata.notifications != null) { + return metadata.notifications.map((n: any) => new Notification(n)) + } + + if (metadata.statuses != null) { + return metadata.statuses.map(polyfillNotification).filter((n: unknown) => n != null) + } + + return [] +} + /** * * @private @@ -535,7 +553,8 @@ export { GqlStatusObject, polyfillGqlStatusObject, polyfillNotification, - buildGqlStatusObjectFromMetadata + buildGqlStatusObjectFromMetadata, + buildNotificationsFromMetadata } export type { diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index dcc55c3a9..f08a5bccd 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -18,7 +18,7 @@ import Integer from './integer.ts' import { NumberOrInteger } from './graph-types.ts' import { util } from './internal/index.ts' -import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata } from './notification.ts' +import Notification, { GqlStatusObject, buildGqlStatusObjectFromMetadata, buildNotificationsFromMetadata } from './notification.ts' /** * A ResultSummary instance contains structured metadata for a {@link Result}. @@ -108,7 +108,7 @@ class ResultSummary { * @type {Array} * @public */ - this.notifications = this._buildNotifications(metadata.notifications) + this.notifications = buildNotificationsFromMetadata(metadata) /** * A list of GqlStatusObjects that arise when executing the query. @@ -156,15 +156,6 @@ class ResultSummary { this.database = { name: metadata.db ?? null } } - _buildNotifications (notifications: any[]): Notification[] { - if (notifications == null) { - return [] - } - return notifications.map(function (n: any): Notification { - return new Notification(n) - }) - } - /** * Check if the result summary has a plan * @return {boolean} From a342a840e893b4c5ff8bdd9cbe024f67952cdeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 22 May 2024 14:45:11 +0200 Subject: [PATCH 17/43] Tests and fixes around stream status --- .../src/bolt/stream-observers.js | 2 + .../test/bolt/stream-observer.test.js | 212 +++++++++++++++++- .../bolt-connection/bolt/stream-observers.js | 2 + 3 files changed, 213 insertions(+), 3 deletions(-) diff --git a/packages/bolt-connection/src/bolt/stream-observers.js b/packages/bolt-connection/src/bolt/stream-observers.js index cbf40cc2d..b0d88246a 100644 --- a/packages/bolt-connection/src/bolt/stream-observers.js +++ b/packages/bolt-connection/src/bolt/stream-observers.js @@ -313,7 +313,9 @@ class ResultStreamObserver extends StreamObserver { for (let i = 0; i < meta.fields.length; i++) { this._fieldLookup[meta.fields[i]] = i } + } + if (meta.fields != null) { // remove fields key from metadata object delete meta.fields } diff --git a/packages/bolt-connection/test/bolt/stream-observer.test.js b/packages/bolt-connection/test/bolt/stream-observer.test.js index caaf24b25..11c3ac3cb 100644 --- a/packages/bolt-connection/test/bolt/stream-observer.test.js +++ b/packages/bolt-connection/test/bolt/stream-observer.test.js @@ -151,7 +151,12 @@ describe('#unit ResultStreamObserver', () => { expect(receivedMetaData).toEqual({ metaDataField1: 'value1', - metaDataField2: 'value2' + metaDataField2: 'value2', + stream_summary: { + have_records_streamed: false, + has_keys: true, + pulled: true + } }) }) @@ -227,7 +232,15 @@ describe('#unit ResultStreamObserver', () => { expect(received.onNext[1].toObject()).toEqual({ A: 11, B: 22, C: 33 }) expect(received.onNext[2].toObject()).toEqual({ A: 111, B: 222, C: 333 }) expect(received.onKeys).toEqual([['A', 'B', 'C']]) - expect(received.onCompleted).toEqual([{ key: 42, has_more: false }]) + expect(received.onCompleted).toEqual([{ + key: 42, + has_more: false, + stream_summary: { + has_keys: true, + have_records_streamed: true, + pulled: true + } + }]) expect(received.onError).toEqual([]) }) @@ -256,7 +269,15 @@ describe('#unit ResultStreamObserver', () => { expect(received[1].toObject()).toEqual({ A: 1, B: 2, C: 3 }) expect(received[2].toObject()).toEqual({ A: 11, B: 22, C: 33 }) expect(received[3].toObject()).toEqual({ A: 111, B: 222, C: 333 }) - expect(received[4]).toEqual({ key: 42, has_more: false }) + expect(received[4]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: true, + have_records_streamed: true, + pulled: true + } + }) }) it('should inform all the pre-existing events of an error stream to the subscriber', () => { @@ -680,6 +701,191 @@ describe('#unit ResultStreamObserver', () => { ) }) }) + + describe('metadata.stream_summary', () => { + it('should notify stream without keys, pulled or record received', async () => { + const streamObserver = new ResultStreamObserver({ reactive: true, discardFunction: jest.fn() }) + const received = [] + const observer = { + onCompleted: metadata => received.push(metadata), + onError: error => received.push(error), + onNext: record => received.push(record), + onKeys: keys => received.push(keys) + } + + streamObserver.subscribe(observer) + + streamObserver.cancel() + streamObserver.onCompleted({ fields: [] }) + + await new Promise((resolve, reject) => { + setImmediate(() => { + try { + streamObserver.onCompleted({ key: 42, has_more: false }) + resolve() + } catch (e) { + reject(e) + } + }) + }) + + expect(received[received.length - 1]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: false, + have_records_streamed: false, + pulled: false + } + }) + }) + + it('should notify stream keys, but without pulled or record received', async () => { + const streamObserver = new ResultStreamObserver({ reactive: true, discardFunction: jest.fn() }) + const received = [] + const observer = { + onCompleted: metadata => received.push(metadata), + onError: error => received.push(error), + onNext: record => received.push(record), + onKeys: keys => received.push(keys) + } + + streamObserver.subscribe(observer) + + streamObserver.cancel() + streamObserver.onCompleted({ fields: ['A'] }) + + await new Promise((resolve, reject) => { + setImmediate(() => { + try { + streamObserver.onCompleted({ key: 42, has_more: false }) + resolve() + } catch (e) { + reject(e) + } + }) + }) + + expect(received[received.length - 1]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: true, + have_records_streamed: false, + pulled: false + } + }) + }) + + it('should notify stream pulled, but without keys or record received', async () => { + const streamObserver = new ResultStreamObserver({ reactive: false }) + const received = [] + const observer = { + onCompleted: metadata => received.push(metadata), + onError: error => received.push(error), + onNext: record => received.push(record), + onKeys: keys => received.push(keys) + } + + streamObserver.subscribe(observer) + + streamObserver.onCompleted({ fields: [] }) + + await new Promise((resolve, reject) => { + setImmediate(() => { + try { + streamObserver.onCompleted({ key: 42, has_more: false }) + resolve() + } catch (e) { + reject(e) + } + }) + }) + + expect(received[received.length - 1]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: false, + have_records_streamed: false, + pulled: true + } + }) + }) + + it('should notify stream pulled and keys received, but no record received', async () => { + const streamObserver = new ResultStreamObserver({ reactive: false }) + const received = [] + const observer = { + onCompleted: metadata => received.push(metadata), + onError: error => received.push(error), + onNext: record => received.push(record), + onKeys: keys => received.push(keys) + } + + streamObserver.subscribe(observer) + + streamObserver.onCompleted({ fields: ['A'] }) + + await new Promise((resolve, reject) => { + setImmediate(() => { + try { + streamObserver.onCompleted({ key: 42, has_more: false }) + resolve() + } catch (e) { + reject(e) + } + }) + }) + + expect(received[received.length - 1]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: true, + have_records_streamed: false, + pulled: true + } + }) + }) + + it('should notify stream pulled, keys received and record received', async () => { + const streamObserver = new ResultStreamObserver({ reactive: false }) + const received = [] + const observer = { + onCompleted: metadata => received.push(metadata), + onError: error => received.push(error), + onNext: record => received.push(record), + onKeys: keys => received.push(keys) + } + + streamObserver.subscribe(observer) + + streamObserver.onCompleted({ fields: ['A'] }) + streamObserver.onNext([1]) + + await new Promise((resolve, reject) => { + setImmediate(() => { + try { + streamObserver.onCompleted({ key: 42, has_more: false }) + resolve() + } catch (e) { + reject(e) + } + }) + }) + + expect(received[received.length - 1]).toEqual({ + key: 42, + has_more: false, + stream_summary: { + has_keys: true, + have_records_streamed: true, + pulled: true + } + }) + }) + }) }) describe('#unit RouteObserver', () => { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js index d3b9fda00..9265f7956 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js @@ -313,7 +313,9 @@ class ResultStreamObserver extends StreamObserver { for (let i = 0; i < meta.fields.length; i++) { this._fieldLookup[meta.fields[i]] = i } + } + if (meta.fields != null) { // remove fields key from metadata object delete meta.fields } From 433eb469ee8c7558b90410dc4927d88db351acf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 23 May 2024 14:31:50 +0200 Subject: [PATCH 18/43] Testkit support + bug fixing --- packages/core/src/notification.ts | 21 ++++++++++++++++--- packages/core/test/notification.test.ts | 9 ++++---- .../lib/core/notification.ts | 21 ++++++++++++++++--- .../testkit-backend/src/feature/common.js | 2 ++ .../src/skipped-tests/common.js | 2 +- .../testkit-backend/src/summary-binder.js | 15 ++++++++++++- 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index f8963baa5..5fd018bed 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -251,6 +251,7 @@ class GqlStatusObject { public readonly rawSeverity?: string public readonly classification: NotificationClassification public readonly rawClassification?: string + public readonly isNotification: boolean constructor (rawGqlStatusObject: any) { /** @@ -352,6 +353,18 @@ class GqlStatusObject { * @public */ this.rawClassification = this.diagnosticRecord._classification + + /** + * Indicates if this object represents a notification and it can be filtered using + * NotificationFilter. + * + * Only GqlStatusObject which is Notification has meaningful position, severity and + * classification. + * + * @type {boolean} + * @public + */ + this.isNotification = rawGqlStatusObject.neo4j_code != null Object.freeze(this) } @@ -451,7 +464,7 @@ const staticGqlStatusObjects = { }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', - status_description: 'note: successful completion - omitted', + status_description: 'note: successful completion - omitted result', diagnostic_record: defaultRawDiagnosticRecord }) } @@ -489,9 +502,11 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) const position = clientGenerated.gqlStatus.startsWith('02') ? 0 - : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 + : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + + polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) - return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] + return polyfilledObjects as [GqlStatusObject, ...GqlStatusObject[]] } /** diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 89d8ebe66..9dd7e77db 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -995,7 +995,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { getOmittedResultStatusObject(), 0, { stream_summary: { have_records_streamed: false, - pulled: false, + pulled: true, has_keys: false }, notifications: [] @@ -1131,7 +1131,8 @@ describe('buildGqlStatusObjectFromMetadata', () => { ] ])('should build from notifications when not available', (filledObject: GqlStatusObject, position: number, metadata: any) => { const notifications = metadata.notifications != null ? metadata.notifications : [] - const expectedStatuses = notifications.map(polyfillGqlStatusObject).splice(position, 0, filledObject) + const expectedStatuses = notifications.map(polyfillGqlStatusObject) + expectedStatuses.splice(position, 0, filledObject) expect(buildGqlStatusObjectFromMetadata(metadata)).toEqual(expectedStatuses) }) @@ -1283,7 +1284,7 @@ describe('buildNotificationsFromMetadata', () => { } ] - ])('should build from notifications when not available', (metadata: any) => { + ])('should build from notifications when available', (metadata: any) => { const notifications = metadata.notifications != null ? metadata.notifications : [] const expectedNotifications = notifications.map((notification: any) => new Notification(notification)) @@ -1531,7 +1532,7 @@ function getNoDataStatusObject (): GqlStatusObject { function getOmittedResultStatus (): any { return { gql_status: '00001', - status_description: 'note: successful completion - omitted', + status_description: 'note: successful completion - omitted result', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 5ba239267..38b30fa26 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -251,6 +251,7 @@ class GqlStatusObject { public readonly rawSeverity?: string public readonly classification: NotificationClassification public readonly rawClassification?: string + public readonly isNotification: boolean constructor (rawGqlStatusObject: any) { /** @@ -352,6 +353,18 @@ class GqlStatusObject { * @public */ this.rawClassification = this.diagnosticRecord._classification + + /** + * Indicates if this object represents a notification and it can be filtered using + * NotificationFilter. + * + * Only GqlStatusObject which is Notification has meaningful position, severity and + * classification. + * + * @type {boolean} + * @public + */ + this.isNotification = rawGqlStatusObject.neo4j_code != null Object.freeze(this) } @@ -451,7 +464,7 @@ const staticGqlStatusObjects = { }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', - status_description: 'note: successful completion - omitted', + status_description: 'note: successful completion - omitted result', diagnostic_record: defaultRawDiagnosticRecord }) } @@ -489,9 +502,11 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) const position = clientGenerated.gqlStatus.startsWith('02') ? 0 - : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + 1 + : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + + polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) - return polyfilledObjects.splice(position, 0, clientGenerated) as [GqlStatusObject, ...GqlStatusObject[]] + return polyfilledObjects as [GqlStatusObject, ...GqlStatusObject[]] } /** diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 772e1b267..fe7f7a277 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -24,6 +24,7 @@ const features = [ 'Feature:Bolt:5.2', 'Feature:Bolt:5.3', 'Feature:Bolt:5.4', + 'Feature:Bolt:5.5', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', @@ -34,6 +35,7 @@ const features = [ 'Feature:API:Driver.VerifyAuthentication', 'Feature:API:Driver.VerifyConnectivity', 'Feature:API:Session:NotificationsConfig', + 'Feature:API:Summary:GqlStatusObjects', 'Feature:API:Liveness.Check', 'Optimization:AuthPipelining', 'Optimization:EagerTransactionBegin', diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index e36d4c9f4..2f1e6f6ee 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -55,7 +55,7 @@ const skippedTests = [ ), skip( 'ResultSummary.notifications defaults to empty array instead of return null/undefined', - ifEquals('stub.summary.test_summary.TestSummary.test_no_notifications'), + ifEquals('stub.summary.test_summary.TestSummaryNotifications4x4.test_no_notifications'), ifEquals('neo4j.test_summary.TestSummary.test_no_notification_info') ), skip( diff --git a/packages/testkit-backend/src/summary-binder.js b/packages/testkit-backend/src/summary-binder.js index 62af6c1d0..6d4c5e709 100644 --- a/packages/testkit-backend/src/summary-binder.js +++ b/packages/testkit-backend/src/summary-binder.js @@ -47,6 +47,18 @@ function mapNotification (notification) { } } +function mapGqlStatusObject (binder) { + return (gqlStatusObject) => { + return { + ...gqlStatusObject, + position: Object.keys(gqlStatusObject.position).length !== 0 ? gqlStatusObject.position : null, + rawSeverity: gqlStatusObject.rawSeverity || '', + rawClassification: gqlStatusObject.rawClassification || '', + diagnosticRecord: binder.objectToCypher(gqlStatusObject.diagnosticRecord) + } + } +} + export function nativeToTestkitSummary (summary, binder) { return { ...binder.objectMemberBitIntToNumber(summary), @@ -62,6 +74,7 @@ export function nativeToTestkitSummary (summary, binder) { counters: mapCounters(summary.counters), plan: mapPlan(summary.plan), profile: mapProfile(summary.profile, false, binder), - notifications: summary.notifications.map(mapNotification) + notifications: summary.notifications.map(mapNotification), + gqlStatusObjects: summary.gqlStatusObjects.map(mapGqlStatusObject(binder)) } } From eec6885caa729adcc6f114ce65d117ad46e425f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 23 May 2024 14:32:47 +0200 Subject: [PATCH 19/43] sync deno --- packages/neo4j-driver-deno/lib/core/notification.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 38b30fa26..26018defb 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -356,11 +356,11 @@ class GqlStatusObject { /** * Indicates if this object represents a notification and it can be filtered using - * NotificationFilter. - * + * NotificationFilter. + * * Only GqlStatusObject which is Notification has meaningful position, severity and * classification. - * + * * @type {boolean} * @public */ @@ -504,7 +504,7 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... ? 0 : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') - polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) + polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) return polyfilledObjects as [GqlStatusObject, ...GqlStatusObject[]] } From e47831fba5982616e0a265c3563fa0433e321ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 23 May 2024 15:17:26 +0200 Subject: [PATCH 20/43] Ordering gqlStatus --- packages/core/src/notification.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 5fd018bed..2552c748b 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -498,15 +498,26 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) } - const polyfilledObjects = (metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[] const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) - const position = clientGenerated.gqlStatus.startsWith('02') - ? 0 - : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + const polyfilledObjects = [clientGenerated, ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[]] - polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) + return polyfilledObjects.sort((a: GqlStatusObject, b: GqlStatusObject) => calculateWeight(a) - calculateWeight(b)) as [GqlStatusObject, ...GqlStatusObject[]] +} - return polyfilledObjects as [GqlStatusObject, ...GqlStatusObject[]] +const gqlStatusWeightByClass = Object.freeze({ + '02': 0, + '01': 1, + '00': 2 +}) +/** + * GqlStatus weight + * + * @private + */ +function calculateWeight (gqlStatusObject: GqlStatusObject): number { + const gqlClass = gqlStatusObject.gqlStatus?.slice(0, 2) + // @ts-expect-error + return gqlStatusWeightByClass[gqlClass] ?? 9999 } /** From 678b12e197755f00ed750e0209ba2b1b650a348b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 23 May 2024 15:18:07 +0200 Subject: [PATCH 21/43] sync deno --- .../lib/core/notification.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 26018defb..652b7dec3 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -498,15 +498,26 @@ function buildGqlStatusObjectFromMetadata (metadata: any): [GqlStatusObject, ... return metadata.statuses.map((status: unknown) => new GqlStatusObject(status)) } - const polyfilledObjects = (metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[] const clientGenerated = getGqlStatusObjectFromStreamSummary(metadata.stream_summary) - const position = clientGenerated.gqlStatus.startsWith('02') - ? 0 - : polyfilledObjects.findIndex(v => v.severity !== 'WARNING') + const polyfilledObjects = [clientGenerated, ...(metadata.notifications?.map(polyfillGqlStatusObject) ?? []) as GqlStatusObject[]] - polyfilledObjects.splice(position !== -1 ? position : polyfilledObjects.length, 0, clientGenerated) + return polyfilledObjects.sort((a: GqlStatusObject, b: GqlStatusObject) => calculateWeight(a) - calculateWeight(b)) as [GqlStatusObject, ...GqlStatusObject[]] +} - return polyfilledObjects as [GqlStatusObject, ...GqlStatusObject[]] +const gqlStatusWeightByClass = Object.freeze({ + '02': 0, + '01': 1, + '00': 2 +}) +/** + * GqlStatus weight + * + * @private + */ +function calculateWeight (gqlStatusObject: GqlStatusObject): number { + const gqlClass = gqlStatusObject.gqlStatus?.slice(0, 2) + // @ts-expect-error + return gqlStatusWeightByClass[gqlClass] ?? 9999 } /** From 6de5a0c3169db9d8d3b2b01ec391567dc8c105d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 27 May 2024 16:58:56 +0200 Subject: [PATCH 22/43] Add default values --- .../src/bolt/bolt-protocol-v5x5.js | 33 ++++++++++++++++++- .../src/bolt/stream-observers.js | 10 ++++-- .../bolt/bolt-protocol-v5x5.js | 33 ++++++++++++++++++- .../bolt-connection/bolt/stream-observers.js | 10 ++++-- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js index 6295884de..c5d3218a8 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js @@ -27,6 +27,20 @@ const { constants: { BOLT_PROTOCOL_V5_5, FETCH_ALL } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } +}) + export default class BoltProtocol extends BoltProtocolV5x4 { get version () { return BOLT_PROTOCOL_V5_5 @@ -140,7 +154,8 @@ export default class BoltProtocol extends BoltProtocolV5x4 { beforeComplete, afterComplete, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + enrichMetadata: BoltProtocol._enrichMetadata }) const flushRun = reactive @@ -163,4 +178,20 @@ export default class BoltProtocol extends BoltProtocolV5x4 { return observer } + + /** + * + * @param {object} metadata + * @returns {object} + */ + static _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + diagnostic_record: { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } + })) + } + + return metadata + } } diff --git a/packages/bolt-connection/src/bolt/stream-observers.js b/packages/bolt-connection/src/bolt/stream-observers.js index b0d88246a..336cbb30a 100644 --- a/packages/bolt-connection/src/bolt/stream-observers.js +++ b/packages/bolt-connection/src/bolt/stream-observers.js @@ -24,6 +24,7 @@ import { internal } from 'neo4j-driver-core' import RawRoutingTable from './routing-table-raw' +import { functional } from '../lang' const { constants: { FETCH_ALL } @@ -62,6 +63,7 @@ class ResultStreamObserver extends StreamObserver { * @param {function(keys: string[]): Promise|void} param.afterKeys - * @param {function(metadata: Object): Promise|void} param.beforeComplete - * @param {function(metadata: Object): Promise|void} param.afterComplete - + * @param {function(metadata: Object): Promise|void} param.enrichMetadata - */ constructor ({ reactive = false, @@ -76,7 +78,8 @@ class ResultStreamObserver extends StreamObserver { afterComplete, server, highRecordWatermark = Number.MAX_VALUE, - lowRecordWatermark = Number.MAX_VALUE + lowRecordWatermark = Number.MAX_VALUE, + enrichMetadata } = {}) { super() @@ -96,6 +99,7 @@ class ResultStreamObserver extends StreamObserver { this._afterKeys = afterKeys this._beforeComplete = beforeComplete this._afterComplete = afterComplete + this._enrichMetadata = enrichMetadata || functional.identity this._queryId = null this._moreFunction = moreFunction @@ -248,7 +252,7 @@ class ResultStreamObserver extends StreamObserver { } _handlePullSuccess (meta) { - const completionMetadata = Object.assign( + const completionMetadata = this._enrichMetadata(Object.assign( this._server ? { server: this._server } : {}, this._meta, { @@ -259,7 +263,7 @@ class ResultStreamObserver extends StreamObserver { } }, meta - ) + )) if (![undefined, null, 'r', 'w', 'rw', 's'].includes(completionMetadata.type)) { this.onError( diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js index a02f35e1c..baeb62b59 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js @@ -27,6 +27,20 @@ const { constants: { BOLT_PROTOCOL_V5_5, FETCH_ALL } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } +}) + export default class BoltProtocol extends BoltProtocolV5x4 { get version () { return BOLT_PROTOCOL_V5_5 @@ -140,7 +154,8 @@ export default class BoltProtocol extends BoltProtocolV5x4 { beforeComplete, afterComplete, highRecordWatermark, - lowRecordWatermark + lowRecordWatermark, + enrichMetadata: BoltProtocol._enrichMetadata }) const flushRun = reactive @@ -163,4 +178,20 @@ export default class BoltProtocol extends BoltProtocolV5x4 { return observer } + + /** + * + * @param {object} metadata + * @returns {object} + */ + static _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + diagnostic_record: { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } + })) + } + + return metadata + } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js index 9265f7956..fb75d83ab 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js @@ -24,6 +24,7 @@ import { internal } from '../../core/index.ts' import RawRoutingTable from './routing-table-raw.js' +import { functional } from '../lang/index.js' const { constants: { FETCH_ALL } @@ -62,6 +63,7 @@ class ResultStreamObserver extends StreamObserver { * @param {function(keys: string[]): Promise|void} param.afterKeys - * @param {function(metadata: Object): Promise|void} param.beforeComplete - * @param {function(metadata: Object): Promise|void} param.afterComplete - + * @param {function(metadata: Object): Promise|void} param.enrichMetadata - */ constructor ({ reactive = false, @@ -76,7 +78,8 @@ class ResultStreamObserver extends StreamObserver { afterComplete, server, highRecordWatermark = Number.MAX_VALUE, - lowRecordWatermark = Number.MAX_VALUE + lowRecordWatermark = Number.MAX_VALUE, + enrichMetadata } = {}) { super() @@ -96,6 +99,7 @@ class ResultStreamObserver extends StreamObserver { this._afterKeys = afterKeys this._beforeComplete = beforeComplete this._afterComplete = afterComplete + this._enrichMetadata = enrichMetadata || functional.identity this._queryId = null this._moreFunction = moreFunction @@ -248,7 +252,7 @@ class ResultStreamObserver extends StreamObserver { } _handlePullSuccess (meta) { - const completionMetadata = Object.assign( + const completionMetadata = this._enrichMetadata(Object.assign( this._server ? { server: this._server } : {}, this._meta, { @@ -259,7 +263,7 @@ class ResultStreamObserver extends StreamObserver { } }, meta - ) + )) if (![undefined, null, 'r', 'w', 'rw', 's'].includes(completionMetadata.type)) { this.onError( From 47ca0076f38c142a615b229222adb2f3eb415975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 28 May 2024 11:58:27 +0200 Subject: [PATCH 23/43] Add test for default values --- .../src/bolt/bolt-protocol-v5x5.js | 2 +- .../test/bolt/bolt-protocol-v5x5.test.js | 442 ++++++++++++++++++ .../bolt/bolt-protocol-v5x5.js | 2 +- 3 files changed, 444 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js index c5d3218a8..01de4b08e 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js @@ -188,7 +188,7 @@ export default class BoltProtocol extends BoltProtocolV5x4 { if (Array.isArray(metadata.statuses)) { metadata.statuses = metadata.statuses.map(status => ({ ...status, - diagnostic_record: { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null })) } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js index d113ea62c..a6ccdf0af 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js @@ -1151,6 +1151,448 @@ describe('#unit BoltProtocolV5x5', () => { }) }) + describe('result metadata enrich', () => { + it('run should configure BoltProtocolV5x5._enrichMetadata as enrichMetadata', () => { + 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 BoltProtocolV5x5(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 + }) + + expect(observer._enrichMetadata).toBe(BoltProtocolV5x5._enrichMetadata) + }) + + describe('BoltProtocolV5x5._enrichMetadata', () => { + it('should handle empty metadata', () => { + const metadata = BoltProtocolV5x5._enrichMetadata({}) + + expect(metadata).toEqual({}) + }) + + it('should handle metadata with random objects', () => { + const metadata = BoltProtocolV5x5._enrichMetadata({ + a: 1133, + b: 345 + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345 + }) + }) + + it('should handle metadata not change notifications ', () => { + const metadata = BoltProtocolV5x5._enrichMetadata({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + notifications: [ + { + severity: 'WARNING', + category: 'HINT' + } + ] + }) + }) + + it.each([ + [null, null], + [undefined, undefined], + [[], []], + [statusesWithDiagnosticRecord(null, null), statusesWithDiagnosticRecord(null, null)], + [statusesWithDiagnosticRecord(undefined, undefined), statusesWithDiagnosticRecord({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }, + { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + })], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C', + _status_parameters: { d: 'E' }, + _severity: 'F', + _classification: 'G', + _position: { + offset: 1, + line: 2, + column: 3 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: '/', + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: {}, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: '', + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: '', + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: { + offset: -1, + line: -1, + column: -1 + } + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null, + _severity: null, + _classification: null, + _position: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }), + statusesWithDiagnosticRecord({ + OPERATION: undefined, + OPERATION_CODE: undefined, + CURRENT_SCHEMA: undefined, + _status_parameters: undefined, + _severity: undefined, + _classification: undefined, + _position: undefined + }) + ] + ])('should handle statuses (%o) ', (statuses, expectedStatuses) => { + const metadata = BoltProtocolV5x5._enrichMetadata({ + a: 1133, + b: 345, + statuses + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345, + statuses: expectedStatuses + }) + }) + }) + + function statusesWithDiagnosticRecord (...diagnosticRecords) { + return diagnosticRecords.map(diagnosticRecord => { + return { + gql_status: '00000', + status_description: 'note: successful completion', + diagnostic_record: diagnosticRecord + } + }) + } + }) + function newProtocol (recorder) { return new BoltProtocolV5x5(recorder, null, false, undefined, undefined, () => {}) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js index baeb62b59..31bfd0f1f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js @@ -188,7 +188,7 @@ export default class BoltProtocol extends BoltProtocolV5x4 { if (Array.isArray(metadata.statuses)) { metadata.statuses = metadata.statuses.map(status => ({ ...status, - diagnostic_record: { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null })) } From 4caeca6aca45643231fbf88421c37b2e6af13ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 30 May 2024 13:48:16 +0200 Subject: [PATCH 24/43] Implement new defaults and nullability --- .../src/bolt/bolt-protocol-v5x5.js | 10 +- .../test/bolt/bolt-protocol-v5x5.test.js | 128 ++---------------- packages/core/src/notification.ts | 33 ++--- packages/core/test/notification.test.ts | 88 ++---------- .../bolt/bolt-protocol-v5x5.js | 10 +- .../lib/core/notification.ts | 31 ++--- .../testkit-backend/src/summary-binder.js | 6 +- 7 files changed, 53 insertions(+), 253 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js index 01de4b08e..464c244a0 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x5.js @@ -30,15 +30,7 @@ const { const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) export default class BoltProtocol extends BoltProtocolV5x4 { diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js index a6ccdf0af..503de4706 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js @@ -1230,28 +1230,12 @@ describe('#unit BoltProtocolV5x5', () => { [statusesWithDiagnosticRecord(undefined, undefined), statusesWithDiagnosticRecord({ OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }, { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' })], [ statusesWithDiagnosticRecord({ @@ -1260,15 +1244,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: 'A', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) ], [ @@ -1279,15 +1255,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: 'A', OPERATION_CODE: 'B', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) ], [ @@ -1299,15 +1267,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: 'A', OPERATION_CODE: 'B', - CURRENT_SCHEMA: '/C', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/C' }) ], [ @@ -1321,14 +1281,7 @@ describe('#unit BoltProtocolV5x5', () => { OPERATION: 'A', OPERATION_CODE: 'B', CURRENT_SCHEMA: '/C', - _status_parameters: { d: 'E' }, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + _status_parameters: { d: 'E' } }) ], [ @@ -1344,13 +1297,7 @@ describe('#unit BoltProtocolV5x5', () => { OPERATION_CODE: 'B', CURRENT_SCHEMA: '/C', _status_parameters: { d: 'E' }, - _severity: 'F', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + _severity: 'F' }) ], [ @@ -1368,12 +1315,7 @@ describe('#unit BoltProtocolV5x5', () => { CURRENT_SCHEMA: '/C', _status_parameters: { d: 'E' }, _severity: 'F', - _classification: 'G', - _position: { - offset: -1, - line: -1, - column: -1 - } + _classification: 'G' }) ], [ @@ -1411,15 +1353,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: null, OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) ], [ @@ -1430,15 +1364,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: null, OPERATION_CODE: null, - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) ], [ @@ -1450,15 +1376,7 @@ describe('#unit BoltProtocolV5x5', () => { statusesWithDiagnosticRecord({ OPERATION: null, OPERATION_CODE: null, - CURRENT_SCHEMA: null, - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: null }) ], [ @@ -1472,14 +1390,7 @@ describe('#unit BoltProtocolV5x5', () => { OPERATION: null, OPERATION_CODE: null, CURRENT_SCHEMA: null, - _status_parameters: null, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + _status_parameters: null }) ], [ @@ -1495,13 +1406,7 @@ describe('#unit BoltProtocolV5x5', () => { OPERATION_CODE: null, CURRENT_SCHEMA: null, _status_parameters: null, - _severity: null, - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + _severity: null }) ], [ @@ -1519,12 +1424,7 @@ describe('#unit BoltProtocolV5x5', () => { CURRENT_SCHEMA: null, _status_parameters: null, _severity: null, - _classification: null, - _position: { - offset: -1, - line: -1, - column: -1 - } + _classification: null }) ], [ diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 2552c748b..7bc53ec57 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -246,7 +246,7 @@ class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string public readonly diagnosticRecord: DiagnosticRecord - public readonly position: NotificationPosition + public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string public readonly classification: NotificationClassification @@ -281,10 +281,10 @@ class GqlStatusObject { /** * The position which the notification had occur. * - * @type {NotificationPosition} + * @type {NotificationPosition | undefined} * @public */ - this.position = _constructPosition(this.diagnosticRecord._position) + this.position = this.diagnosticRecord._position != null ? _constructPosition(this.diagnosticRecord._position) : undefined /** * The severity @@ -415,10 +415,7 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { neo4j_code: notification.code, title: notification.title, diagnostic_record: { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, + ...rawPolyfilledDiagnosticRecord, _severity: notification.severity, _classification: notification.category, _position: notification.position @@ -426,21 +423,13 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { }) } -const defaultRawDiagnosticRecord = { +const rawPolyfilledDiagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } -Object.freeze(defaultRawDiagnosticRecord) +Object.freeze(rawPolyfilledDiagnosticRecord) /** * This objects are used for polyfilling the first status on the status list @@ -451,21 +440,21 @@ const staticGqlStatusObjects = { SUCCESS: new GqlStatusObject({ gql_status: '00000', status_description: 'note: successful completion', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), NO_DATA: new GqlStatusObject({ gql_status: '02000', status_description: 'note: no data', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ ...unknownGqlStatus.NO_DATA, - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', status_description: 'note: successful completion - omitted result', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }) } diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 9dd7e77db..ee0164ebf 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -478,8 +478,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -513,8 +512,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -549,8 +547,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -584,8 +581,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -626,8 +622,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -667,8 +662,7 @@ describe('GqlStatusObject', () => { offset: 0, line: 0, column: 0 - }, - _status_parameters: {} + } } })) }) @@ -699,15 +693,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { status_description: 'successful completion — omitted', diagnostic_record: { OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } ], @@ -731,15 +717,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { status_description: 'successful completion — omitted', diagnostic_record: { OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } ] @@ -1302,15 +1280,7 @@ describe('buildNotificationsFromMetadata', () => { status_description: 'successful completion — omitted', diagnostic_record: { OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } ] @@ -1488,15 +1458,7 @@ function getSuccessStatus (): any { diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } } @@ -1512,15 +1474,7 @@ function getNoDataStatus (): any { diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } } @@ -1536,15 +1490,7 @@ function getOmittedResultStatus (): any { diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } } @@ -1560,15 +1506,7 @@ function getNoDataUnknownSubconditionStatus (): any { diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' } } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js index 31bfd0f1f..afb04cee2 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x5.js @@ -30,15 +30,7 @@ const { const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } + CURRENT_SCHEMA: '/' }) export default class BoltProtocol extends BoltProtocolV5x4 { diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 652b7dec3..6b28f42fc 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -246,7 +246,7 @@ class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string public readonly diagnosticRecord: DiagnosticRecord - public readonly position: NotificationPosition + public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string public readonly classification: NotificationClassification @@ -281,10 +281,10 @@ class GqlStatusObject { /** * The position which the notification had occur. * - * @type {NotificationPosition} + * @type {NotificationPosition | undefined} * @public */ - this.position = _constructPosition(this.diagnosticRecord._position) + this.position = this.diagnosticRecord._position != null ? _constructPosition(this.diagnosticRecord._position) : undefined /** * The severity @@ -415,10 +415,7 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { neo4j_code: notification.code, title: notification.title, diagnostic_record: { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', - _status_parameters: {}, + ...rawPolyfilledDiagnosticRecord, _severity: notification.severity, _classification: notification.category, _position: notification.position @@ -426,21 +423,13 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { }) } -const defaultRawDiagnosticRecord = { +const rawPolyfilledDiagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', - _status_parameters: {}, - _severity: '', - _classification: '', - _position: { - offset: -1, - line: -1, - column: -1 - } } -Object.freeze(defaultRawDiagnosticRecord) +Object.freeze(rawPolyfilledDiagnosticRecord) /** * This objects are used for polyfilling the first status on the status list @@ -451,21 +440,21 @@ const staticGqlStatusObjects = { SUCCESS: new GqlStatusObject({ gql_status: '00000', status_description: 'note: successful completion', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), NO_DATA: new GqlStatusObject({ gql_status: '02000', status_description: 'note: no data', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), NO_DATA_UNKNOWN_SUBCONDITION: new GqlStatusObject({ ...unknownGqlStatus.NO_DATA, - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }), OMITTED_RESULT: new GqlStatusObject({ gql_status: '00001', status_description: 'note: successful completion - omitted result', - diagnostic_record: defaultRawDiagnosticRecord + diagnostic_record: rawPolyfilledDiagnosticRecord }) } diff --git a/packages/testkit-backend/src/summary-binder.js b/packages/testkit-backend/src/summary-binder.js index 6d4c5e709..9f13f75a5 100644 --- a/packages/testkit-backend/src/summary-binder.js +++ b/packages/testkit-backend/src/summary-binder.js @@ -51,9 +51,9 @@ function mapGqlStatusObject (binder) { return (gqlStatusObject) => { return { ...gqlStatusObject, - position: Object.keys(gqlStatusObject.position).length !== 0 ? gqlStatusObject.position : null, - rawSeverity: gqlStatusObject.rawSeverity || '', - rawClassification: gqlStatusObject.rawClassification || '', + position: gqlStatusObject.position || null, + rawSeverity: gqlStatusObject.rawSeverity !== undefined ? gqlStatusObject.rawSeverity : null, + rawClassification: gqlStatusObject.rawClassification !== undefined ? gqlStatusObject.rawClassification : null, diagnosticRecord: binder.objectToCypher(gqlStatusObject.diagnosticRecord) } } From e43d31ee509739b659d99228355f5042464584f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 4 Jun 2024 11:30:32 +0200 Subject: [PATCH 25/43] Update packages/core/src/driver.ts Co-authored-by: Stephen Cathcart --- packages/core/src/driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 9233812fe..73d8cbbee 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -903,7 +903,7 @@ function validateConfig (config: any, log: Logger): any { } if (config.notificationFilter?.disabledCategories != null && config.notificationFilter?.disabledClassifications != null) { - throw new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.') + throw new Error('The notificationFilter can't have both "disabledCategories" and "disabledClassifications" configured at the same time.') } return config From 01b4020a7ac145bc6e12293f3b829b792945d4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 4 Jun 2024 11:49:00 +0200 Subject: [PATCH 26/43] Small fixes and sync deno --- packages/core/src/driver.ts | 2 +- packages/core/test/driver.test.ts | 2 +- packages/neo4j-driver-deno/lib/core/driver.ts | 2 +- packages/neo4j-driver-deno/lib/core/notification.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 73d8cbbee..38a93f184 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -903,7 +903,7 @@ function validateConfig (config: any, log: Logger): any { } if (config.notificationFilter?.disabledCategories != null && config.notificationFilter?.disabledClassifications != null) { - throw new Error('The notificationFilter can't have both "disabledCategories" and "disabledClassifications" configured at the same time.') + throw new Error('The notificationFilter can\'t have both "disabledCategories" and "disabledClassifications" configured at the same time.') } return config diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index e81f4e423..e4d7994fb 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -614,7 +614,7 @@ describe('Driver', () => { { notificationFilter }, createConnectionProviderMock, createSession - )).toThrow(new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.')) + )).toThrow(new Error('The notificationFilter can\'t have both "disabledCategories" and "disabledClassifications" configured at the same time.')) expect(createConnectionProviderMock).not.toHaveBeenCalled() }) diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 40b9d4a8a..1e13f0ce3 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -903,7 +903,7 @@ function validateConfig (config: any, log: Logger): any { } if (config.notificationFilter?.disabledCategories != null && config.notificationFilter?.disabledClassifications != null) { - throw new Error('The notificationFilter can have both "disabledCategories" and "disabledClassifications" configured at same time.') + throw new Error('The notificationFilter can\'t have both "disabledCategories" and "disabledClassifications" configured at the same time.') } return config diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 6b28f42fc..5417850c6 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -426,7 +426,7 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { const rawPolyfilledDiagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', - CURRENT_SCHEMA: '/', + CURRENT_SCHEMA: '/' } Object.freeze(rawPolyfilledDiagnosticRecord) From 6fcea2434c42518797d7e5a02d2b348b2e5002b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 7 Jun 2024 12:34:04 +0200 Subject: [PATCH 27/43] Adjust diagnostic record naming to avoid clashes --- packages/core/src/notification.ts | 6 +++--- packages/neo4j-driver-deno/lib/core/notification.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 7bc53ec57..e3fa6d6c2 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -222,7 +222,7 @@ class Notification { } } -interface DiagnosticRecord { +interface NotificationDiagnosticRecord { OPERATION: string OPERATION_CODE: string CURRENT_SCHEMA: string @@ -245,7 +245,7 @@ interface DiagnosticRecord { class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string - public readonly diagnosticRecord: DiagnosticRecord + public readonly diagnosticRecord: NotificationDiagnosticRecord public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string @@ -577,5 +577,5 @@ export type { NotificationSeverityLevel, NotificationCategory, NotificationClassification, - DiagnosticRecord + NotificationDiagnosticRecord } diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 5417850c6..c860107c4 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -222,7 +222,7 @@ class Notification { } } -interface DiagnosticRecord { +interface NotificationDiagnosticRecord { OPERATION: string OPERATION_CODE: string CURRENT_SCHEMA: string @@ -245,7 +245,7 @@ interface DiagnosticRecord { class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string - public readonly diagnosticRecord: DiagnosticRecord + public readonly diagnosticRecord: NotificationDiagnosticRecord public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string @@ -577,5 +577,5 @@ export type { NotificationSeverityLevel, NotificationCategory, NotificationClassification, - DiagnosticRecord + NotificationDiagnosticRecord } From a14dfac6611dc2cee00670ac242b65a196b4395f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Mon, 10 Jun 2024 10:13:33 +0200 Subject: [PATCH 28/43] fix test --- .../test/bolt/behaviour/notification-filter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js index 3004b8403..9839cde73 100644 --- a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js +++ b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js @@ -18,6 +18,7 @@ import { internal, notificationFilterDisabledCategory, + notificationFilterDisabledClassification, notificationFilterMinimumSeverityLevel } from 'neo4j-driver-core' import RequestMessage from '../../../src/bolt/request-message' @@ -375,8 +376,7 @@ export function notificationFilterFixture () { function notificationFilterSetFixture () { const minimumSeverityLevelSet = Object.values(notificationFilterMinimumSeverityLevel) const disabledCategories = Object.values(notificationFilterDisabledCategory) - // TODO: Fix the type - const disabledClassifications = Object.values(notificationFilterDisabledCategory) + const disabledClassifications = Object.values(notificationFilterDisabledClassification) const disabledCategoriesSet = [...disabledCategories.keys()] .map(length => disabledCategories.slice(0, length + 1)) const disabledClassificationsSet = [...disabledCategories.keys()] From 3ef928d9020ff4c6ee6e0ff7c9ecef4e529f6561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 11 Jun 2024 09:38:23 +0200 Subject: [PATCH 29/43] Diagnostic record should only have null values when returned by the server --- packages/core/src/notification.ts | 23 ++++++++++++++----- packages/core/test/notification.test.ts | 4 ++-- .../lib/core/notification.ts | 23 ++++++++++++++----- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index e3fa6d6c2..ebab94cfe 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -409,18 +409,29 @@ function polyfillNotification (status: any): Notification | undefined { */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION - return new GqlStatusObject({ + const polyfilledRawObj: any & { diagnostic_record: NotificationDiagnosticRecord } = { gql_status: defaultStatus.gql_status, status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, title: notification.title, diagnostic_record: { - ...rawPolyfilledDiagnosticRecord, - _severity: notification.severity, - _classification: notification.category, - _position: notification.position + ...rawPolyfilledDiagnosticRecord } - }) + } + + if (notification.severity != null) { + polyfilledRawObj.diagnostic_record._severity = notification.severity + } + + if (notification.category != null) { + polyfilledRawObj.diagnostic_record._classification = notification.category + } + + if (notification.position != null) { + polyfilledRawObj.diagnostic_record._position = notification.position + } + + return new GqlStatusObject(polyfilledRawObj) } const rawPolyfilledDiagnosticRecord = { diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index ee0164ebf..3288b4195 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -616,7 +616,7 @@ describe('GqlStatusObject', () => { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', - _severity: severity, + _severity: severity != null ? severity : undefined, _classification: rawNotification.category, _position: { offset: 0, @@ -656,7 +656,7 @@ describe('GqlStatusObject', () => { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', - _severity: severity, + _severity: severity != null ? severity : undefined, _classification: rawNotification.category, _position: { offset: 0, diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index c860107c4..d64824fc4 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -409,18 +409,29 @@ function polyfillNotification (status: any): Notification | undefined { */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION - return new GqlStatusObject({ + const polyfilledRawObj: any & { diagnostic_record: NotificationDiagnosticRecord } = { gql_status: defaultStatus.gql_status, status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, title: notification.title, diagnostic_record: { - ...rawPolyfilledDiagnosticRecord, - _severity: notification.severity, - _classification: notification.category, - _position: notification.position + ...rawPolyfilledDiagnosticRecord } - }) + } + + if (notification.severity != null) { + polyfilledRawObj.diagnostic_record._severity = notification.severity + } + + if (notification.category != null) { + polyfilledRawObj.diagnostic_record._classification = notification.category + } + + if (notification.position != null) { + polyfilledRawObj.diagnostic_record._position = notification.position + } + + return new GqlStatusObject(polyfilledRawObj) } const rawPolyfilledDiagnosticRecord = { From 23ecd0e238891cc8c474b65c6fbbbb47769538e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Tue, 11 Jun 2024 09:39:32 +0200 Subject: [PATCH 30/43] sync deno --- packages/neo4j-driver-deno/lib/core/notification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index d64824fc4..305c7c2a9 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -430,7 +430,7 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { if (notification.position != null) { polyfilledRawObj.diagnostic_record._position = notification.position } - + return new GqlStatusObject(polyfilledRawObj) } From bedf5bb9cce773f83e45fc76343c166f5b2938cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 14:54:28 +0200 Subject: [PATCH 31/43] Apply suggestions from code review Co-authored-by: Robsdedude --- .../test/bolt/behaviour/notification-filter.js | 2 +- .../bolt-connection/test/bolt/bolt-protocol-v5x5.test.js | 2 +- packages/core/src/notification-filter.ts | 5 ++--- packages/core/src/notification.ts | 6 +++--- packages/core/test/notification.test.ts | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js index 9839cde73..527ca8b72 100644 --- a/packages/bolt-connection/test/bolt/behaviour/notification-filter.js +++ b/packages/bolt-connection/test/bolt/behaviour/notification-filter.js @@ -229,7 +229,7 @@ export function shouldSupportNotificationFilterOnBeginTransaction (createProtoco export function shouldSupportGqlNotificationFilterOnBeginTransaction (createProtocol) { it.each( notificationFilterFixture() - )('should send notificationsFilter=%o on begin a transaction', (notificationFilter) => { + )('should send notificationsFilter=%o on beginning a transaction', (notificationFilter) => { const recorder = new utils.MessageRecordingConnection() const protocol = createProtocol(recorder) utils.spyProtocolWrite(protocol) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js index 503de4706..19670087a 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x5.test.js @@ -1151,7 +1151,7 @@ describe('#unit BoltProtocolV5x5', () => { }) }) - describe('result metadata enrich', () => { + describe('result metadata enrichment', () => { it('run should configure BoltProtocolV5x5._enrichMetadata as enrichMetadata', () => { const database = 'testdb' const bookmarks = new Bookmarks([ diff --git a/packages/core/src/notification-filter.ts b/packages/core/src/notification-filter.ts index 070be9573..0426ca4b4 100644 --- a/packages/core/src/notification-filter.ts +++ b/packages/core/src/notification-filter.ts @@ -101,7 +101,7 @@ class NotificationFilter { * * * This property is equivalent to {@link NotificationFilter#disabledClassifications} - * and it should not be enabled at same time. + * and it must not be enabled at same time. * * @type {?NotificationFilterDisabledCategory[]} */ @@ -111,8 +111,7 @@ class NotificationFilter { * Classifications the user would like to opt-out of receiving. * * This property is equivalent to {@link NotificationFilter#disabledCategories} - * and it should not be enabled at same time. - * + * and it must not be enabled at same time. * * @type {?NotificationFilterDisabledClassification[]} * @experimental diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index ebab94cfe..fc84da1b2 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -176,7 +176,7 @@ class Notification { * } * } */ - this.severityLevel = _asEnumerableSeverity(this.severity) + this.severityLevel = _asEnumerableSeverity(notification.severity) /** * The severity level returned by the server without any validation. @@ -279,7 +279,7 @@ class GqlStatusObject { this.diagnosticRecord = rawGqlStatusObject.diagnostic_record ?? {} /** - * The position which the notification had occur. + * The position at which the notification had occurred. * * @type {NotificationPosition | undefined} * @public @@ -338,7 +338,7 @@ class GqlStatusObject { * break * case neo4j.notificationClassification.UNKNOWN: // or simply 'UNKNOWN' * default: - * // the raw info came from the server could be found at notification.rawCategory + * // the raw info came from the server can be found at notification.rawCategory * console.log(gqlStatusObject.statusDescription) * break * } diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 3288b4195..04aeff821 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -219,7 +219,7 @@ describe('Notification', () => { })) }) - it('should polyfill when diagnostic records is not present', () => { + it('should polyfill when diagnostic record is not present', () => { const status = { neo4j_code: 'Neo.Notification.Warning.Code', gql_status: '03N42', @@ -285,7 +285,7 @@ describe('GqlStatusObject', () => { expect(gqlStatusObject.diagnosticRecord).toBe(diagnosticRecord) }) - it('should fill position with values came from raw.diagnostic_record', () => { + it('should fill position with values from raw.diagnostic_record', () => { const diagnosticRecord = { OPERATION: '', OPERATION_CODE: '0', From 638a990ecd85d1bbff0c621f251016b4eef215b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 14:58:05 +0200 Subject: [PATCH 32/43] Update packages/core/test/notification.test.ts Co-authored-by: Robsdedude --- packages/core/test/notification.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 04aeff821..7cdbcf5d5 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -1107,7 +1107,7 @@ describe('buildGqlStatusObjectFromMetadata', () => { }] } ] - ])('should build from notifications when not available', (filledObject: GqlStatusObject, position: number, metadata: any) => { + ])('should build from notifications when statuses not available', (filledObject: GqlStatusObject, position: number, metadata: any) => { const notifications = metadata.notifications != null ? metadata.notifications : [] const expectedStatuses = notifications.map(polyfillGqlStatusObject) expectedStatuses.splice(position, 0, filledObject) From ada9fb2857d39013bbfc05d9d78c50928f8437a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 15:08:00 +0200 Subject: [PATCH 33/43] address comments in the PR --- .../src/bolt/request-message.js | 26 ++++++++++++------- packages/core/test/notification.test.ts | 20 +++++++------- .../bolt-connection/bolt/request-message.js | 26 ++++++++++++------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index 1d0e20aa6..bad0f1615 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -346,11 +346,13 @@ export default class RequestMessage { * Create a new RUN message with additional metadata. * @param {string} query the cypher query. * @param {Object} parameters the query parameters. - * @param {Bookmarks} bookmarks the bookmarks. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the database name. - * @param {string} mode the access mode. - * @param {string} impersonatedUser the impersonated user. + * @param {Object} extra - extra params + * @param {Bookmarks} extra.bookmarks the bookmarks. + * @param {TxConfig} extra.txConfig the configuration. + * @param {string} extra.database the database name. + * @param {string} extra.mode the access mode. + * @param {string} extra.impersonatedUser the impersonated user. + * @param {notificationFilter} extra.notificationFilter the notification filter * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata ( @@ -371,11 +373,13 @@ export default class RequestMessage { * Create a new RUN message with additional metadata. * @param {string} query the cypher query. * @param {Object} parameters the query parameters. - * @param {Bookmarks} bookmarks the bookmarks. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the database name. - * @param {string} mode the access mode. - * @param {string} impersonatedUser the impersonated user. + * @param {Object} extra - extra params + * @param {Bookmarks} extra.bookmarks the bookmarks. + * @param {TxConfig} extra.txConfig the configuration. + * @param {string} extra.database the database name. + * @param {string} extra.mode the access mode. + * @param {string} extra.impersonatedUser the impersonated user. + * @param {notificationFilter} extra.notificationFilter the notification filter * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata5x5 ( @@ -506,6 +510,8 @@ export default class RequestMessage { * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user mode. * @param {notificationFilter} notificationFilter the notification filter + * @param {Object} functions Transformation functions applied to metadata + * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by append the Notification Filter to it. * @return {Object} a metadata object. */ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 7cdbcf5d5..0b7d4c5d6 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -123,9 +123,9 @@ describe('Notification', () => { } } - const gqlStatusObject = polyfillNotification(status) + const notification = polyfillNotification(status) - expect(gqlStatusObject).toEqual(new Notification({ + expect(notification).toEqual(new Notification({ code: 'Neo.Notification.Warning.Code', title: 'Notification Title', description: 'Description', @@ -160,9 +160,9 @@ describe('Notification', () => { } } - const gqlStatusObject = polyfillNotification(status) + const notification = polyfillNotification(status) - expect(gqlStatusObject).toEqual(new Notification({ + expect(notification).toEqual(new Notification({ code: 'Neo.Notification.Warning.Code', title: 'Notification Title', description: 'Description', @@ -182,7 +182,7 @@ describe('Notification', () => { undefined, 'I_AM_NOT_OKAY', 'information' - ])('should polyfill UNKNOWN', (severity) => { + ])('should polyfill severity UNKNOWN', (severity) => { const status = { neo4j_code: 'Neo.Notification.Warning.Code', gql_status: '03N42', @@ -203,9 +203,9 @@ describe('Notification', () => { } } - const gqlStatusObject = polyfillNotification(status) + const notification = polyfillNotification(status) - expect(gqlStatusObject).toEqual(new Notification({ + expect(notification).toEqual(new Notification({ code: 'Neo.Notification.Warning.Code', title: 'Notification Title', description: 'Description', @@ -227,9 +227,9 @@ describe('Notification', () => { status_description: 'Description' } - const gqlStatusObject = polyfillNotification(status) + const notification = polyfillNotification(status) - expect(gqlStatusObject).toEqual(new Notification({ + expect(notification).toEqual(new Notification({ code: 'Neo.Notification.Warning.Code', title: 'Notification Title', description: 'Description' @@ -592,7 +592,7 @@ describe('GqlStatusObject', () => { undefined, 'I_AM_NOT_OKAY', 'information' - ])('should polyfill UNKNOWN', (severity) => { + ])('should polyfill severity UNKNOWN', (severity) => { const rawNotification = { code: 'Neo.Notification.Warning.Code', title: 'Notification Title', 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 eee7f09a7..ea3627af0 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 @@ -346,11 +346,13 @@ export default class RequestMessage { * Create a new RUN message with additional metadata. * @param {string} query the cypher query. * @param {Object} parameters the query parameters. - * @param {Bookmarks} bookmarks the bookmarks. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the database name. - * @param {string} mode the access mode. - * @param {string} impersonatedUser the impersonated user. + * @param {Object} extra - extra params + * @param {Bookmarks} extra.bookmarks the bookmarks. + * @param {TxConfig} extra.txConfig the configuration. + * @param {string} extra.database the database name. + * @param {string} extra.mode the access mode. + * @param {string} extra.impersonatedUser the impersonated user. + * @param {notificationFilter} extra.notificationFilter the notification filter * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata ( @@ -371,11 +373,13 @@ export default class RequestMessage { * Create a new RUN message with additional metadata. * @param {string} query the cypher query. * @param {Object} parameters the query parameters. - * @param {Bookmarks} bookmarks the bookmarks. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the database name. - * @param {string} mode the access mode. - * @param {string} impersonatedUser the impersonated user. + * @param {Object} extra - extra params + * @param {Bookmarks} extra.bookmarks the bookmarks. + * @param {TxConfig} extra.txConfig the configuration. + * @param {string} extra.database the database name. + * @param {string} extra.mode the access mode. + * @param {string} extra.impersonatedUser the impersonated user. + * @param {notificationFilter} extra.notificationFilter the notification filter * @return {RequestMessage} new RUN message with additional metadata. */ static runWithMetadata5x5 ( @@ -506,6 +510,8 @@ export default class RequestMessage { * @param {string} mode the access mode. * @param {string} impersonatedUser the impersonated user mode. * @param {notificationFilter} notificationFilter the notification filter + * @param {Object} functions Transformation functions applied to metadata + * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by append the Notification Filter to it. * @return {Object} a metadata object. */ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { From f9b117ef29b3e87666b073eb1f0728b9ae23918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 15:14:38 +0200 Subject: [PATCH 34/43] sync deno --- packages/neo4j-driver-deno/lib/core/notification-filter.ts | 5 ++--- packages/neo4j-driver-deno/lib/core/notification.ts | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/notification-filter.ts b/packages/neo4j-driver-deno/lib/core/notification-filter.ts index ed0ddf501..1523782b4 100644 --- a/packages/neo4j-driver-deno/lib/core/notification-filter.ts +++ b/packages/neo4j-driver-deno/lib/core/notification-filter.ts @@ -101,7 +101,7 @@ class NotificationFilter { * * * This property is equivalent to {@link NotificationFilter#disabledClassifications} - * and it should not be enabled at same time. + * and it must not be enabled at same time. * * @type {?NotificationFilterDisabledCategory[]} */ @@ -111,8 +111,7 @@ class NotificationFilter { * Classifications the user would like to opt-out of receiving. * * This property is equivalent to {@link NotificationFilter#disabledCategories} - * and it should not be enabled at same time. - * + * and it must not be enabled at same time. * * @type {?NotificationFilterDisabledClassification[]} * @experimental diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 305c7c2a9..29fef6bce 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -176,7 +176,7 @@ class Notification { * } * } */ - this.severityLevel = _asEnumerableSeverity(this.severity) + this.severityLevel = _asEnumerableSeverity(notification.severity) /** * The severity level returned by the server without any validation. @@ -279,7 +279,7 @@ class GqlStatusObject { this.diagnosticRecord = rawGqlStatusObject.diagnostic_record ?? {} /** - * The position which the notification had occur. + * The position at which the notification had occurred. * * @type {NotificationPosition | undefined} * @public @@ -338,7 +338,7 @@ class GqlStatusObject { * break * case neo4j.notificationClassification.UNKNOWN: // or simply 'UNKNOWN' * default: - * // the raw info came from the server could be found at notification.rawCategory + * // the raw info came from the server can be found at notification.rawCategory * console.log(gqlStatusObject.statusDescription) * break * } From f7e424450839142a5d35d8a0f0c072d0d070fd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 15:19:43 +0200 Subject: [PATCH 35/43] Ajust test --- packages/core/test/notification.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 0b7d4c5d6..84fe364de 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -1232,7 +1232,7 @@ describe('buildNotificationsFromMetadata', () => { } ], [ - getSuccessStatusObject(), 0, { + { stream_summary: { have_records_streamed: true }, @@ -1258,10 +1258,10 @@ describe('buildNotificationsFromMetadata', () => { column: 0 }, category: 'TOPOLOGY' - }] + }], + statuses: [getSuccessStatusObject()] } ] - ])('should build from notifications when available', (metadata: any) => { const notifications = metadata.notifications != null ? metadata.notifications : [] const expectedNotifications = notifications.map((notification: any) => new Notification(notification)) From 4e15f2d92e6e45cf983a28a0aff15b028e2a05c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 15:24:35 +0200 Subject: [PATCH 36/43] update gqlstatus docs --- packages/core/src/result-summary.ts | 12 +++++++++--- .../neo4j-driver-deno/lib/core/result-summary.ts | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 3da4a366a..1ab747c3d 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -114,13 +114,19 @@ class ResultSummary { * A list of GqlStatusObjects that arise when executing the query. * * The list always contains at least 1 status representing the Success, No Data or Omitted Result. - * This status will be always the first one. - * When discarding records while connected to a non-gql aware server, the driver might not be able to - * tell apart Success and No Data. + * + * When discarding records while connected to a non-gql aware server and using a RxSession, + * the driver might not be able to tell apart Success and No Data. * * All following status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. * + * The GqlStatusObjects will be presented in the following order: + * + * - A “no data” (02xxx) has precedence over a warning; + * - A warning (01xxx) has precedence over a success. + * - A success (00xxx) has precedence over anything informational (03xxx) + * * @type {Array} * @public * @experimental diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index f08a5bccd..f43d44da2 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -114,12 +114,18 @@ class ResultSummary { * A list of GqlStatusObjects that arise when executing the query. * * The list always contains at least 1 status representing the Success, No Data or Omitted Result. - * This status will be always the first one. - * When discarding records while connected to a non-gql aware server, the driver might not be able to - * tell apart Success and No Data. + * + * When discarding records while connected to a non-gql aware server and using a RxSession, + * the driver might not be able to tell apart Success and No Data. * * All following status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. + * + * The GqlStatusObjects will be presented in the following order: + * + * - A “no data” (02xxx) has precedence over a warning; + * - A warning (01xxx) has precedence over a success. + * - A success (00xxx) has precedence over anything informational (03xxx) * * @type {Array} * @public From fa1fe229a627f4259b57e45222365e395c1168e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 12 Jun 2024 15:25:11 +0200 Subject: [PATCH 37/43] sync deno --- packages/neo4j-driver-deno/lib/core/result-summary.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index f43d44da2..c2d8f2b96 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -114,13 +114,13 @@ class ResultSummary { * A list of GqlStatusObjects that arise when executing the query. * * The list always contains at least 1 status representing the Success, No Data or Omitted Result. - * - * When discarding records while connected to a non-gql aware server and using a RxSession, + * + * When discarding records while connected to a non-gql aware server and using a RxSession, * the driver might not be able to tell apart Success and No Data. * * All following status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. - * + * * The GqlStatusObjects will be presented in the following order: * * - A “no data” (02xxx) has precedence over a warning; From b5984b337b9e0bf6051e7ca331673bc7069654b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 13 Jun 2024 09:55:20 +0200 Subject: [PATCH 38/43] Adjust diagnostic record serialization --- packages/core/src/json.ts | 15 +- packages/core/src/notification.ts | 7 +- .../core/test/__snapshots__/json.test.ts.snap | 216 ++++++++++++++++++ packages/core/test/json.test.ts | 59 ++++- packages/core/test/notification.test.ts | 7 +- packages/neo4j-driver-deno/lib/core/json.ts | 15 +- .../lib/core/notification.ts | 7 +- 7 files changed, 319 insertions(+), 7 deletions(-) diff --git a/packages/core/src/json.ts b/packages/core/src/json.ts index e0fa61d83..cba26b0ba 100644 --- a/packages/core/src/json.ts +++ b/packages/core/src/json.ts @@ -17,13 +17,17 @@ import { isBrokenObject, getBrokenObjectReason } from './internal/object-util' +interface StringifyOpts { + useCustomToString?: boolean +} + /** * Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt. * @private * @param val A JavaScript value, usually an object or array, to be converted. * @returns A JSON string representing the given value. */ -export function stringify (val: any): string { +export function stringify (val: any, opts?: StringifyOpts): string { return JSON.stringify(val, (_, value) => { if (isBrokenObject(value)) { return { @@ -31,9 +35,18 @@ export function stringify (val: any): string { __reason__: getBrokenObjectReason(value) } } + if (typeof value === 'bigint') { return `${value}n` } + + if (opts?.useCustomToString === true && + typeof value === 'object' && + !Array.isArray(value) && + typeof value.toString === 'function' && + value.toString !== Object.prototype.toString) { + return value?.toString() + } return value }) } diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index fc84da1b2..ca124f2a5 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -253,6 +253,11 @@ class GqlStatusObject { public readonly rawClassification?: string public readonly isNotification: boolean + /** + * + * @param rawGqlStatusObject + * @private + */ constructor (rawGqlStatusObject: any) { /** * The GQLSTATUS @@ -376,7 +381,7 @@ class GqlStatusObject { * @public */ public get diagnosticRecordAsJsonString (): string { - return json.stringify(this.diagnosticRecord) + return json.stringify(this.diagnosticRecord, { useCustomToString: true }) } } diff --git a/packages/core/test/__snapshots__/json.test.ts.snap b/packages/core/test/__snapshots__/json.test.ts.snap index 6f85f5a26..14abb06b7 100644 --- a/packages/core/test/__snapshots__/json.test.ts.snap +++ b/packages/core/test/__snapshots__/json.test.ts.snap @@ -1,7 +1,223 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`json .stringify should handle Date 1`] = `"{"year":1999,"month":4,"day":12}"`; + +exports[`json .stringify should handle Date in list 1`] = `"[{"year":1999,"month":4,"day":12}]"`; + +exports[`json .stringify should handle Date in object 1`] = `"{"key":{"year":1999,"month":4,"day":12}}"`; + +exports[`json .stringify should handle DateTime 1`] = `"{"year":2024,"month":6,"day":13,"hour":10,"minute":0,"second":30,"nanosecond":134,"timeZoneOffsetSeconds":-3600,"timeZoneId":"Europe/Berlin"}"`; + +exports[`json .stringify should handle DateTime in list 1`] = `"[{"year":2024,"month":6,"day":13,"hour":10,"minute":0,"second":30,"nanosecond":134,"timeZoneOffsetSeconds":-3600,"timeZoneId":"Europe/Berlin"}]"`; + +exports[`json .stringify should handle DateTime in object 1`] = `"{"key":{"year":2024,"month":6,"day":13,"hour":10,"minute":0,"second":30,"nanosecond":134,"timeZoneOffsetSeconds":-3600,"timeZoneId":"Europe/Berlin"}}"`; + +exports[`json .stringify should handle Duration 1`] = `"{"months":10,"days":2,"seconds":{"low":35,"high":0},"nanoseconds":{"low":100,"high":0}}"`; + +exports[`json .stringify should handle Duration in list 1`] = `"[{"months":10,"days":2,"seconds":{"low":35,"high":0},"nanoseconds":{"low":100,"high":0}}]"`; + +exports[`json .stringify should handle Duration in object 1`] = `"{"key":{"months":10,"days":2,"seconds":{"low":35,"high":0},"nanoseconds":{"low":100,"high":0}}}"`; + +exports[`json .stringify should handle Integer 1`] = `"{"low":5,"high":0}"`; + +exports[`json .stringify should handle Integer in list 1`] = `"[{"low":5,"high":0}]"`; + +exports[`json .stringify should handle Integer in object 1`] = `"{"key":{"low":5,"high":0}}"`; + +exports[`json .stringify should handle LocalDateTime 1`] = `"{"year":1999,"month":4,"day":28,"hour":12,"minute":40,"second":12,"nanosecond":301}"`; + +exports[`json .stringify should handle LocalDateTime in list 1`] = `"[{"year":1999,"month":4,"day":28,"hour":12,"minute":40,"second":12,"nanosecond":301}]"`; + +exports[`json .stringify should handle LocalDateTime in object 1`] = `"{"key":{"year":1999,"month":4,"day":28,"hour":12,"minute":40,"second":12,"nanosecond":301}}"`; + +exports[`json .stringify should handle LocalTime 1`] = `"{"hour":2,"minute":30,"second":25,"nanosecond":150}"`; + +exports[`json .stringify should handle LocalTime in list 1`] = `"[{"hour":2,"minute":30,"second":25,"nanosecond":150}]"`; + +exports[`json .stringify should handle LocalTime in object 1`] = `"{"key":{"hour":2,"minute":30,"second":25,"nanosecond":150}}"`; + +exports[`json .stringify should handle Node 1`] = `"{"identity":1,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"myId"}"`; + +exports[`json .stringify should handle Node in list 1`] = `"[{"identity":1,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"myId"}]"`; + +exports[`json .stringify should handle Node in object 1`] = `"{"key":{"identity":1,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"myId"}}"`; + +exports[`json .stringify should handle Path 1`] = `"{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"},"segments":[{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"relationship":{"identity":3,"start":1,"end":2,"type":"PLAY_FOOTBALL","properties":{"since":1897},"elementId":"relId","startNodeElementId":"antonioId","endNodeElementId":"mrBauerId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"}}],"length":1}"`; + +exports[`json .stringify should handle Path in list 1`] = `"[{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"},"segments":[{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"relationship":{"identity":3,"start":1,"end":2,"type":"PLAY_FOOTBALL","properties":{"since":1897},"elementId":"relId","startNodeElementId":"antonioId","endNodeElementId":"mrBauerId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"}}],"length":1}]"`; + +exports[`json .stringify should handle Path in object 1`] = `"{"key":{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"},"segments":[{"start":{"identity":1,"labels":["Person"],"properties":{"name":"Antonio"},"elementId":"antonioId"},"relationship":{"identity":3,"start":1,"end":2,"type":"PLAY_FOOTBALL","properties":{"since":1897},"elementId":"relId","startNodeElementId":"antonioId","endNodeElementId":"mrBauerId"},"end":{"identity":2,"labels":["Person"],"properties":{"name":"Mr. Bauer"},"elementId":"mrBauerId"}}],"length":1}}"`; + +exports[`json .stringify should handle Point 1`] = `"{"srid":4979,"x":1,"y":2,"z":3}"`; + +exports[`json .stringify should handle Point in list 1`] = `"[{"srid":4979,"x":1,"y":2,"z":3}]"`; + +exports[`json .stringify should handle Point in object 1`] = `"{"key":{"srid":4979,"x":1,"y":2,"z":3}}"`; + +exports[`json .stringify should handle Relationship 1`] = `"{"identity":1,"start":2,"end":3,"type":"FRIENDSHIP","properties":{"started":1999},"elementId":"myId","startNodeElementId":"startId","endNodeElementId":"endId"}"`; + +exports[`json .stringify should handle Relationship in list 1`] = `"[{"identity":1,"start":2,"end":3,"type":"FRIENDSHIP","properties":{"started":1999},"elementId":"myId","startNodeElementId":"startId","endNodeElementId":"endId"}]"`; + +exports[`json .stringify should handle Relationship in object 1`] = `"{"key":{"identity":1,"start":2,"end":3,"type":"FRIENDSHIP","properties":{"started":1999},"elementId":"myId","startNodeElementId":"startId","endNodeElementId":"endId"}}"`; + +exports[`json .stringify should handle Time 1`] = `"{"hour":12,"minute":50,"second":23,"nanosecond":300,"timeZoneOffsetSeconds":3600}"`; + +exports[`json .stringify should handle Time in list 1`] = `"[{"hour":12,"minute":50,"second":23,"nanosecond":300,"timeZoneOffsetSeconds":3600}]"`; + +exports[`json .stringify should handle Time in object 1`] = `"{"key":{"hour":12,"minute":50,"second":23,"nanosecond":300,"timeZoneOffsetSeconds":3600}}"`; + +exports[`json .stringify should handle UnboundRelationship 1`] = `"{"identity":1,"type":"ALONE","properties":{"since":2001},"elementId":"myId"}"`; + +exports[`json .stringify should handle UnboundRelationship in list 1`] = `"[{"identity":1,"type":"ALONE","properties":{"since":2001},"elementId":"myId"}]"`; + +exports[`json .stringify should handle UnboundRelationship in object 1`] = `"{"key":{"identity":1,"type":"ALONE","properties":{"since":2001},"elementId":"myId"}}"`; + +exports[`json .stringify should handle bigint 1`] = `""3n""`; + +exports[`json .stringify should handle bigint in list 1`] = `"["3n"]"`; + +exports[`json .stringify should handle bigint in object 1`] = `"{"key":"3n"}"`; + +exports[`json .stringify should handle list 1`] = `"["1",2,{"tres":3}]"`; + +exports[`json .stringify should handle list in list 1`] = `"[["1",2,{"tres":3}]]"`; + +exports[`json .stringify should handle list in object 1`] = `"{"key":["1",2,{"tres":3}]}"`; + +exports[`json .stringify should handle number 1`] = `"2"`; + +exports[`json .stringify should handle number in list 1`] = `"[2]"`; + +exports[`json .stringify should handle number in object 1`] = `"{"key":2}"`; + +exports[`json .stringify should handle object 1`] = `"{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}"`; + +exports[`json .stringify should handle object in list 1`] = `"[{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}]"`; + +exports[`json .stringify should handle object in object 1`] = `"{"key":{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}}"`; + +exports[`json .stringify should handle object with custom toString 1`] = `"{"identity":"1"}"`; + +exports[`json .stringify should handle object with custom toString in list 1`] = `"[{"identity":"1"}]"`; + +exports[`json .stringify should handle object with custom toString in object 1`] = `"{"key":{"identity":"1"}}"`; + exports[`json .stringify should handle objects created with createBrokenObject 1`] = `"{"__isBrokenObject__":true,"__reason__":{"code":"N/A","name":"Neo4jError","retriable":false}}"`; exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"code":"N/A","name":"Neo4jError","retriable":false}}]"`; exports[`json .stringify should handle objects created with createBrokenObject inside other object 1`] = `"{"number":1,"broken":{"__isBrokenObject__":true,"__reason__":{"code":"N/A","name":"Neo4jError","retriable":false}}}"`; + +exports[`json .stringify should handle string 1`] = `""my string""`; + +exports[`json .stringify should handle string in list 1`] = `"["my string"]"`; + +exports[`json .stringify should handle string in object 1`] = `"{"key":"my string"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Date 1`] = `""1999-04-12""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Date in list 1`] = `"["1999-04-12"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Date in object 1`] = `"{"key":"1999-04-12"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle DateTime 1`] = `""2024-06-13T10:00:30.000000134-01:00[Europe/Berlin]""`; + +exports[`json .stringify when opts.useCustomToString=true should handle DateTime in list 1`] = `"["2024-06-13T10:00:30.000000134-01:00[Europe/Berlin]"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle DateTime in object 1`] = `"{"key":"2024-06-13T10:00:30.000000134-01:00[Europe/Berlin]"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Duration 1`] = `""P10M2DT35.000000100S""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Duration in list 1`] = `"["P10M2DT35.000000100S"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Duration in object 1`] = `"{"key":"P10M2DT35.000000100S"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Integer 1`] = `""5""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Integer in list 1`] = `"["5"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Integer in object 1`] = `"{"key":"5"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalDateTime 1`] = `""1999-04-28T12:40:12.000000301""`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalDateTime in list 1`] = `"["1999-04-28T12:40:12.000000301"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalDateTime in object 1`] = `"{"key":"1999-04-28T12:40:12.000000301"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalTime 1`] = `""02:30:25.000000150""`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalTime in list 1`] = `"["02:30:25.000000150"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle LocalTime in object 1`] = `"{"key":"02:30:25.000000150"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Node 1`] = `""(myId:Person {name:\\"Mr. Bauer\\"})""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Node in list 1`] = `"["(myId:Person {name:\\"Mr. Bauer\\"})"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Node in object 1`] = `"{"key":"(myId:Person {name:\\"Mr. Bauer\\"})"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Path 1`] = `"{"start":"(antonioId:Person {name:\\"Antonio\\"})","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})","segments":[{"start":"(antonioId:Person {name:\\"Antonio\\"})","relationship":"(antonioId)-[:PLAY_FOOTBALL {since:1897}]->(mrBauerId)","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})"}],"length":1}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Path in list 1`] = `"[{"start":"(antonioId:Person {name:\\"Antonio\\"})","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})","segments":[{"start":"(antonioId:Person {name:\\"Antonio\\"})","relationship":"(antonioId)-[:PLAY_FOOTBALL {since:1897}]->(mrBauerId)","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})"}],"length":1}]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Path in object 1`] = `"{"key":{"start":"(antonioId:Person {name:\\"Antonio\\"})","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})","segments":[{"start":"(antonioId:Person {name:\\"Antonio\\"})","relationship":"(antonioId)-[:PLAY_FOOTBALL {since:1897}]->(mrBauerId)","end":"(mrBauerId:Person {name:\\"Mr. Bauer\\"})"}],"length":1}}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Point 1`] = `""Point{srid=4979.0, x=1.0, y=2.0, z=3.0}""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Point in list 1`] = `"["Point{srid=4979.0, x=1.0, y=2.0, z=3.0}"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Point in object 1`] = `"{"key":"Point{srid=4979.0, x=1.0, y=2.0, z=3.0}"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Relationship 1`] = `""(startId)-[:FRIENDSHIP {started:1999}]->(endId)""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Relationship in list 1`] = `"["(startId)-[:FRIENDSHIP {started:1999}]->(endId)"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Relationship in object 1`] = `"{"key":"(startId)-[:FRIENDSHIP {started:1999}]->(endId)"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Time 1`] = `""12:50:23.000000300+01:00""`; + +exports[`json .stringify when opts.useCustomToString=true should handle Time in list 1`] = `"["12:50:23.000000300+01:00"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle Time in object 1`] = `"{"key":"12:50:23.000000300+01:00"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle UnboundRelationship 1`] = `""-[:ALONE {since:2001}]->""`; + +exports[`json .stringify when opts.useCustomToString=true should handle UnboundRelationship in list 1`] = `"["-[:ALONE {since:2001}]->"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle UnboundRelationship in object 1`] = `"{"key":"-[:ALONE {since:2001}]->"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle bigint 1`] = `""3n""`; + +exports[`json .stringify when opts.useCustomToString=true should handle bigint in list 1`] = `"["3n"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle bigint in object 1`] = `"{"key":"3n"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle list 1`] = `"["1",2,{"tres":3}]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle list in list 1`] = `"[["1",2,{"tres":3}]]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle list in object 1`] = `"{"key":["1",2,{"tres":3}]}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle number 1`] = `"2"`; + +exports[`json .stringify when opts.useCustomToString=true should handle number in list 1`] = `"[2]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle number in object 1`] = `"{"key":2}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle object 1`] = `"{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle object in list 1`] = `"[{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle object in object 1`] = `"{"key":{"identity":123,"labels":["a"],"properties":{"key":"value"},"elementId":"abc"}}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle object with custom toString 1`] = `""My identity is One""`; + +exports[`json .stringify when opts.useCustomToString=true should handle object with custom toString in list 1`] = `"["My identity is One"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle object with custom toString in object 1`] = `"{"key":"My identity is One"}"`; + +exports[`json .stringify when opts.useCustomToString=true should handle string 1`] = `""my string""`; + +exports[`json .stringify when opts.useCustomToString=true should handle string in list 1`] = `"["my string"]"`; + +exports[`json .stringify when opts.useCustomToString=true should handle string in object 1`] = `"{"key":"my string"}"`; diff --git a/packages/core/test/json.test.ts b/packages/core/test/json.test.ts index e0dd140e4..88b64ce3a 100644 --- a/packages/core/test/json.test.ts +++ b/packages/core/test/json.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { json, newError } from '../src' +import { Date, DateTime, Duration, LocalDateTime, LocalTime, Node, Path, PathSegment, Point, Relationship, Time, UnboundRelationship, int, json, newError } from '../src' import { createBrokenObject } from '../src/internal/object-util' describe('json', () => { @@ -43,5 +43,62 @@ describe('json', () => { broken })).toMatchSnapshot() }) + + it.each(commonTypesFixture())('should handle %s', (_, value) => { + expect(json.stringify(value)).toMatchSnapshot() + }) + + it.each(commonTypesFixture())('should handle %s in list', (_, value) => { + expect(json.stringify([value])).toMatchSnapshot() + }) + + it.each(commonTypesFixture())('should handle %s in object', (_, value) => { + expect(json.stringify({ key: value })).toMatchSnapshot() + }) + + describe('when opts.useCustomToString=true', () => { + it.each(commonTypesFixture())('should handle %s', (_, value) => { + expect(json.stringify(value, { useCustomToString: true })).toMatchSnapshot() + }) + + it.each(commonTypesFixture())('should handle %s in list', (_, value) => { + expect(json.stringify([value], { useCustomToString: true })).toMatchSnapshot() + }) + + it.each(commonTypesFixture())('should handle %s in object', (_, value) => { + expect(json.stringify({ key: value }, { useCustomToString: true })).toMatchSnapshot() + }) + }) }) }) + +function commonTypesFixture (): Array<[string, unknown]> { + return [ + ['number', 2], + ['bigint', BigInt(3)], + ['Integer', int(5)], + ['string', 'my string'], + ['object', { identity: 123, labels: ['a'], properties: { key: 'value' }, elementId: 'abc' }], + ['object with custom toString', { identity: '1', toString () { return 'My identity is One' } }], + ['list', ['1', 2, { tres: 3 }]], + ['Node', new Node(1, ['Person'], { name: 'Mr. Bauer' }, 'myId')], + ['Relationship', new Relationship(1, 2, 3, 'FRIENDSHIP', { started: 1999 }, 'myId', 'startId', 'endId')], + ['UnboundRelationship', new UnboundRelationship(1, 'ALONE', { since: 2001 }, 'myId')], + ['Path', new Path( + new Node(1, ['Person'], { name: 'Antonio' }, 'antonioId'), + new Node(2, ['Person'], { name: 'Mr. Bauer' }, 'mrBauerId'), + [new PathSegment( + new Node(1, ['Person'], { name: 'Antonio' }, 'antonioId'), + new Relationship(3, 1, 2, 'PLAY_FOOTBALL', { since: 1897 }, 'relId', 'antonioId', 'mrBauerId'), + new Node(2, ['Person'], { name: 'Mr. Bauer' }, 'mrBauerId') + )]) + ], + ['Point', new Point(4979, 1, 2, 3)], + ['Duration', new Duration(10, 2, 35, 100)], + ['LocalTime', new LocalTime(2, 30, 25, 150)], + ['Time', new Time(12, 50, 23, 300, 3600)], + ['Date', new Date(1999, 4, 12)], + ['LocalDateTime', new LocalDateTime(1999, 4, 28, 12, 40, 12, 301)], + ['DateTime', new DateTime(2024, 6, 13, 10, 0, 30, 134, -3600, 'Europe/Berlin')] + ] +} diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 84fe364de..d79623600 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { Node, int } from '../src' import * as json from '../src/json' import { Notification, @@ -435,7 +436,9 @@ describe('GqlStatusObject', () => { line: 0, column: 0 }, - _status_parameters: {} + _status_parameters: { + some_node: new Node(int(1), ['ABC'], { bla: 'string' }, 'myElementId') + } } const rawGqlStatusObject = { diagnostic_record: diagnosticRecord @@ -443,7 +446,7 @@ describe('GqlStatusObject', () => { const gqlStatusObject = new GqlStatusObject(rawGqlStatusObject) - expect(gqlStatusObject.diagnosticRecordAsJsonString).toBe(json.stringify(diagnosticRecord)) + expect(gqlStatusObject.diagnosticRecordAsJsonString).toBe(json.stringify(diagnosticRecord, { useCustomToString: true })) }) }) diff --git a/packages/neo4j-driver-deno/lib/core/json.ts b/packages/neo4j-driver-deno/lib/core/json.ts index 22bb4ad6c..25142ec12 100644 --- a/packages/neo4j-driver-deno/lib/core/json.ts +++ b/packages/neo4j-driver-deno/lib/core/json.ts @@ -17,13 +17,17 @@ import { isBrokenObject, getBrokenObjectReason } from './internal/object-util.ts' +interface StringifyOpts { + useCustomToString?: boolean +} + /** * Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt. * @private * @param val A JavaScript value, usually an object or array, to be converted. * @returns A JSON string representing the given value. */ -export function stringify (val: any): string { +export function stringify (val: any, opts?: StringifyOpts): string { return JSON.stringify(val, (_, value) => { if (isBrokenObject(value)) { return { @@ -31,9 +35,18 @@ export function stringify (val: any): string { __reason__: getBrokenObjectReason(value) } } + if (typeof value === 'bigint') { return `${value}n` } + + if (opts?.useCustomToString === true && + typeof value === 'object' && + !Array.isArray(value) && + typeof value.toString === 'function' && + value.toString !== Object.prototype.toString) { + return value?.toString() + } return value }) } diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 29fef6bce..d5566614c 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -253,6 +253,11 @@ class GqlStatusObject { public readonly rawClassification?: string public readonly isNotification: boolean + /** + * + * @param rawGqlStatusObject + * @private + */ constructor (rawGqlStatusObject: any) { /** * The GQLSTATUS @@ -376,7 +381,7 @@ class GqlStatusObject { * @public */ public get diagnosticRecordAsJsonString (): string { - return json.stringify(this.diagnosticRecord) + return json.stringify(this.diagnosticRecord, { useCustomToString: true }) } } From 7da3f785f2a3db787c334fcd976f69619344b915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 13 Jun 2024 11:33:14 +0200 Subject: [PATCH 39/43] Adjust status_description for warn and info --- packages/core/src/notification.ts | 4 ++-- packages/core/test/notification.test.ts | 10 +++++----- packages/neo4j-driver-deno/lib/core/notification.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index ca124f2a5..8dfa890b9 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -28,7 +28,7 @@ type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` const unknownGqlStatus: Record = { WARNING: { gql_status: '01N42', - status_description: 'warn: warning - unknown warning' + status_description: 'warn: unknown warning' }, NO_DATA: { gql_status: '02N42', @@ -36,7 +36,7 @@ const unknownGqlStatus: Record { expect(gqlStatusObject).toEqual(new GqlStatusObject({ neo4j_code: rawNotification.code, gql_status: '01N42', - status_description: 'warn: warning - unknown warning', + status_description: 'warn: unknown warning', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -573,7 +573,7 @@ describe('GqlStatusObject', () => { expect(gqlStatusObject).toEqual(new GqlStatusObject({ neo4j_code: rawNotification.code, gql_status: '03N42', - status_description: 'info: informational - unknown notification', + status_description: 'info: unknown notification', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -654,7 +654,7 @@ describe('GqlStatusObject', () => { expect(gqlStatusObject).toEqual(new GqlStatusObject({ neo4j_code: rawNotification.code, gql_status: '03N42', - status_description: 'info: informational - unknown notification', + status_description: 'info: unknown notification', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', @@ -1425,7 +1425,7 @@ function getValidNotificationStatus (): any [] { return [ { gql_status: '01N00', - status_description: 'warn: warning - feature deprecated', + status_description: 'warn: feature deprecated', neo4j_code: 'Neo.Some.Warning.Code', title: 'the title', diagnostic_record: { @@ -1439,7 +1439,7 @@ function getValidNotificationStatus (): any [] { }, { gql_status: '03N60', - status_description: 'info: informational - subquery variable shadowing', + status_description: 'info: subquery variable shadowing', neo4j_code: 'Neo.Some.Informational.Code', title: 'the title', diagnostic_record: { diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index d5566614c..1f600458f 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -28,7 +28,7 @@ type UnknownGqlStatus = `${'01' | '02' | '03' | '50'}N42` const unknownGqlStatus: Record = { WARNING: { gql_status: '01N42', - status_description: 'warn: warning - unknown warning' + status_description: 'warn: unknown warning' }, NO_DATA: { gql_status: '02N42', @@ -36,7 +36,7 @@ const unknownGqlStatus: Record Date: Thu, 13 Jun 2024 11:39:21 +0200 Subject: [PATCH 40/43] Better type support for DiagnosticRecord --- packages/core/src/notification.ts | 9 +++++++-- packages/neo4j-driver-deno/lib/core/notification.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 8dfa890b9..894800fbf 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -16,6 +16,7 @@ */ import * as json from './json' import { util } from './internal' +import { NumberOrInteger } from './graph-types' interface NotificationPosition { offset?: number @@ -228,8 +229,12 @@ interface NotificationDiagnosticRecord { CURRENT_SCHEMA: string _severity?: string _classification?: string - _position?: object - _status_parameters?: object + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record [key: string]: unknown } diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 1f600458f..3f9ee7269 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -16,6 +16,7 @@ */ import * as json from './json.ts' import { util } from './internal/index.ts' +import { NumberOrInteger } from './graph-types.ts' interface NotificationPosition { offset?: number @@ -228,8 +229,12 @@ interface NotificationDiagnosticRecord { CURRENT_SCHEMA: string _severity?: string _classification?: string - _position?: object - _status_parameters?: object + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record [key: string]: unknown } From c88068225c7dbb2e18f130062d65cbe2ce42d94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Thu, 13 Jun 2024 13:19:15 +0200 Subject: [PATCH 41/43] More unit tests --- packages/core/test/notification.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/core/test/notification.test.ts b/packages/core/test/notification.test.ts index 409ea9544..d3227afd4 100644 --- a/packages/core/test/notification.test.ts +++ b/packages/core/test/notification.test.ts @@ -972,6 +972,15 @@ describe('buildGqlStatusObjectFromMetadata', () => { } } ], + [ + getOmittedResultStatusObject(), 0, { + stream_summary: { + have_records_streamed: false, + pulled: false, + has_keys: false + } + } + ], [ getOmittedResultStatusObject(), 0, { stream_summary: { From ac12a3f0fe48504a130aad32be1d43c04b894bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 14 Jun 2024 12:37:57 +0200 Subject: [PATCH 42/43] Apply suggestions from code review Co-authored-by: Robsdedude --- packages/bolt-connection/src/bolt/request-message.js | 2 +- packages/core/src/result-summary.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index bad0f1615..1a08d100b 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -511,7 +511,7 @@ export default class RequestMessage { * @param {string} impersonatedUser the impersonated user mode. * @param {notificationFilter} notificationFilter the notification filter * @param {Object} functions Transformation functions applied to metadata - * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by append the Notification Filter to it. + * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by appending the Notification Filter to it. * @return {Object} a metadata object. */ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { diff --git a/packages/core/src/result-summary.ts b/packages/core/src/result-summary.ts index 1ab747c3d..4b2cfc5fc 100644 --- a/packages/core/src/result-summary.ts +++ b/packages/core/src/result-summary.ts @@ -118,7 +118,7 @@ class ResultSummary { * When discarding records while connected to a non-gql aware server and using a RxSession, * the driver might not be able to tell apart Success and No Data. * - * All following status are notifications like warnings about problematic queries or other valuable + * All other status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. * * The GqlStatusObjects will be presented in the following order: From 0dce22251ca7deda982d3cb8108648d4f0884920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Fri, 14 Jun 2024 12:41:40 +0200 Subject: [PATCH 43/43] Sync deno --- .../lib/bolt-connection/bolt/request-message.js | 2 +- packages/neo4j-driver-deno/lib/core/result-summary.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 ea3627af0..43812e795 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 @@ -511,7 +511,7 @@ export default class RequestMessage { * @param {string} impersonatedUser the impersonated user mode. * @param {notificationFilter} notificationFilter the notification filter * @param {Object} functions Transformation functions applied to metadata - * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by append the Notification Filter to it. + * @param {function(metadata,notificationFilter):void} functions.appendNotificationFilter Changes metadata by appending the Notification Filter to it. * @return {Object} a metadata object. */ function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser, notificationFilter, functions = {}) { diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts index c2d8f2b96..cf0a8552a 100644 --- a/packages/neo4j-driver-deno/lib/core/result-summary.ts +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -118,7 +118,7 @@ class ResultSummary { * When discarding records while connected to a non-gql aware server and using a RxSession, * the driver might not be able to tell apart Success and No Data. * - * All following status are notifications like warnings about problematic queries or other valuable + * All other status are notifications like warnings about problematic queries or other valuable * information that can be presented in a client. * * The GqlStatusObjects will be presented in the following order: