+
+
+
+
+ WARNING
+
+
+ My xx1 warning
+
+
+
+
+ This is xx2 warning
+
+
+
+ EXPLAIN MATCH xx3
+
+
+ ^
+
+
+
+
+ Status code:
+
+ xx3.Warning
+
diff --git a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts
new file mode 100644
index 00000000000..ec3cfd4c1f0
--- /dev/null
+++ b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.test.ts
@@ -0,0 +1,56 @@
+import {
+ formatDescriptionFromGqlStatusDescription,
+ formatTitleFromGqlStatusDescription
+} from './gqlStatusUtils'
+
+describe('gql status formatting', () => {
+ test('formats a title from a gql status description correctly', () => {
+ const gqlStatusDescription =
+ 'error: syntax error or access rule violation - invalid parameter. Invalid parameter $`param`.'
+
+ const result = formatTitleFromGqlStatusDescription(gqlStatusDescription)
+
+ expect(result).toEqual(
+ 'Syntax error or access rule violation - invalid parameter'
+ )
+ })
+
+ test('formats a description from a gql status description correctly', () => {
+ const gqlStatusDescription =
+ "error: system configuration or operation exception - cyclic shortest path search disabled. Cannot find the shortest path when the start and end nodes are the same. To enable this behavior, set 'dbms.cypher.forbid_shortestpath_common_nodes' to false."
+
+ const result =
+ formatDescriptionFromGqlStatusDescription(gqlStatusDescription)
+
+ expect(result).toEqual(
+ "Cannot find the shortest path when the start and end nodes are the same. To enable this behavior, set 'dbms.cypher.forbid_shortestpath_common_nodes' to false."
+ )
+ })
+
+ test('formats a description with no period correctly', () => {
+ const gqlStatusDescription =
+ 'error: system configuration or operation exception - cyclic shortest path search disabled. Cannot find the shortest path when the start and end nodes are the same'
+ const result =
+ formatDescriptionFromGqlStatusDescription(gqlStatusDescription)
+
+ expect(result).toEqual(
+ 'Cannot find the shortest path when the start and end nodes are the same.'
+ )
+ })
+
+ test('formats a title from a gql status description with no matches correctly', () => {
+ const gqlStatusDescription =
+ 'Unfortunately, no one can be told what the Matrix is. You have to see it for yourself'
+ const result = formatTitleFromGqlStatusDescription(gqlStatusDescription)
+
+ expect(result).toEqual('')
+ })
+
+ test('formats a description from a gql status description with no matches correctly', () => {
+ const gqlStatusDescription = 'Believe the unbelievable'
+ const result =
+ formatDescriptionFromGqlStatusDescription(gqlStatusDescription)
+
+ expect(result).toEqual('')
+ })
+})
diff --git a/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts
new file mode 100644
index 00000000000..b46c7f054ea
--- /dev/null
+++ b/src/browser/modules/Stream/CypherFrame/gqlStatusUtils.ts
@@ -0,0 +1,45 @@
+import { capitalize, isNonEmptyString } from 'shared/utils/strings'
+
+const gqlStatusIndexes = {
+ title: 1,
+ description: 2
+}
+
+const formatPropertyFromStatusDescripton = (
+ index: number,
+ gqlStatusDescription?: string
+): string | undefined => {
+ const matches =
+ gqlStatusDescription?.match(
+ /^(?:error|info|warn):\s(.+?)(?:\.(.+?))?\.?$/
+ ) ?? []
+
+ return matches[index] === undefined
+ ? undefined
+ : capitalize(matches[index].trim())
+}
+
+export const formatTitleFromGqlStatusDescription = (
+ gqlStatusDescription?: string
+): string => {
+ return (
+ formatPropertyFromStatusDescripton(
+ gqlStatusIndexes.title,
+ gqlStatusDescription
+ )?.trim() ?? ''
+ )
+}
+
+export const formatDescriptionFromGqlStatusDescription = (
+ gqlStatusDescription?: string
+): string => {
+ const description =
+ formatPropertyFromStatusDescripton(
+ gqlStatusIndexes.description,
+ gqlStatusDescription
+ )?.trim() ?? ''
+
+ return isNonEmptyString(description) && !description.endsWith('.')
+ ? `${description}.`
+ : description
+}
diff --git a/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts b/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts
new file mode 100644
index 00000000000..c5e13322f1a
--- /dev/null
+++ b/src/browser/modules/Stream/CypherFrame/warningUtils.test.ts
@@ -0,0 +1,146 @@
+import type { GqlStatusObject } from 'neo4j-driver-core'
+import {
+ notificationCategory,
+ notificationSeverityLevel
+} from 'neo4j-driver-core'
+import { formatNotificationsFromSummary } from './warningUtilts'
+
+describe('format rseult summary', () => {
+ test('formats result summary for notifications', () => {
+ const resultSummary = {
+ server: {
+ protocolVersion: 5.5
+ },
+ notifications: [
+ {
+ code: 'Neo.ClientNotification.Statement.CartesianProduct',
+ title:
+ 'This query builds a cartesian product between disconnected patterns.',
+ description:
+ 'If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifiers are: ())',
+ severity: 'INFORMATION',
+ position: { offset: 0, line: 1, column: 1 },
+ severityLevel: notificationSeverityLevel.INFORMATION,
+ rawSeverityLevel: 'INFORMATION',
+ category: notificationCategory.PERFORMANCE,
+ rawCategory: 'PERFORMANCE'
+ },
+ {
+ code: 'Neo.ClientNotification.Statement.UnknownLabelWarning',
+ title: 'The provided label is not in the database.',
+ description:
+ "One of the labels in your query is not available in the database, make sure you didn't misspell it or that the label is available when you run this statement in your application (the missing label name is: A)",
+ severity: 'WARNING',
+ position: { offset: 9, line: 1, column: 10 },
+ severityLevel: notificationSeverityLevel.WARNING,
+ rawSeverityLevel: 'WARNING',
+ category: notificationCategory.UNRECOGNIZED,
+ rawCategory: 'UNRECOGNIZED'
+ }
+ ]
+ }
+
+ const result = formatNotificationsFromSummary(resultSummary)
+
+ expect(result).toEqual([
+ {
+ code: 'Neo.ClientNotification.Statement.CartesianProduct',
+ description:
+ 'If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifiers are: ())',
+ position: {
+ column: 1,
+ line: 1,
+ offset: 0
+ },
+ title:
+ 'This query builds a cartesian product between disconnected patterns.',
+ severity: 'INFORMATION'
+ },
+ {
+ code: 'Neo.ClientNotification.Statement.UnknownLabelWarning',
+ description:
+ "One of the labels in your query is not available in the database, make sure you didn't misspell it or that the label is available when you run this statement in your application (the missing label name is: A)",
+ position: {
+ column: 10,
+ line: 1,
+ offset: 9
+ },
+ title: 'The provided label is not in the database.',
+ severity: 'WARNING'
+ }
+ ])
+ })
+
+ test('formats result summary for gql status objects', () => {
+ const gqlStatusObjects: [GqlStatusObject, ...GqlStatusObject[]] = [
+ {
+ gqlStatus: '03N90',
+ statusDescription:
+ "info: cartesian product. The disconnected pattern 'p = ()--(), q = ()--()' builds a cartesian product. A cartesian product may produce a large amount of data and slow down query processing.",
+ diagnosticRecord: {
+ OPERATION: '',
+ OPERATION_CODE: '0',
+ CURRENT_SCHEMA: '/',
+ classification: 'PERFORMANCE'
+ },
+ position: { offset: 0, line: 1, column: 1 },
+ severity: 'INFORMATION',
+ rawSeverity: 'INFORMATION',
+ classification: 'PERFORMANCE',
+ rawClassification: 'PERFORMANCE',
+ isNotification: true,
+ diagnosticRecordAsJsonString: ''
+ },
+ {
+ gqlStatus: '01N50',
+ statusDescription:
+ 'warn: label does not exist. The label `A` does not exist. Verify that the spelling is correct.',
+ diagnosticRecord: {
+ OPERATION: '',
+ OPERATION_CODE: '0',
+ CURRENT_SCHEMA: '/'
+ },
+ position: { offset: 9, line: 1, column: 10 },
+ severity: 'WARNING',
+ rawSeverity: 'WARNING',
+ classification: 'UNRECOGNIZED',
+ rawClassification: 'UNRECOGNIZED',
+ isNotification: true,
+ diagnosticRecordAsJsonString: ''
+ }
+ ]
+ const resultSummary = {
+ server: {
+ protocolVersion: 5.7
+ },
+ gqlStatusObjects
+ }
+
+ const result = formatNotificationsFromSummary(resultSummary)
+
+ expect(result).toEqual([
+ {
+ description:
+ "The disconnected pattern 'p = ()--(), q = ()--()' builds a cartesian product. A cartesian product may produce a large amount of data and slow down query processing.",
+ position: {
+ column: 1,
+ line: 1,
+ offset: 0
+ },
+ title: '03N90: Cartesian product',
+ severity: 'INFORMATION'
+ },
+ {
+ description:
+ 'The label `A` does not exist. Verify that the spelling is correct.',
+ position: {
+ column: 10,
+ line: 1,
+ offset: 9
+ },
+ title: '01N50: Label does not exist',
+ severity: 'WARNING'
+ }
+ ])
+ })
+})
diff --git a/src/browser/modules/Stream/CypherFrame/warningUtilts.ts b/src/browser/modules/Stream/CypherFrame/warningUtilts.ts
new file mode 100644
index 00000000000..67a0e61077d
--- /dev/null
+++ b/src/browser/modules/Stream/CypherFrame/warningUtilts.ts
@@ -0,0 +1,74 @@
+import type { Notification, NotificationSeverityLevel } from 'neo4j-driver'
+import {
+ GqlStatusObject,
+ NotificationPosition,
+ ResultSummary
+} from 'neo4j-driver-core'
+import {
+ formatDescriptionFromGqlStatusDescription,
+ formatTitleFromGqlStatusDescription
+} from './gqlStatusUtils'
+import { isNonEmptyString } from 'shared/utils/strings'
+
+export type FormattedNotification = {
+ title?: string
+ description: string
+ position?: NotificationPosition
+ code?: string | null
+ severity?: NotificationSeverityLevel | string
+}
+
+const mapGqlStatusObjectsToFormattedNotifications = (
+ statusObjects: Omit
[]
+): FormattedNotification[] => {
+ return statusObjects.map(statusObject => {
+ const gqlStatusTitle = formatTitleFromGqlStatusDescription(
+ statusObject.statusDescription
+ )
+ const { gqlStatus } = statusObject
+ const description = formatDescriptionFromGqlStatusDescription(
+ statusObject.statusDescription
+ )
+ const title = isNonEmptyString(gqlStatusTitle)
+ ? gqlStatusTitle
+ : description
+ return {
+ title: isNonEmptyString(title) ? `${gqlStatus}: ${title}` : gqlStatus,
+ description,
+ position: statusObject.position,
+ severity: statusObject.severity
+ }
+ })
+}
+
+const mapNotificationsToFormattedNotifications = (
+ notifications: Notification[]
+): FormattedNotification[] => {
+ return notifications.map(notification => ({
+ title: notification.title,
+ description: notification.description,
+ position: notification.position,
+ severity: notification.severity,
+ code: notification.code
+ }))
+}
+
+export const formatNotificationsFromSummary = (
+ resultSummary: Partial
+): FormattedNotification[] => {
+ const { protocolVersion } = resultSummary.server ?? {}
+ const severityLevels = ['ERROR', 'WARNING', 'INFORMATION']
+ if (protocolVersion === undefined || protocolVersion < 5.6) {
+ const filteredNotifications =
+ resultSummary.notifications?.filter(x =>
+ severityLevels.includes(x.severity)
+ ) ?? []
+ return mapNotificationsToFormattedNotifications(filteredNotifications)
+ }
+
+ const filteredStatusObjects =
+ resultSummary.gqlStatusObjects?.filter(x =>
+ severityLevels.includes(x.severity)
+ ) ?? []
+ return mapGqlStatusObjectsToFormattedNotifications(filteredStatusObjects)
+}
diff --git a/src/shared/utils/strings.ts b/src/shared/utils/strings.ts
new file mode 100644
index 00000000000..6277a553e2f
--- /dev/null
+++ b/src/shared/utils/strings.ts
@@ -0,0 +1,5 @@
+export const capitalize = (s: string): string =>
+ s.charAt(0).toUpperCase() + s.slice(1)
+
+export const isNonEmptyString = (s: unknown): s is string =>
+ typeof s === 'string' && s !== ''