From 65b8ab0bf10b70b299a78033eb72ec393c35b68a Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:03:22 +0200 Subject: [PATCH 01/35] First draft of gql error implementation --- .../src/bolt/response-handler.js | 16 +++- .../test/bolt/response-handler.test.js | 68 +++++++++++++ .../connection/connection-channel.test.js | 4 +- packages/core/src/error.ts | 60 +++++++++++- .../core/test/__snapshots__/json.test.ts.snap | 6 +- packages/core/test/error.test.ts | 95 ++++++++++++++++--- .../bolt-connection/bolt/response-handler.js | 16 +++- packages/neo4j-driver-deno/lib/core/error.ts | 60 +++++++++++- 8 files changed, 289 insertions(+), 36 deletions(-) diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index 6944c8ed4..91fbc6895 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -115,11 +115,7 @@ export default class ResponseHandler { this._log.debug(`S: FAILURE ${json.stringify(msg)}`) } try { - const standardizedCode = _standardizeCode(payload.code) - const error = newError(payload.message, standardizedCode) - this._currentFailure = this._observer.onErrorApplyTransformation( - error - ) + this._currentFailure = this._handleErrorPayload(payload) this._currentObserver.onError(this._currentFailure) } finally { this._updateCurrentObserver() @@ -196,6 +192,16 @@ export default class ResponseHandler { _resetFailure () { this._currentFailure = null } + + _handleErrorPayload (payload) { + const standardizedCode = _standardizeCode(payload.code) + const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined + const diagnosticRecord = payload.diagnostic_record != null ? payload.diagnostic_record : undefined + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, diagnosticRecord, cause) + return this._observer.onErrorApplyTransformation( + error + ) + } } /** diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index 3c75c1b98..5199a97a5 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -69,6 +69,74 @@ describe('response-handler', () => { expect(receivedError.message).toBe(expectedError.message) expect(receivedError.code).toBe(expectedError.code) }) + + it('should correctly handle errors with gql data', () => { + const errorPayload = { + message: 'older message', + code: 'Neo.ClientError.Test.Kaboom', + gql_status: '13N37', + status_description: 'I made this error up, for fun and profit!', + diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR' } + } + const observer = { + capturedErrors: [], + onFailure: error => observer.capturedErrors.push(error) + } + const responseHandler = new ResponseHandler({ observer, log: Logger.noOp() }) + responseHandler._queueObserver({}) + + const errorMessage = { + signature: FAILURE, + fields: [errorPayload] + } + responseHandler.handleResponse(errorMessage) + + expect(observer.capturedErrors.length).toBe(1) + const [receivedError] = observer.capturedErrors + expect(receivedError.code).toBe(errorPayload.code) + expect(receivedError.message).toBe(errorPayload.message) + expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) + expect(receivedError.gqlStatusDescription).toBe(errorPayload.status_description) + expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) + }) + + it('should correctly handle errors with gql data and nested causes', () => { + const errorPayload = { + message: 'old message', + code: 'Neo.ClientError.Test.Error', + gql_status: '13N37', + status_description: 'I made this error up, for fun and profit!', + diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR' }, + cause: { + message: 'old cause message', + code: 'Neo.ClientError.Test.Cause', + gql_status: '13N38', + status_description: 'I made this error up, for fun and profit and reasons!', + diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'DATABASE_ERROR' } + } + } + const observer = { + capturedErrors: [], + onFailure: error => observer.capturedErrors.push(error) + } + const responseHandler = new ResponseHandler({ observer, log: Logger.noOp() }) + responseHandler._queueObserver({}) + + const errorMessage = { + signature: FAILURE, + fields: [errorPayload] + } + responseHandler.handleResponse(errorMessage) + + expect(observer.capturedErrors.length).toBe(1) + const [receivedError] = observer.capturedErrors + expect(receivedError.code).toBe(errorPayload.code) + expect(receivedError.message).toBe(errorPayload.message) + expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) + expect(receivedError.gqlStatusDescription).toBe(errorPayload.status_description) + expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) + expect(receivedError.cause.classification).toBe(errorPayload.cause.diagnostic_record._classification) + }) }) it('should keep track of observers and notify onObserversCountChange()', () => { diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 95edaa5bb..9f8f0b908 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -371,7 +371,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: some error ` + - '({"code":"C","name":"Neo4jError","retriable":false})' + '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) @@ -419,7 +419,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: current failure ` + - '({"code":"ongoing","name":"Neo4jError","retriable":false})' + '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index d391e7fc2..a7772e906 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -17,6 +17,13 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. +import { NumberOrInteger } from './graph-types' +import * as json from './json' + +export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' +/** + * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification + */ /** * Error code representing complete loss of service. Used by {@link Neo4jError#code}. @@ -60,6 +67,11 @@ class Neo4jError extends Error { * Optional error code. Will be populated when error originates in the database. */ code: Neo4jErrorCode + gqlStatus: string + gqlStatusDescription: string + diagnosticRecord: ErrorDiagnosticRecord | undefined + classification: ErrorClassification + cause?: Error retriable: boolean __proto__: Neo4jError @@ -67,8 +79,11 @@ class Neo4jError extends Error { * @constructor * @param {string} message - the error message * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {string} gqlStatus - the error message + * @param {string} gqlStatusDescription - the error message + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message */ - constructor (message: string, code: Neo4jErrorCode, cause?: Error) { + constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Error) { // eslint-disable-next-line // @ts-ignore: not available in ES6 yet super(message, cause != null ? { cause } : undefined) @@ -76,6 +91,10 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code + this.gqlStatus = gqlStatus + this.gqlStatusDescription = gqlStatusDescription + this.diagnosticRecord = diagnosticRecord + this.classification = diagnosticRecord?._classification ?? 'UNKNOWN' this.name = 'Neo4jError' /** * Indicates if the error is retriable. @@ -96,17 +115,35 @@ class Neo4jError extends Error { error instanceof Neo4jError && error.retriable } + + /** + * 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, { useCustomToString: true }) + } } /** * Create a new error from a message and error code * @param message the error message - * @param code the error code + * @param {Neo4jErrorCode} [code] the error code + * @param {String} [gqlStatus] + * @param {String} [gqlStatusDescription] + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {Neo4jError} [cause] * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, cause?: Error): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, cause) +function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { + if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { + diagnosticRecord = undefined + } + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'general processing exception - unknown error. ' + message, diagnosticRecord, cause) } /** @@ -148,6 +185,21 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +interface ErrorDiagnosticRecord { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: ErrorClassification + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record + [key: string]: unknown +} + export { newError, isRetriableError, diff --git a/packages/core/test/__snapshots__/json.test.ts.snap b/packages/core/test/__snapshots__/json.test.ts.snap index 14abb06b7..36e9172aa 100644 --- a/packages/core/test/__snapshots__/json.test.ts.snap +++ b/packages/core/test/__snapshots__/json.test.ts.snap @@ -102,11 +102,11 @@ exports[`json .stringify should handle object with custom toString in list 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 1`] = `"{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","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 in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","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 objects created with createBrokenObject inside other object 1`] = `"{"number":1,"broken":{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}}"`; exports[`json .stringify should handle string 1`] = `""my string""`; diff --git a/packages/core/test/error.test.ts b/packages/core/test/error.test.ts index 3b628d8a5..adcf073e6 100644 --- a/packages/core/test/error.test.ts +++ b/packages/core/test/error.test.ts @@ -43,36 +43,93 @@ describe('newError', () => { } ) - test('should create Neo4jErro without code should be created with "N/A" error', () => { + test('should create Neo4jError without code with "N/A" error', () => { const error: Neo4jError = newError('some error') expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') }) - test('should create Neo4jErro with cause', () => { - const cause = new Error('cause') - const error: Neo4jError = newError('some error', undefined, cause) + test('should create Neo4jError without status description with default description', () => { + const error: Neo4jError = newError('some error') + + expect(error.gqlStatusDescription).toEqual('general processing exception - unknown error. some error') + expect(error.code).toEqual('N/A') + }) + + test('should create Neo4jError without gql status with default status', () => { + const error: Neo4jError = newError('some error') + + expect(error.gqlStatus).toEqual('50N42') + expect(error.code).toEqual('N/A') + }) + + test('should create Neo4jError with cause', () => { + const cause = newError('cause') + const error: Neo4jError = newError('some error', undefined, 'some status', 'some description', undefined, cause) expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') if (supportsCause) { - // @ts-expect-error expect(error.cause).toBe(cause) + // @ts-expect-error + expect(error.cause.classification).toBe('UNKNOWN') } else { + expect(error.cause).toBeUndefined() + } + }) + + test('should create Neo4jError with nested cause', () => { + const cause = newError('cause', undefined, undefined, undefined, undefined, newError('nested')) + const error: Neo4jError = newError('some error', undefined, 'some status', 'some description', undefined, cause) + + expect(error.message).toEqual('some error') + expect(error.code).toEqual('N/A') + if (supportsCause) { + expect(error.cause).toBe(cause) // @ts-expect-error + expect(error.cause.classification).toBe('UNKNOWN') + // @ts-expect-error + expect(error.cause.cause.classification).toBe('UNKNOWN') + } else { expect(error.cause).toBeUndefined() } }) test.each([null, undefined])('should create Neo4jError without cause (%s)', (cause) => { // @ts-expect-error - const error: Neo4jError = newError('some error', undefined, cause) + const error: Neo4jError = newError('some error', undefined, undefined, undefined, undefined, cause) expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') + expect(error.cause).toBeUndefined() + }) + + test('should create Neo4jError without diagnosticRecord with UNKNOWN classification', () => { + const error: Neo4jError = newError('some error') + + expect(error.classification).toEqual('UNKNOWN') + }) + + test.each([ + 'TRANSIENT_ERROR', + 'CLIENT_ERROR', + 'DATABASE_ERROR' + ])('should create Neo4jError with diagnosticRecord with classification (%s)', (classification) => { // @ts-expect-error + const error: Neo4jError = newError('some error', undefined, undefined, undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: classification }) + + expect(error.classification).toEqual(classification) + }) + + test.each([undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/' }])('should create Neo4jError without diagnostic record (%s)', (diagnosticRecord) => { + const error: Neo4jError = newError('some error', undefined, undefined, undefined, diagnosticRecord) + + expect(error.message).toEqual('some error') + expect(error.code).toEqual('N/A') expect(error.cause).toBeUndefined() + expect(error.diagnosticRecord).toBeUndefined() + expect(error.classification).toEqual('UNKNOWN') }) }) @@ -88,31 +145,43 @@ describe('isRetriableError()', () => { describe('Neo4jError', () => { test('should have message', () => { - const error = new Neo4jError('message', 'code') + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') expect(error.message).toEqual('message') }) test('should have code', () => { - const error = new Neo4jError('message', 'code') + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') expect(error.code).toEqual('code') }) + test('should have gqlStatus', () => { + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') + + expect(error.gqlStatus).toEqual('gqlStatus') + }) + + test('should have gqlStatusDescription', () => { + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') + + expect(error.gqlStatusDescription).toEqual('gqlStatusDescription') + }) + test('should have name equal to Neo4jError', () => { - const error = new Neo4jError('message', 'code') + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') expect(error.name).toEqual('Neo4jError') }) test('should define stackstrace', () => { - const error = new Neo4jError('message', 'code') + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') expect(error.stack).toBeDefined() }) test('should define __proto__ and constructor to backwards compatility with ES6', () => { - const error = new Neo4jError('message', 'code') + const error = new Neo4jError('message', 'code', 'gqlStatus', 'gqlStatusDescription') // eslint-disable-next-line no-proto expect(error.__proto__).toEqual(Neo4jError.prototype) @@ -120,13 +189,13 @@ describe('Neo4jError', () => { }) test.each(getRetriableCodes())('should define retriable as true for error with code %s', code => { - const error = new Neo4jError('message', code) + const error = new Neo4jError('message', code, 'gqlStatus', 'gqlStatusDescription') expect(error.retriable).toBe(true) }) test.each(getNonRetriableCodes())('should define retriable as false for error with code %s', code => { - const error = new Neo4jError('message', code) + const error = new Neo4jError('message', code, 'gqlStatus', 'gqlStatusDescription') expect(error.retriable).toBe(false) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index 8a0aeddbf..71cff46f1 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -115,11 +115,7 @@ export default class ResponseHandler { this._log.debug(`S: FAILURE ${json.stringify(msg)}`) } try { - const standardizedCode = _standardizeCode(payload.code) - const error = newError(payload.message, standardizedCode) - this._currentFailure = this._observer.onErrorApplyTransformation( - error - ) + this._currentFailure = this._handleErrorPayload(payload) this._currentObserver.onError(this._currentFailure) } finally { this._updateCurrentObserver() @@ -196,6 +192,16 @@ export default class ResponseHandler { _resetFailure () { this._currentFailure = null } + + _handleErrorPayload (payload) { + const standardizedCode = _standardizeCode(payload.code) + const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined + const diagnosticRecord = payload.diagnostic_record != null ? payload.diagnostic_record : undefined + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, diagnosticRecord, cause) + return this._observer.onErrorApplyTransformation( + error + ) + } } /** diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index d391e7fc2..3465e3f99 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -17,6 +17,13 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. +import { NumberOrInteger } from './graph-types.ts' +import * as json from './json.ts' + +export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' +/** + * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification + */ /** * Error code representing complete loss of service. Used by {@link Neo4jError#code}. @@ -60,6 +67,11 @@ class Neo4jError extends Error { * Optional error code. Will be populated when error originates in the database. */ code: Neo4jErrorCode + gqlStatus: string + gqlStatusDescription: string + diagnosticRecord: ErrorDiagnosticRecord | undefined + classification: ErrorClassification + cause?: Error retriable: boolean __proto__: Neo4jError @@ -67,8 +79,11 @@ class Neo4jError extends Error { * @constructor * @param {string} message - the error message * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {string} gqlStatus - the error message + * @param {string} gqlStatusDescription - the error message + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message */ - constructor (message: string, code: Neo4jErrorCode, cause?: Error) { + constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Error) { // eslint-disable-next-line // @ts-ignore: not available in ES6 yet super(message, cause != null ? { cause } : undefined) @@ -76,6 +91,10 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code + this.gqlStatus = gqlStatus + this.gqlStatusDescription = gqlStatusDescription + this.diagnosticRecord = diagnosticRecord + this.classification = diagnosticRecord?._classification ?? "UNKNOWN" this.name = 'Neo4jError' /** * Indicates if the error is retriable. @@ -96,17 +115,35 @@ class Neo4jError extends Error { error instanceof Neo4jError && error.retriable } + + /** + * 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, { useCustomToString: true }) + } } /** * Create a new error from a message and error code * @param message the error message - * @param code the error code + * @param {Neo4jErrorCode} [code] the error code + * @param {String} [gqlStatus] + * @param {String} [gqlStatusDescription] + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {Neo4jError} [cause] * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, cause?: Error): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, cause) +function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { + if(diagnosticRecord && Object.keys(diagnosticRecord).length == 3 && diagnosticRecord.OPERATION == "" && diagnosticRecord.OPERATION_CODE == "0" && diagnosticRecord.CURRENT_SCHEMA == "/" ) { + diagnosticRecord = undefined + } + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? "50N42", gqlStatusDescription ?? "general processing exception - unknown error. " + message, diagnosticRecord, cause) } /** @@ -148,6 +185,21 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +interface ErrorDiagnosticRecord { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: ErrorClassification + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record + [key: string]: unknown +} + export { newError, isRetriableError, From ba9b85b1e78ba3820971fe36a79ce2f196b40ac0 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:30:37 +0200 Subject: [PATCH 02/35] deno build --- packages/neo4j-driver-deno/lib/core/error.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 3465e3f99..8fc73ec95 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -94,7 +94,7 @@ class Neo4jError extends Error { this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord - this.classification = diagnosticRecord?._classification ?? "UNKNOWN" + this.classification = diagnosticRecord?._classification ?? 'UNKNOWN' this.name = 'Neo4jError' /** * Indicates if the error is retriable. @@ -140,10 +140,10 @@ class Neo4jError extends Error { * @private */ function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { - if(diagnosticRecord && Object.keys(diagnosticRecord).length == 3 && diagnosticRecord.OPERATION == "" && diagnosticRecord.OPERATION_CODE == "0" && diagnosticRecord.CURRENT_SCHEMA == "/" ) { + if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { diagnosticRecord = undefined } - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? "50N42", gqlStatusDescription ?? "general processing exception - unknown error. " + message, diagnosticRecord, cause) + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'general processing exception - unknown error. ' + message, diagnosticRecord, cause) } /** From f4cfeb29d66d2068577a9a975c813dd921af862d Mon Sep 17 00:00:00 2001 From: Max Gustafsson <61233757+MaxAake@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:01:47 +0200 Subject: [PATCH 03/35] Merge 5.0 into gql-errors Browser version of testkit was running firefox with a DNS cache, which was liable to cause failures of mixing up the cores, as they do not always keep their IP addresses --- .gitignore | 1 + testkit/backend.py | 3 ++- testkit/firefox_profile/prefs.js | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 testkit/firefox_profile/prefs.js diff --git a/.gitignore b/.gitignore index dab77719f..70b2502cf 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ coverage *.code-workspace /testkit/CAs /testkit/CustomCAs +/testkit/firefox_profile/* diff --git a/testkit/backend.py b/testkit/backend.py index 7e1bb6784..5f6c1b5cb 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -12,6 +12,7 @@ import os import time + if __name__ == "__main__": print("starting backend") backend_script = "start-testkit-backend" @@ -34,7 +35,7 @@ time.sleep(5) print("openning firefox") with open_proccess_in_driver_repo([ - "firefox", "-headless", "http://localhost:8000" + "firefox", "--profile", "./testkit/firefox_profile", "--headless", "http://localhost:8000" # type: ignore ]) as firefox: firefox.wait() backend.wait() diff --git a/testkit/firefox_profile/prefs.js b/testkit/firefox_profile/prefs.js new file mode 100644 index 000000000..2f8b5160b --- /dev/null +++ b/testkit/firefox_profile/prefs.js @@ -0,0 +1,3 @@ +/* global user_pref */ +user_pref('network.dnsCacheExpiration', 0); +user_pref('network.dnsCacheExpirationGracePeriod', 0); From 8bec197fd6e9fa572995bd4f804ca44d765474b9 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:50:29 +0200 Subject: [PATCH 04/35] Updates to support upcoming bolt version 5.7 --- .../src/bolt/bolt-protocol-v5x6.js | 2 +- .../src/bolt/bolt-protocol-v5x7.js | 55 +++++++++++++++++++ .../bolt/bolt-protocol-v5x7.transformer.js | 22 ++++++++ packages/bolt-connection/src/bolt/create.js | 9 +++ .../bolt-connection/src/bolt/handshake.js | 2 +- .../src/bolt/response-handler.js | 3 +- .../bolt-connection/test/bolt/index.test.js | 2 +- .../bolt/bolt-protocol-v5x6.js | 2 +- .../bolt/bolt-protocol-v5x7.js | 55 +++++++++++++++++++ .../bolt/bolt-protocol-v5x7.transformer.js | 22 ++++++++ .../lib/bolt-connection/bolt/create.js | 9 +++ .../lib/bolt-connection/bolt/handshake.js | 2 +- .../bolt-connection/bolt/response-handler.js | 3 +- .../testkit-backend/src/feature/common.js | 1 + 14 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v5x7.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.transformer.js diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js index d1177d28b..a54c38360 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js @@ -31,7 +31,7 @@ const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ CURRENT_SCHEMA: '/' }) -export default class BoltProtocol extends BoltProtocolV5x5 { +export default class BoltProtocolV5x6 extends BoltProtocolV5x5 { get version () { return BOLT_PROTOCOL_V5_6 } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js new file mode 100644 index 000000000..d73e413b1 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js @@ -0,0 +1,55 @@ +/** + * 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 BoltProtocolV5x6 from './bolt-protocol-v5x6' + +import transformersFactories from './bolt-protocol-v5x5.transformer' +import Transformer from './transformer' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V5_7 } +} = internal + +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + +export default class BoltProtocol extends BoltProtocolV5x6 { + get version () { + return BOLT_PROTOCOL_V5_7 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null, + message: status.status_message + })) + } + } +} diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.transformer.js new file mode 100644 index 000000000..96fe5565b --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.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 v5x6 from './bolt-protocol-v5x6.transformer' + +export default { + ...v5x6 +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index fa8b41098..f893c5e9e 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -31,6 +31,7 @@ import BoltProtocolV5x3 from './bolt-protocol-v5x3' import BoltProtocolV5x4 from './bolt-protocol-v5x4' import BoltProtocolV5x5 from './bolt-protocol-v5x5' import BoltProtocolV5x6 from './bolt-protocol-v5x6' +import BoltProtocolV5x7 from './bolt-protocol-v5x7' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -247,6 +248,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.7: + return new BoltProtocolV5x7(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 00f08e8e5..abf318025 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, 6), version(5, 0)], + [version(5, 7), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index 91fbc6895..3758b1e5d 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -196,8 +196,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const diagnosticRecord = payload.diagnostic_record != null ? payload.diagnostic_record : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, diagnosticRecord, cause) + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, payload.diagnostic_record, cause) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index b70a18c26..e0c81a954 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -48,7 +48,7 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x6to5x0 = '00 06 06 05' + const protocolVersion5x6to5x0 = '00 07 07 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js index ea5bd840b..fa2dfc385 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js @@ -31,7 +31,7 @@ const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ CURRENT_SCHEMA: '/' }) -export default class BoltProtocol extends BoltProtocolV5x5 { +export default class BoltProtocolV5x6 extends BoltProtocolV5x5 { get version () { return BOLT_PROTOCOL_V5_6 } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js new file mode 100644 index 000000000..0e813da15 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js @@ -0,0 +1,55 @@ +/** + * 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 BoltProtocolV5x6 from './bolt-protocol-v5x6.js' + +import transformersFactories from './bolt-protocol-v5x5.transformer.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V5_7 } +} = internal + +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + +export default class BoltProtocol extends BoltProtocolV5x6 { + get version () { + return BOLT_PROTOCOL_V5_7 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null, + message: status.status_message + })) + } + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.transformer.js new file mode 100644 index 000000000..ff74e0120 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.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 v5x6 from './bolt-protocol-v5x6.transformer.js' + +export default { + ...v5x6 +} 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 54bfef412..cfa1d5c03 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -31,6 +31,7 @@ import BoltProtocolV5x3 from './bolt-protocol-v5x3.js' import BoltProtocolV5x4 from './bolt-protocol-v5x4.js' import BoltProtocolV5x5 from './bolt-protocol-v5x5.js' import BoltProtocolV5x6 from './bolt-protocol-v5x6.js' +import BoltProtocolV5x7 from './bolt-protocol-v5x7.js' // eslint-disable-next-line no-unused-vars import { Chunker, Dechunker } from '../channel/index.js' import ResponseHandler from './response-handler.js' @@ -247,6 +248,14 @@ function createProtocol ( log, onProtocolError, serversideRouting) + case 5.7: + return new BoltProtocolV5x7(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 38354274f..c91e0e18f 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, 6), version(5, 0)], + [version(5, 7), version(5, 0)], [version(4, 4), version(4, 2)], version(4, 1), version(3, 0) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index 71cff46f1..9bb1d9f8b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -196,8 +196,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const diagnosticRecord = payload.diagnostic_record != null ? payload.diagnostic_record : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, diagnosticRecord, cause) + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, payload.diagnostic_record, cause) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 756bcc92a..a56917f96 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -26,6 +26,7 @@ const features = [ 'Feature:Bolt:5.4', 'Feature:Bolt:5.5', 'Feature:Bolt:5.6', + 'Feature:Bolt:5.7', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', From a0833c54a1cdc2ecb87e317e9e81d9839e1eddd4 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:01:19 +0200 Subject: [PATCH 05/35] disable bolt 5.7 testkit feature --- packages/testkit-backend/src/feature/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index a56917f96..280d501de 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -26,7 +26,7 @@ const features = [ 'Feature:Bolt:5.4', 'Feature:Bolt:5.5', 'Feature:Bolt:5.6', - 'Feature:Bolt:5.7', + // 'Feature:Bolt:5.7', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', From 8dcb0e08697b91ccf5bc90c87a7f317fba27220f Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 24 Sep 2024 08:56:53 +0200 Subject: [PATCH 06/35] bolt 5.7 and connection to testkit --- .../src/bolt/bolt-protocol-v5x7.js | 16 --------- .../src/bolt/response-handler.js | 2 +- packages/core/src/error.ts | 2 +- .../bolt/bolt-protocol-v5x7.js | 16 --------- .../bolt-connection/bolt/response-handler.js | 2 +- packages/neo4j-driver-deno/lib/core/error.ts | 2 +- packages/testkit-backend/deno/controller.ts | 33 ++++++++++++------- .../testkit-backend/src/controller/local.js | 27 +++++++++++---- .../testkit-backend/src/feature/common.js | 2 +- testkit/testkit.json | 2 +- 10 files changed, 47 insertions(+), 57 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js index d73e413b1..6d9ec641b 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js @@ -25,12 +25,6 @@ const { constants: { BOLT_PROTOCOL_V5_7 } } = internal -const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/' -}) - export default class BoltProtocol extends BoltProtocolV5x6 { get version () { return BOLT_PROTOCOL_V5_7 @@ -42,14 +36,4 @@ export default class BoltProtocol extends BoltProtocolV5x6 { } return this._transformer } - - _enrichMetadata (metadata) { - if (Array.isArray(metadata.statuses)) { - metadata.statuses = metadata.statuses.map(status => ({ - ...status, - diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null, - message: status.status_message - })) - } - } } diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index 3758b1e5d..af2b7fe26 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -196,7 +196,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, payload.diagnostic_record, cause) + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.description, payload.diagnostic_record, cause) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index a7772e906..1498981d0 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -143,7 +143,7 @@ function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, g if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { diagnosticRecord = undefined } - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'general processing exception - unknown error. ' + message, diagnosticRecord, cause) + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js index 0e813da15..8ba6e9f69 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js @@ -25,12 +25,6 @@ const { constants: { BOLT_PROTOCOL_V5_7 } } = internal -const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/' -}) - export default class BoltProtocol extends BoltProtocolV5x6 { get version () { return BOLT_PROTOCOL_V5_7 @@ -42,14 +36,4 @@ export default class BoltProtocol extends BoltProtocolV5x6 { } return this._transformer } - - _enrichMetadata (metadata) { - if (Array.isArray(metadata.statuses)) { - metadata.statuses = metadata.statuses.map(status => ({ - ...status, - diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null, - message: status.status_message - })) - } - } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index 9bb1d9f8b..2b350feed 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -196,7 +196,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.status_description, payload.diagnostic_record, cause) + const error = newError(payload.message, standardizedCode, payload.gql_status, payload.description, payload.diagnostic_record, cause) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 8fc73ec95..faa51d441 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -143,7 +143,7 @@ function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, g if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { diagnosticRecord = undefined } - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'general processing exception - unknown error. ' + message, diagnosticRecord, cause) + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } /** diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index b0d8385d7..f319a5d3e 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -30,18 +30,7 @@ function newWire(context: Context, reply: Reply): Wire { }); } else { const id = context.addError(e); - return reply({ - name: "DriverError", - data: { - id, - errorType: e.name, - msg: e.message, - // @ts-ignore Code Neo4jError does have code - code: e.code, - // @ts-ignore Code Neo4jError does retryable - retryable: e.retriable, - }, - }); + return reply(writeDriverError(id, e)); } } const msg = e.message; @@ -85,6 +74,26 @@ export function createHandler( }; } +function writeDriverError(id, e) { + let cause; + if (e.cause != null && e.cause != null) { + cause = writeDriverError(id, e.cause); + } + return { + name: "DriverError", + id, + errorType: e.name, + msg: e.message, + code: e.code, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: e.diagnosticRecord, + cause, + classification: e.classification, + retryable: e.retriable, + }; +} + export default { createHandler, }; diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index 8cc0b3e64..b46cc5cfa 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -67,13 +67,7 @@ export default class LocalController extends Controller { })) } else { const id = this._contexts.get(contextId).addError(e) - this._writeResponse(contextId, newResponse('DriverError', { - id, - errorType: e.name, - msg: e.message, - code: e.code, - retryable: e.retriable - })) + this._writeResponse(contextId, writeDriverError(id, e)) } return } @@ -86,3 +80,22 @@ function newResponse (name, data) { name, data } } + +function writeDriverError (id, e) { + let cause + if (e.cause != null && e.cause != null) { + cause = writeDriverError(id, e.cause) + } + return newResponse('DriverError', { + id, + errorType: e.name, + msg: e.message, + code: e.code, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: e.diagnosticRecord, + cause, + classification: e.classification, + retryable: e.retriable + }) +} diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 280d501de..a56917f96 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -26,7 +26,7 @@ const features = [ 'Feature:Bolt:5.4', 'Feature:Bolt:5.5', 'Feature:Bolt:5.6', - // 'Feature:Bolt:5.7', + 'Feature:Bolt:5.7', 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver.ExecuteQuery', diff --git a/testkit/testkit.json b/testkit/testkit.json index 931900356..f56584673 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "5.0" + "ref": "24c9fee5e33850af9a1c4a28151e8869a251bcd0" } } From b6c0dd9e42269060448f7acb169aa8af90774c0c Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:19:12 +0200 Subject: [PATCH 07/35] Update testkit.json --- testkit/testkit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testkit/testkit.json b/testkit/testkit.json index f56584673..931900356 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "24c9fee5e33850af9a1c4a28151e8869a251bcd0" + "ref": "5.0" } } From 9e3608412f3837a074f7a617a5a27311a03b5bf8 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:09:38 +0200 Subject: [PATCH 08/35] Update of diagnostic record and bump to latest testkit --- packages/core/src/error.ts | 3 --- testkit/testkit.json | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 1498981d0..550a86ada 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -140,9 +140,6 @@ class Neo4jError extends Error { * @private */ function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { - if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { - diagnosticRecord = undefined - } return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } diff --git a/testkit/testkit.json b/testkit/testkit.json index 931900356..e72222053 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "5.0" + "ref": "d1af132bbac07f5a980f655c00751f5beefca73b" } } From 2b13e3b836cdea05d9959bd9ad9da80019a98baa Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:13:54 +0200 Subject: [PATCH 09/35] handle rename of code to neo4j_code in 5.7 --- .../src/bolt/bolt-protocol-v5x7.js | 23 +++++++++++++++++++ .../bolt/bolt-protocol-v5x7.js | 23 +++++++++++++++++++ packages/neo4j-driver-deno/lib/core/error.ts | 3 --- testkit/testkit.json | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js index 6d9ec641b..985e46749 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js @@ -25,6 +25,12 @@ const { constants: { BOLT_PROTOCOL_V5_7 } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + export default class BoltProtocol extends BoltProtocolV5x6 { get version () { return BOLT_PROTOCOL_V5_7 @@ -36,4 +42,21 @@ export default class BoltProtocol extends BoltProtocolV5x6 { } return this._transformer } + + /** + * + * @param {object} metadata + * @returns {object} + */ + _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + code: status.neo4j_code, + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null + })) + } + + return metadata + } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js index 8ba6e9f69..24fcf13aa 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js @@ -25,6 +25,12 @@ const { constants: { BOLT_PROTOCOL_V5_7 } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + export default class BoltProtocol extends BoltProtocolV5x6 { get version () { return BOLT_PROTOCOL_V5_7 @@ -36,4 +42,21 @@ export default class BoltProtocol extends BoltProtocolV5x6 { } return this._transformer } + + /** + * + * @param {object} metadata + * @returns {object} + */ + _enrichMetadata (metadata) { + if (Array.isArray(metadata.statuses)) { + metadata.statuses = metadata.statuses.map(status => ({ + ...status, + code: status.neo4j_code, + diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null + })) + } + + return metadata + } } diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index faa51d441..15f82ce27 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -140,9 +140,6 @@ class Neo4jError extends Error { * @private */ function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { - if ((diagnosticRecord != null) && Object.keys(diagnosticRecord).length === 3 && diagnosticRecord.OPERATION === '' && diagnosticRecord.OPERATION_CODE === '0' && diagnosticRecord.CURRENT_SCHEMA === '/') { - diagnosticRecord = undefined - } return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } diff --git a/testkit/testkit.json b/testkit/testkit.json index e72222053..d30868f0e 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "d1af132bbac07f5a980f655c00751f5beefca73b" + "ref": "74e32aed8ce7890997cc7f94118a44e556fc2c02" } } From bd3bb56abb8fed82bd380116b0a054f8f54039a7 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:31:14 +0200 Subject: [PATCH 10/35] expanded support for bolt 5x7 --- .../src/bolt/bolt-protocol-v1.js | 13 +++++++++ .../src/bolt/bolt-protocol-v5x7.js | 15 +++++------ packages/bolt-connection/src/bolt/create.js | 1 + .../src/bolt/response-handler.js | 5 ++-- packages/core/src/error.ts | 21 ++++++++++++++- .../bolt-connection/bolt/bolt-protocol-v1.js | 13 +++++++++ .../bolt/bolt-protocol-v5x7.js | 15 +++++------ .../lib/bolt-connection/bolt/create.js | 1 + .../bolt-connection/bolt/response-handler.js | 5 ++-- packages/neo4j-driver-deno/lib/core/error.ts | 22 ++++++++++++++- .../testkit-backend/src/controller/local.js | 27 +++++++++++++++---- testkit/testkit.json | 2 +- 12 files changed, 110 insertions(+), 30 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 51a65de2d..d04b33b4e 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -45,6 +45,12 @@ const { txConfig: { TxConfig } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + export default class BoltProtocol { /** * @callback CreateResponseHandler Creates the response handler @@ -164,6 +170,13 @@ export default class BoltProtocol { return metadata } + enrichErrorMetadata (metadata) { + return { + ...metadata, + diagnostic_record: metadata.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...metadata.diagnostic_record } : null + } + } + /** * Perform initialization and authentication of the underlying connection. * @param {Object} param diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js index 985e46749..719e3b1bc 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x7.js @@ -48,15 +48,12 @@ export default class BoltProtocol extends BoltProtocolV5x6 { * @param {object} metadata * @returns {object} */ - _enrichMetadata (metadata) { - if (Array.isArray(metadata.statuses)) { - metadata.statuses = metadata.statuses.map(status => ({ - ...status, - code: status.neo4j_code, - diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null - })) + enrichErrorMetadata (metadata) { + return { + ...metadata, + cause: (metadata.cause !== null && metadata.cause !== undefined) ? this.enrichErrorMetadata(metadata.cause) : null, + code: metadata.neo4j_code, + diagnostic_record: metadata.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...metadata.diagnostic_record } : null } - - return metadata } } diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index f893c5e9e..fe204b612 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -65,6 +65,7 @@ export default function create ({ const createResponseHandler = protocol => { const responseHandler = new ResponseHandler({ transformMetadata: protocol.transformMetadata.bind(protocol), + enrichErrorMetadata: protocol.enrichErrorMetadata.bind(protocol), log, observer }) diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index af2b7fe26..79f74d9da 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -70,10 +70,11 @@ export default class ResponseHandler { * @param {Logger} log The logger * @param {ResponseHandler~Observer} observer Object which will be notified about errors */ - constructor ({ transformMetadata, log, observer } = {}) { + constructor ({ transformMetadata, enrichErrorMetadata, log, observer } = {}) { this._pendingObservers = [] this._log = log this._transformMetadata = transformMetadata || NO_OP_IDENTITY + this._enrichErrorMetadata = enrichErrorMetadata || NO_OP_IDENTITY this._observer = Object.assign( { onObserversCountChange: NO_OP, @@ -115,7 +116,7 @@ export default class ResponseHandler { this._log.debug(`S: FAILURE ${json.stringify(msg)}`) } try { - this._currentFailure = this._handleErrorPayload(payload) + this._currentFailure = this._handleErrorPayload(this._enrichErrorMetadata(payload)) this._currentObserver.onError(this._currentFailure) } finally { this._updateCurrentObserver() diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 550a86ada..6ca18175d 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -25,6 +25,16 @@ export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification */ +const errorClassification: { [key in ErrorClassification]: key } = { + DATABASE_ERROR: 'DATABASE_ERROR', + CLIENT_ERROR: 'CLIENT_ERROR', + TRANSIENT_ERROR: 'TRANSIENT_ERROR', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(errorClassification) +const classifications = Object.values(errorClassification) + /** * Error code representing complete loss of service. Used by {@link Neo4jError#code}. * @type {string} @@ -71,6 +81,7 @@ class Neo4jError extends Error { gqlStatusDescription: string diagnosticRecord: ErrorDiagnosticRecord | undefined classification: ErrorClassification + rawClassification?: string cause?: Error retriable: boolean __proto__: Neo4jError @@ -94,7 +105,8 @@ class Neo4jError extends Error { this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord - this.classification = diagnosticRecord?._classification ?? 'UNKNOWN' + this.classification = extractClassification(this.diagnosticRecord) + this.rawClassification = diagnosticRecord?._classification ?? undefined this.name = 'Neo4jError' /** * Indicates if the error is retriable. @@ -182,6 +194,13 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { + if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { + return 'UNKNOWN' + } + return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' +} + interface ErrorDiagnosticRecord { OPERATION: string OPERATION_CODE: string diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 17cd57d04..a83f32f5a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -45,6 +45,12 @@ const { txConfig: { TxConfig } } = internal +const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +}) + export default class BoltProtocol { /** * @callback CreateResponseHandler Creates the response handler @@ -164,6 +170,13 @@ export default class BoltProtocol { return metadata } + enrichErrorMetadata (metadata) { + return { + ...metadata, + diagnostic_record: metadata.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...metadata.diagnostic_record } : null + } + } + /** * Perform initialization and authentication of the underlying connection. * @param {Object} param diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js index 24fcf13aa..9f417ab50 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x7.js @@ -48,15 +48,12 @@ export default class BoltProtocol extends BoltProtocolV5x6 { * @param {object} metadata * @returns {object} */ - _enrichMetadata (metadata) { - if (Array.isArray(metadata.statuses)) { - metadata.statuses = metadata.statuses.map(status => ({ - ...status, - code: status.neo4j_code, - diagnostic_record: status.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...status.diagnostic_record } : null - })) + enrichErrorMetadata (metadata) { + return { + ...metadata, + cause: (metadata.cause !== null && metadata.cause !== undefined) ? this.enrichErrorMetadata(metadata.cause) : null, + code: metadata.neo4j_code, + diagnostic_record: metadata.diagnostic_record !== null ? { ...DEFAULT_DIAGNOSTIC_RECORD, ...metadata.diagnostic_record } : null } - - return metadata } } 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 cfa1d5c03..7a0fe2165 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -65,6 +65,7 @@ export default function create ({ const createResponseHandler = protocol => { const responseHandler = new ResponseHandler({ transformMetadata: protocol.transformMetadata.bind(protocol), + enrichErrorMetadata: protocol.enrichErrorMetadata.bind(protocol), log, observer }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index 2b350feed..99036adf6 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -70,10 +70,11 @@ export default class ResponseHandler { * @param {Logger} log The logger * @param {ResponseHandler~Observer} observer Object which will be notified about errors */ - constructor ({ transformMetadata, log, observer } = {}) { + constructor ({ transformMetadata, enrichErrorMetadata, log, observer } = {}) { this._pendingObservers = [] this._log = log this._transformMetadata = transformMetadata || NO_OP_IDENTITY + this._enrichErrorMetadata = enrichErrorMetadata || NO_OP_IDENTITY this._observer = Object.assign( { onObserversCountChange: NO_OP, @@ -115,7 +116,7 @@ export default class ResponseHandler { this._log.debug(`S: FAILURE ${json.stringify(msg)}`) } try { - this._currentFailure = this._handleErrorPayload(payload) + this._currentFailure = this._handleErrorPayload(this._enrichErrorMetadata(payload)) this._currentObserver.onError(this._currentFailure) } finally { this._updateCurrentObserver() diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 15f82ce27..dd647bfb0 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -25,6 +25,16 @@ export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification */ +const errorClassification: { [key in ErrorClassification]: key } = { + DATABASE_ERROR: 'DATABASE_ERROR', + CLIENT_ERROR: 'CLIENT_ERROR', + TRANSIENT_ERROR: 'TRANSIENT_ERROR', + UNKNOWN: 'UNKNOWN' +} + +Object.freeze(errorClassification) +const classifications = Object.values(errorClassification) + /** * Error code representing complete loss of service. Used by {@link Neo4jError#code}. * @type {string} @@ -58,6 +68,7 @@ type Neo4jErrorCode = | typeof PROTOCOL_ERROR | typeof NOT_AVAILABLE + /// TODO: Remove definitions of this.constructor and this.__proto__ /** * Class for all errors thrown/returned by the driver. @@ -71,6 +82,7 @@ class Neo4jError extends Error { gqlStatusDescription: string diagnosticRecord: ErrorDiagnosticRecord | undefined classification: ErrorClassification + rawClassification?: string cause?: Error retriable: boolean __proto__: Neo4jError @@ -94,7 +106,8 @@ class Neo4jError extends Error { this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord - this.classification = diagnosticRecord?._classification ?? 'UNKNOWN' + this.classification = extractClassification(this.diagnosticRecord) + this.rawClassification = diagnosticRecord?._classification ?? undefined this.name = 'Neo4jError' /** * Indicates if the error is retriable. @@ -182,6 +195,13 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +function extractClassification(diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification{ + if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { + return "UNKNOWN" + } + return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' +} + interface ErrorDiagnosticRecord { OPERATION: string OPERATION_CODE: string diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index b46cc5cfa..1ebfa57f3 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -67,7 +67,7 @@ export default class LocalController extends Controller { })) } else { const id = this._contexts.get(contextId).addError(e) - this._writeResponse(contextId, writeDriverError(id, e)) + this._writeResponse(contextId, writeDriverError(id, e, this._binder)) } return } @@ -81,10 +81,10 @@ function newResponse (name, data) { } } -function writeDriverError (id, e) { +function writeDriverError (id, e, binder) { let cause - if (e.cause != null && e.cause != null) { - cause = writeDriverError(id, e.cause) + if (e.cause != null) { + cause = writeGqlError(e.cause, binder) } return newResponse('DriverError', { id, @@ -93,9 +93,26 @@ function writeDriverError (id, e) { code: e.code, gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, - diagnosticRecord: e.diagnosticRecord, + diagnosticRecord: binder.objectToCypher(e.diagnosticRecord), cause, classification: e.classification, + rawClassification: e.rawClassification, retryable: e.retriable }) } + +function writeGqlError (e, binder) { + let cause + if (e.cause != null) { + cause = writeGqlError(e.cause, binder) + } + return newResponse('GqlError', { + msg: e.message, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: binder.objectToCypher(e.diagnosticRecord), + cause, + classification: e.classification, + rawClassification: e.rawClassification + }) +} diff --git a/testkit/testkit.json b/testkit/testkit.json index d30868f0e..e31040787 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "74e32aed8ce7890997cc7f94118a44e556fc2c02" + "ref": "d0cc6959b017d716d958f47fb3c36066a81be8fa" } } From a65e3e595785ab49637341580018672f3d3f0645 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:35:03 +0200 Subject: [PATCH 11/35] change order of newError arguments to not conflict with older code --- .../src/bolt/response-handler.js | 2 +- .../test/bolt/response-handler.test.js | 10 ++++----- .../connection/connection-channel.test.js | 4 ++-- packages/core/src/error.ts | 4 ++-- .../core/test/__snapshots__/json.test.ts.snap | 6 ++--- packages/core/test/error.test.ts | 22 +++++-------------- .../bolt-connection/bolt/response-handler.js | 2 +- packages/neo4j-driver-deno/lib/core/error.ts | 13 +++++------ 8 files changed, 26 insertions(+), 37 deletions(-) diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index 79f74d9da..b46599f55 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -197,7 +197,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.description, payload.diagnostic_record, cause) + const error = newError(payload.message, standardizedCode, cause, payload.gql_status, payload.description, payload.diagnostic_record) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index 5199a97a5..534c83e70 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -75,7 +75,7 @@ describe('response-handler', () => { message: 'older message', code: 'Neo.ClientError.Test.Kaboom', gql_status: '13N37', - status_description: 'I made this error up, for fun and profit!', + description: 'I made this error up, for fun and profit!', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR' } } const observer = { @@ -96,7 +96,7 @@ describe('response-handler', () => { expect(receivedError.code).toBe(errorPayload.code) expect(receivedError.message).toBe(errorPayload.message) expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) - expect(receivedError.gqlStatusDescription).toBe(errorPayload.status_description) + expect(receivedError.gqlStatusDescription).toBe(errorPayload.description) expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) }) @@ -105,13 +105,13 @@ describe('response-handler', () => { message: 'old message', code: 'Neo.ClientError.Test.Error', gql_status: '13N37', - status_description: 'I made this error up, for fun and profit!', + description: 'I made this error up, for fun and profit!', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR' }, cause: { message: 'old cause message', code: 'Neo.ClientError.Test.Cause', gql_status: '13N38', - status_description: 'I made this error up, for fun and profit and reasons!', + description: 'I made this error up, for fun and profit and reasons!', diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'DATABASE_ERROR' } } } @@ -133,7 +133,7 @@ describe('response-handler', () => { expect(receivedError.code).toBe(errorPayload.code) expect(receivedError.message).toBe(errorPayload.message) expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) - expect(receivedError.gqlStatusDescription).toBe(errorPayload.status_description) + expect(receivedError.gqlStatusDescription).toBe(errorPayload.description) expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) expect(receivedError.cause.classification).toBe(errorPayload.cause.diagnostic_record._classification) }) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 9f8f0b908..71ac12ee6 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -371,7 +371,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: some error ` + - '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) @@ -419,7 +419,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: current failure ` + - '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 6ca18175d..b2e33e28a 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -144,14 +144,14 @@ class Neo4jError extends Error { * Create a new error from a message and error code * @param message the error message * @param {Neo4jErrorCode} [code] the error code + * @param {Neo4jError} [cause] * @param {String} [gqlStatus] * @param {String} [gqlStatusDescription] * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message - * @param {Neo4jError} [cause] * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { +function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } diff --git a/packages/core/test/__snapshots__/json.test.ts.snap b/packages/core/test/__snapshots__/json.test.ts.snap index 36e9172aa..dbef64daf 100644 --- a/packages/core/test/__snapshots__/json.test.ts.snap +++ b/packages/core/test/__snapshots__/json.test.ts.snap @@ -102,11 +102,11 @@ exports[`json .stringify should handle object with custom toString in list 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","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}"`; +exports[`json .stringify should handle objects created with createBrokenObject 1`] = `"{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}"`; -exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}]"`; +exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. some error","classification":"UNKNOWN","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","gqlStatus":"50N42","gqlStatusDescription":"general processing exception - unknown error. some error","classification":"UNKNOWN","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","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}}"`; exports[`json .stringify should handle string 1`] = `""my string""`; diff --git a/packages/core/test/error.test.ts b/packages/core/test/error.test.ts index adcf073e6..17a641079 100644 --- a/packages/core/test/error.test.ts +++ b/packages/core/test/error.test.ts @@ -53,7 +53,7 @@ describe('newError', () => { test('should create Neo4jError without status description with default description', () => { const error: Neo4jError = newError('some error') - expect(error.gqlStatusDescription).toEqual('general processing exception - unknown error. some error') + expect(error.gqlStatusDescription).toEqual('error: general processing exception - unknown error. some error') expect(error.code).toEqual('N/A') }) @@ -66,7 +66,7 @@ describe('newError', () => { test('should create Neo4jError with cause', () => { const cause = newError('cause') - const error: Neo4jError = newError('some error', undefined, 'some status', 'some description', undefined, cause) + const error: Neo4jError = newError('some error', undefined, cause, 'some status', 'some description', undefined) expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') @@ -80,8 +80,8 @@ describe('newError', () => { }) test('should create Neo4jError with nested cause', () => { - const cause = newError('cause', undefined, undefined, undefined, undefined, newError('nested')) - const error: Neo4jError = newError('some error', undefined, 'some status', 'some description', undefined, cause) + const cause = newError('cause', undefined, newError('nested'), undefined, undefined, undefined) + const error: Neo4jError = newError('some error', undefined, cause, 'some status', 'some description', undefined) expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') @@ -98,7 +98,7 @@ describe('newError', () => { test.each([null, undefined])('should create Neo4jError without cause (%s)', (cause) => { // @ts-expect-error - const error: Neo4jError = newError('some error', undefined, undefined, undefined, undefined, cause) + const error: Neo4jError = newError('some error', undefined, cause, undefined, undefined, undefined) expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') @@ -117,20 +117,10 @@ describe('newError', () => { 'DATABASE_ERROR' ])('should create Neo4jError with diagnosticRecord with classification (%s)', (classification) => { // @ts-expect-error - const error: Neo4jError = newError('some error', undefined, undefined, undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: classification }) + const error: Neo4jError = newError('some error', undefined, undefined, undefined, undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: classification }) expect(error.classification).toEqual(classification) }) - - test.each([undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/' }])('should create Neo4jError without diagnostic record (%s)', (diagnosticRecord) => { - const error: Neo4jError = newError('some error', undefined, undefined, undefined, diagnosticRecord) - - expect(error.message).toEqual('some error') - expect(error.code).toEqual('N/A') - expect(error.cause).toBeUndefined() - expect(error.diagnosticRecord).toBeUndefined() - expect(error.classification).toEqual('UNKNOWN') - }) }) describe('isRetriableError()', () => { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index 99036adf6..fe3fa9cad 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -197,7 +197,7 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined - const error = newError(payload.message, standardizedCode, payload.gql_status, payload.description, payload.diagnostic_record, cause) + const error = newError(payload.message, standardizedCode, cause, payload.gql_status, payload.description, payload.diagnostic_record) return this._observer.onErrorApplyTransformation( error ) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index dd647bfb0..9234b372e 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -25,7 +25,7 @@ export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification */ -const errorClassification: { [key in ErrorClassification]: key } = { +const errorClassification: { [key in ErrorClassification]: key } = { DATABASE_ERROR: 'DATABASE_ERROR', CLIENT_ERROR: 'CLIENT_ERROR', TRANSIENT_ERROR: 'TRANSIENT_ERROR', @@ -68,7 +68,6 @@ type Neo4jErrorCode = | typeof PROTOCOL_ERROR | typeof NOT_AVAILABLE - /// TODO: Remove definitions of this.constructor and this.__proto__ /** * Class for all errors thrown/returned by the driver. @@ -145,14 +144,14 @@ class Neo4jError extends Error { * Create a new error from a message and error code * @param message the error message * @param {Neo4jErrorCode} [code] the error code + * @param {Neo4jError} [cause] * @param {String} [gqlStatus] * @param {String} [gqlStatusDescription] * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message - * @param {Neo4jError} [cause] * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Neo4jError): Neo4jError { +function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) } @@ -195,11 +194,11 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } -function extractClassification(diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification{ +function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { - return "UNKNOWN" + return 'UNKNOWN' } - return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' + return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } interface ErrorDiagnosticRecord { From 887f0315c909c557529b446a8cc191b3215f2acf Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:29:41 +0200 Subject: [PATCH 12/35] switch to latest testkit commit and fix deno backend --- packages/testkit-backend/deno/controller.ts | 23 +++++++++++++++++++-- testkit/testkit.json | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index f319a5d3e..a491b8589 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -76,8 +76,8 @@ export function createHandler( function writeDriverError(id, e) { let cause; - if (e.cause != null && e.cause != null) { - cause = writeDriverError(id, e.cause); + if (e.cause != null && e.cause != undefined) { + cause = writeGqlError(e.cause); } return { name: "DriverError", @@ -94,6 +94,25 @@ function writeDriverError(id, e) { }; } +function writeGqlError(e) { + let cause; + if (e.cause != null && e.cause != undefined) { + cause = writeGqlError(e.cause); + } + return { + name: "GqlError", + errorType: e.name, + msg: e.message, + code: e.code, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: e.diagnosticRecord, + cause, + classification: e.classification, + retryable: e.retriable, + }; +} + export default { createHandler, }; diff --git a/testkit/testkit.json b/testkit/testkit.json index e31040787..9665de093 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "d0cc6959b017d716d958f47fb3c36066a81be8fa" + "ref": "02b0eaac0664057e00e30ecc8aea73cb3df13409" } } From a6f132c8834f66bd2f7e1eb4a883995bbeae1895 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:45:07 +0200 Subject: [PATCH 13/35] Update controller.ts --- packages/testkit-backend/deno/controller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index a491b8589..c58eb9595 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -90,6 +90,7 @@ function writeDriverError(id, e) { diagnosticRecord: e.diagnosticRecord, cause, classification: e.classification, + rawClassification: e.rawClassification, retryable: e.retriable, }; } @@ -101,15 +102,13 @@ function writeGqlError(e) { } return { name: "GqlError", - errorType: e.name, msg: e.message, - code: e.code, gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, diagnosticRecord: e.diagnosticRecord, cause, classification: e.classification, - retryable: e.retriable, + rawClassification: e.rawClassification, }; } From a17c82591915d29fc8e24257d21591e95ec6e0f9 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:00:24 +0200 Subject: [PATCH 14/35] prettified deno code and expanded testkit logging --- packages/testkit-backend/deno/controller.ts | 18 ++++++++---------- .../src/channel/testkit-protocol.js | 3 +-- testkit/testkit.json | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index c58eb9595..6b4e04e69 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -75,10 +75,9 @@ export function createHandler( } function writeDriverError(id, e) { - let cause; - if (e.cause != null && e.cause != undefined) { - cause = writeGqlError(e.cause); - } + const cause = (e.cause != null && e.cause != undefined) + ? writeGqlError(e.cause) + : undefined; return { name: "DriverError", id, @@ -88,7 +87,7 @@ function writeDriverError(id, e) { gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, diagnosticRecord: e.diagnosticRecord, - cause, + cause: cause, classification: e.classification, rawClassification: e.rawClassification, retryable: e.retriable, @@ -96,17 +95,16 @@ function writeDriverError(id, e) { } function writeGqlError(e) { - let cause; - if (e.cause != null && e.cause != undefined) { - cause = writeGqlError(e.cause); - } + const cause = (e.cause != null && e.cause != undefined) + ? writeGqlError(e.cause) + : undefined; return { name: "GqlError", msg: e.message, gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, diagnosticRecord: e.diagnosticRecord, - cause, + cause: cause, classification: e.classification, rawClassification: e.rawClassification, }; diff --git a/packages/testkit-backend/src/channel/testkit-protocol.js b/packages/testkit-backend/src/channel/testkit-protocol.js index 43bcd57ca..78bd22cd9 100644 --- a/packages/testkit-backend/src/channel/testkit-protocol.js +++ b/packages/testkit-backend/src/channel/testkit-protocol.js @@ -62,8 +62,7 @@ export default class Protocol extends EventEmitter { serializeResponse (response) { const responseStr = stringify(response) - console.log('> writing response', response.name) - console.debug(responseStr) + console.log('> writing response', responseStr) return ['#response begin', responseStr, '#response end'].join('\n') + '\n' } diff --git a/testkit/testkit.json b/testkit/testkit.json index 9665de093..bd5e1f7c1 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "02b0eaac0664057e00e30ecc8aea73cb3df13409" + "ref": "bolt-5x7-and-gql-errors" } } From 6abc1971626ba84761026d0030393ed3ae14b5e0 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:13:24 +0200 Subject: [PATCH 15/35] update to conform to ADR --- .../test/connection/connection-channel.test.js | 4 ++-- packages/core/src/error.ts | 2 +- packages/core/src/notification.ts | 2 +- packages/core/test/__snapshots__/json.test.ts.snap | 6 +++--- packages/core/test/error.test.ts | 2 +- packages/neo4j-driver-deno/lib/core/error.ts | 2 +- packages/neo4j-driver-deno/lib/core/notification.ts | 2 +- .../neo4j-driver/test/internal/transaction-executor.test.js | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 71ac12ee6..4677379b0 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -371,7 +371,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: some error ` + - '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) @@ -419,7 +419,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: current failure ` + - '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unknown error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' ) }) }) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index b2e33e28a..aaf5feec0 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -152,7 +152,7 @@ class Neo4jError extends Error { * @private */ function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord, cause) } /** diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index 695373cd7..b1823b523 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -41,7 +41,7 @@ const unknownGqlStatus: Record { test('should create Neo4jError without status description with default description', () => { const error: Neo4jError = newError('some error') - expect(error.gqlStatusDescription).toEqual('error: general processing exception - unknown error. some error') + expect(error.gqlStatusDescription).toEqual('error: general processing exception - unexpected error. some error') expect(error.code).toEqual('N/A') }) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 9234b372e..af45aae01 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -152,7 +152,7 @@ class Neo4jError extends Error { * @private */ function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unknown error. ' + message, diagnosticRecord, cause) + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord, cause) } /** diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 0e2fc02c6..97b43baec 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -41,7 +41,7 @@ const unknownGqlStatus: Record { await testNoRetryOnUnknownError([OOM_ERROR], 1) }, 30000) - it('should not retry when transaction work returns promise rejected with unknown error', async () => { + it('should not retry when transaction work returns promise rejected with unexpected error', async () => { await testNoRetryOnUnknownError([UNKNOWN_ERROR], 1) }, 30000) @@ -155,7 +155,7 @@ describe('#unit TransactionExecutor', () => { await testNoRetryOnUnknownError([LOCKS_TERMINATED_ERROR], 1) }, 30000) - it('should not retry when transaction work returns promise rejected with unknown error type', async () => { + it('should not retry when transaction work returns promise rejected with unexpected error type', async () => { class MyTestError extends Error { constructor (message, code) { super(message) From e257ea467f5ecb8ecf7121151fa64c5be718a9d8 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:00:27 +0200 Subject: [PATCH 16/35] additional testkit logging --- packages/testkit-backend/deno/controller.ts | 6 ++++++ packages/testkit-backend/src/controller/local.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index 6b4e04e69..0fd085b3c 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -75,6 +75,9 @@ export function createHandler( } function writeDriverError(id, e) { + console.log("writing DriverError"); + console.log(e); + console.log("Cause: ", e.cause); const cause = (e.cause != null && e.cause != undefined) ? writeGqlError(e.cause) : undefined; @@ -95,6 +98,9 @@ function writeDriverError(id, e) { } function writeGqlError(e) { + console.log("writing GqlError"); + console.log(e); + console.log("Cause: ", e.cause); const cause = (e.cause != null && e.cause != undefined) ? writeGqlError(e.cause) : undefined; diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index 1ebfa57f3..a26375478 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -82,6 +82,9 @@ function newResponse (name, data) { } function writeDriverError (id, e, binder) { + console.log('writing DriverError') + console.log(e) + console.log('Cause: ', e.cause) let cause if (e.cause != null) { cause = writeGqlError(e.cause, binder) @@ -102,6 +105,9 @@ function writeDriverError (id, e, binder) { } function writeGqlError (e, binder) { + console.log('writing GqlError') + console.log(e) + console.log('Cause: ', e.cause) let cause if (e.cause != null) { cause = writeGqlError(e.cause, binder) From 5e18917562a00f64e46cf3fcd977b6cd492aab13 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:42:41 +0200 Subject: [PATCH 17/35] fix silly mistake --- packages/core/src/error.ts | 2 ++ packages/neo4j-driver-deno/lib/core/error.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index aaf5feec0..04d7cbb09 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -90,6 +90,7 @@ class Neo4jError extends Error { * @constructor * @param {string} message - the error message * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {Neo4jError} cause - Optional error code. Will be populated when error originates in the database. * @param {string} gqlStatus - the error message * @param {string} gqlStatusDescription - the error message * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message @@ -102,6 +103,7 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code + this.cause = cause this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index af45aae01..a35fe3e9d 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -90,6 +90,7 @@ class Neo4jError extends Error { * @constructor * @param {string} message - the error message * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {Neo4jError} cause - Optional error code. Will be populated when error originates in the database. * @param {string} gqlStatus - the error message * @param {string} gqlStatusDescription - the error message * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message @@ -102,6 +103,7 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code + this.cause = cause this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord From e11cb49c11c36d313ac73c3d84d14bf89f55cc65 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:56:08 +0200 Subject: [PATCH 18/35] small cause update --- packages/core/src/error.ts | 2 +- packages/neo4j-driver-deno/lib/core/error.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 04d7cbb09..7631d8e8f 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -103,7 +103,7 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code - this.cause = cause + this.cause = cause != null ? cause : undefined this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index a35fe3e9d..69abe45b0 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -103,7 +103,7 @@ class Neo4jError extends Error { // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype this.code = code - this.cause = cause + this.cause = cause != null ? cause : undefined this.gqlStatus = gqlStatus this.gqlStatusDescription = gqlStatusDescription this.diagnosticRecord = diagnosticRecord From 6324dcee3decda77b7062f8b8a74c39f6b4320c5 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:32:56 +0200 Subject: [PATCH 19/35] learn to recognize bolt 5.7 and remove some logging --- packages/core/src/internal/constants.ts | 2 ++ packages/neo4j-driver-deno/lib/core/internal/constants.ts | 2 ++ packages/testkit-backend/deno/controller.ts | 6 ------ packages/testkit-backend/src/controller/local.js | 6 ------ 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index 09ccb7602..ed32dcef5 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -38,6 +38,7 @@ 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 BOLT_PROTOCOL_V5_6: number = 5.6 +const BOLT_PROTOCOL_V5_7: number = 5.7 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -72,5 +73,6 @@ export { BOLT_PROTOCOL_V5_4, BOLT_PROTOCOL_V5_5, BOLT_PROTOCOL_V5_6, + BOLT_PROTOCOL_V5_7, TELEMETRY_APIS } diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts index 09ccb7602..ed32dcef5 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/constants.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -38,6 +38,7 @@ 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 BOLT_PROTOCOL_V5_6: number = 5.6 +const BOLT_PROTOCOL_V5_7: number = 5.7 const TELEMETRY_APIS = { MANAGED_TRANSACTION: 0, @@ -72,5 +73,6 @@ export { BOLT_PROTOCOL_V5_4, BOLT_PROTOCOL_V5_5, BOLT_PROTOCOL_V5_6, + BOLT_PROTOCOL_V5_7, TELEMETRY_APIS } diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index 0fd085b3c..6b4e04e69 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -75,9 +75,6 @@ export function createHandler( } function writeDriverError(id, e) { - console.log("writing DriverError"); - console.log(e); - console.log("Cause: ", e.cause); const cause = (e.cause != null && e.cause != undefined) ? writeGqlError(e.cause) : undefined; @@ -98,9 +95,6 @@ function writeDriverError(id, e) { } function writeGqlError(e) { - console.log("writing GqlError"); - console.log(e); - console.log("Cause: ", e.cause); const cause = (e.cause != null && e.cause != undefined) ? writeGqlError(e.cause) : undefined; diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index a26375478..1ebfa57f3 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -82,9 +82,6 @@ function newResponse (name, data) { } function writeDriverError (id, e, binder) { - console.log('writing DriverError') - console.log(e) - console.log('Cause: ', e.cause) let cause if (e.cause != null) { cause = writeGqlError(e.cause, binder) @@ -105,9 +102,6 @@ function writeDriverError (id, e, binder) { } function writeGqlError (e, binder) { - console.log('writing GqlError') - console.log(e) - console.log('Cause: ', e.cause) let cause if (e.cause != null) { cause = writeGqlError(e.cause, binder) From b902f31458b3beccaf5c6c5aa681aaf1ae20c769 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:22:06 +0200 Subject: [PATCH 20/35] corrected error sending in deno testkit --- packages/testkit-backend/deno/controller.ts | 40 +++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index 6b4e04e69..82ea8bc93 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -80,17 +80,19 @@ function writeDriverError(id, e) { : undefined; return { name: "DriverError", - id, - errorType: e.name, - msg: e.message, - code: e.code, - gqlStatus: e.gqlStatus, - statusDescription: e.gqlStatusDescription, - diagnosticRecord: e.diagnosticRecord, - cause: cause, - classification: e.classification, - rawClassification: e.rawClassification, - retryable: e.retriable, + data: { + id, + errorType: e.name, + msg: e.message, + code: e.code, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: e.diagnosticRecord, + cause: cause, + classification: e.classification, + rawClassification: e.rawClassification, + retryable: e.retriable, + }, }; } @@ -100,13 +102,15 @@ function writeGqlError(e) { : undefined; return { name: "GqlError", - msg: e.message, - gqlStatus: e.gqlStatus, - statusDescription: e.gqlStatusDescription, - diagnosticRecord: e.diagnosticRecord, - cause: cause, - classification: e.classification, - rawClassification: e.rawClassification, + data: { + msg: e.message, + gqlStatus: e.gqlStatus, + statusDescription: e.gqlStatusDescription, + diagnosticRecord: e.diagnosticRecord, + cause: cause, + classification: e.classification, + rawClassification: e.rawClassification, + }, }; } From 09f7cda290a2cf71ea5e7a91b55cff5db0f5a4ab Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:58:23 +0200 Subject: [PATCH 21/35] Cypher type bindings for deno --- packages/testkit-backend/deno/controller.ts | 24 +++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index 82ea8bc93..310b70a65 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -1,4 +1,5 @@ import Context from "../src/context.js"; +import CypherNativeBinders from "../src/cypher-native-binders.js"; import { FakeTime } from "./deps.ts"; import { RequestHandlerMap, @@ -16,7 +17,11 @@ interface Wire { writeBackendError(msg: string): Promise; } -function newWire(context: Context, reply: Reply): Wire { +function newWire( + context: Context, + binder: CypherNativeBinders, + reply: Reply, +): Wire { return { writeResponse: (response: TestkitResponse) => reply(response), writeError: (e: Error) => { @@ -30,7 +35,7 @@ function newWire(context: Context, reply: Reply): Wire { }); } else { const id = context.addError(e); - return reply(writeDriverError(id, e)); + return reply(writeDriverError(id, e, binder)); } } const msg = e.message; @@ -47,12 +52,13 @@ export function createHandler( newContext: () => Context, requestHandlers: RequestHandlerMap, ) { + const binder = new CypherNativeBinders(neo4j); return async function ( reply: Reply, requests: () => AsyncIterable, ) { const context = newContext(); - const wire = newWire(context, (response) => { + const wire = newWire(context, binder, (response) => { console.log("response:", response.name); console.debug(response.data); return reply(response); @@ -74,9 +80,9 @@ export function createHandler( }; } -function writeDriverError(id, e) { +function writeDriverError(id, e, binder) { const cause = (e.cause != null && e.cause != undefined) - ? writeGqlError(e.cause) + ? writeGqlError(e.cause, binder) : undefined; return { name: "DriverError", @@ -87,7 +93,7 @@ function writeDriverError(id, e) { code: e.code, gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, - diagnosticRecord: e.diagnosticRecord, + diagnosticRecord: binder.objectToCypher(e.diagnosticRecord), cause: cause, classification: e.classification, rawClassification: e.rawClassification, @@ -96,9 +102,9 @@ function writeDriverError(id, e) { }; } -function writeGqlError(e) { +function writeGqlError(e, binder) { const cause = (e.cause != null && e.cause != undefined) - ? writeGqlError(e.cause) + ? writeGqlError(e.cause, binder) : undefined; return { name: "GqlError", @@ -106,7 +112,7 @@ function writeGqlError(e) { msg: e.message, gqlStatus: e.gqlStatus, statusDescription: e.gqlStatusDescription, - diagnosticRecord: e.diagnosticRecord, + diagnosticRecord: binder.objectToCypher(e.diagnosticRecord), cause: cause, classification: e.classification, rawClassification: e.rawClassification, From 963bec96d26c2d7a504c4364053fdba670d4e48a Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:37:17 +0200 Subject: [PATCH 22/35] Documentation and integration test fix --- .../bolt-connection/test/bolt/index.test.js | 4 +- packages/core/src/error.ts | 49 +++++++++++++++++++ .../test/internal/connection-channel.test.js | 2 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index e0c81a954..1a9117556 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -48,13 +48,13 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' - const protocolVersion5x6to5x0 = '00 07 07 05' + const protocolVersion5x7to5x0 = '00 07 07 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion5x6to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x7to5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 7631d8e8f..0f1dd4f9f 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -102,12 +102,61 @@ class Neo4jError extends Error { this.constructor = Neo4jError // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype + /** + * The Neo4j Error code + * + * @type {string} + * @deprecated This property will be removed in 7.0. + * @public + */ this.code = code + /** + * Optional, nested error which caused the error + * + * @type {Error?} + * @experimental This is a preview feature. + * @public + */ this.cause = cause != null ? cause : undefined + /** + * The GQL Status code + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.gqlStatus = gqlStatus + /** + * The GQL Status Description + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.gqlStatusDescription = gqlStatusDescription + /** + * The GQL diagnostic record + * + * @type {ErrorDiagnosticRecord} + * @experimental This is a preview feature. + * @public + */ this.diagnosticRecord = diagnosticRecord + /** + * The GQL error classification, extracted from the diagnostic record + * + * @type {ErrorClassification} + * @experimental This is a preview feature. + * @public + */ this.classification = extractClassification(this.diagnosticRecord) + /** + * The GQL error classification, extracted from the diagnostic record as a raw string + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.rawClassification = diagnosticRecord?._classification ?? undefined this.name = 'Neo4jError' /** diff --git a/packages/neo4j-driver/test/internal/connection-channel.test.js b/packages/neo4j-driver/test/internal/connection-channel.test.js index 8a8bd9e91..a4aa40610 100644 --- a/packages/neo4j-driver/test/internal/connection-channel.test.js +++ b/packages/neo4j-driver/test/internal/connection-channel.test.js @@ -300,7 +300,7 @@ describe('#integration ChannelConnection', () => { .then(() => done.fail('Should fail')) .catch(error => { expect(error.message).toEqual( - 'Received FAILURE as a response for RESET: Neo4jError: Hello' + 'Received FAILURE as a response for RESET: Neo4jError' ) expect(connection._isBroken).toBeTruthy() expect(connection.isOpen()).toBeFalsy() From 2bbc3e4d2f44b67f3b5a0cc7d474532fc5795ce2 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:42:24 +0200 Subject: [PATCH 23/35] add documentation to deno error class --- packages/neo4j-driver-deno/lib/core/error.ts | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 69abe45b0..23178d9fc 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -102,12 +102,61 @@ class Neo4jError extends Error { this.constructor = Neo4jError // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype + /** + * The Neo4j Error code + * + * @type {string} + * @deprecated This property will be removed in 7.0. + * @public + */ this.code = code + /** + * Optional, nested error which caused the error + * + * @type {Error?} + * @experimental This is a preview feature. + * @public + */ this.cause = cause != null ? cause : undefined + /** + * The GQL Status code + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.gqlStatus = gqlStatus + /** + * The GQL Status Description + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.gqlStatusDescription = gqlStatusDescription + /** + * The GQL diagnostic record + * + * @type {ErrorDiagnosticRecord} + * @experimental This is a preview feature. + * @public + */ this.diagnosticRecord = diagnosticRecord + /** + * The GQL error classification, extracted from the diagnostic record + * + * @type {ErrorClassification} + * @experimental This is a preview feature. + * @public + */ this.classification = extractClassification(this.diagnosticRecord) + /** + * The GQL error classification, extracted from the diagnostic record as a raw string + * + * @type {string} + * @experimental This is a preview feature. + * @public + */ this.rawClassification = diagnosticRecord?._classification ?? undefined this.name = 'Neo4jError' /** From bc0d95c3794e0bccc3f2bf3da64f26ba791d21b4 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:05:28 +0200 Subject: [PATCH 24/35] Update response-handler.test.js --- .../test/bolt/response-handler.test.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index 534c83e70..a4f3f7cf1 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -106,13 +106,13 @@ describe('response-handler', () => { code: 'Neo.ClientError.Test.Error', gql_status: '13N37', description: 'I made this error up, for fun and profit!', - diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR' }, + diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR', additional_thing: 5268 }, cause: { message: 'old cause message', code: 'Neo.ClientError.Test.Cause', gql_status: '13N38', description: 'I made this error up, for fun and profit and reasons!', - diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'DATABASE_ERROR' } + diagnostic_record: { OPERATION: '', OPERATION_CODE: '2', CURRENT_SCHEMA: '/', _classification: 'DATABASE_ERROR', additional_thing: false } } } const observer = { @@ -134,11 +134,27 @@ describe('response-handler', () => { expect(receivedError.message).toBe(errorPayload.message) expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) expect(receivedError.gqlStatusDescription).toBe(errorPayload.description) + // eslint-disable-next-line camelcase + testDiagnosticRecord(receivedError.diagnosticRecord, errorPayload.diagnostic_record) + // eslint-disable-next-line camelcase + testDiagnosticRecord(receivedError.cause.diagnosticRecord, errorPayload.cause.diagnostic_record) expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) expect(receivedError.cause.classification).toBe(errorPayload.cause.diagnostic_record._classification) }) }) + // eslint-disable-next-line camelcase + function testDiagnosticRecord (diagnostic_record, expected_diagnostic_record) { + // eslint-disable-next-line camelcase + expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION) + // eslint-disable-next-line camelcase + expect(diagnostic_record.CURRENT_SCHEMA).toBe(expected_diagnostic_record.CURRENT_SCHEMA) + // eslint-disable-next-line camelcase + expect(diagnostic_record.OPERATION_CODE).toBe(expected_diagnostic_record.OPERATION_CODE) + // eslint-disable-next-line camelcase + expect(diagnostic_record.additional_thing).toBe(expected_diagnostic_record.additional_thing) + } + it('should keep track of observers and notify onObserversCountChange()', () => { const observer = { onObserversCountChange: jest.fn() From 8ca335e442a4e2c1c10b746655da5cbdc981a921 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:06:34 +0200 Subject: [PATCH 25/35] Update response-handler.test.js --- .../bolt-connection/test/bolt/response-handler.test.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index a4f3f7cf1..d635d093f 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -18,6 +18,7 @@ import ResponseHandler from '../../src/bolt/response-handler' import { internal, newError } from 'neo4j-driver-core' +/* eslint-disable camelcase */ const { logger: { Logger } } = internal @@ -134,24 +135,17 @@ describe('response-handler', () => { expect(receivedError.message).toBe(errorPayload.message) expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) expect(receivedError.gqlStatusDescription).toBe(errorPayload.description) - // eslint-disable-next-line camelcase testDiagnosticRecord(receivedError.diagnosticRecord, errorPayload.diagnostic_record) - // eslint-disable-next-line camelcase testDiagnosticRecord(receivedError.cause.diagnosticRecord, errorPayload.cause.diagnostic_record) expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) expect(receivedError.cause.classification).toBe(errorPayload.cause.diagnostic_record._classification) }) }) - // eslint-disable-next-line camelcase function testDiagnosticRecord (diagnostic_record, expected_diagnostic_record) { - // eslint-disable-next-line camelcase expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION) - // eslint-disable-next-line camelcase expect(diagnostic_record.CURRENT_SCHEMA).toBe(expected_diagnostic_record.CURRENT_SCHEMA) - // eslint-disable-next-line camelcase expect(diagnostic_record.OPERATION_CODE).toBe(expected_diagnostic_record.OPERATION_CODE) - // eslint-disable-next-line camelcase expect(diagnostic_record.additional_thing).toBe(expected_diagnostic_record.additional_thing) } From 2992eff5286992de7a752128d19c608050476b93 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:33:53 +0200 Subject: [PATCH 26/35] remove code from cause in test --- packages/bolt-connection/test/bolt/response-handler.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index d635d093f..f17851bab 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -110,7 +110,6 @@ describe('response-handler', () => { diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR', additional_thing: 5268 }, cause: { message: 'old cause message', - code: 'Neo.ClientError.Test.Cause', gql_status: '13N38', description: 'I made this error up, for fun and profit and reasons!', diagnostic_record: { OPERATION: '', OPERATION_CODE: '2', CURRENT_SCHEMA: '/', _classification: 'DATABASE_ERROR', additional_thing: false } From 99761beee1a04a4841c7405a8c620b190dcdaf70 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:15:09 +0200 Subject: [PATCH 27/35] updated documentation for error --- packages/core/src/error.ts | 13 ++++++------- packages/neo4j-driver-deno/lib/core/error.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 0f1dd4f9f..a084ec70f 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -106,7 +106,6 @@ class Neo4jError extends Error { * The Neo4j Error code * * @type {string} - * @deprecated This property will be removed in 7.0. * @public */ this.code = code @@ -114,7 +113,6 @@ class Neo4jError extends Error { * Optional, nested error which caused the error * * @type {Error?} - * @experimental This is a preview feature. * @public */ this.cause = cause != null ? cause : undefined @@ -122,7 +120,6 @@ class Neo4jError extends Error { * The GQL Status code * * @type {string} - * @experimental This is a preview feature. * @public */ this.gqlStatus = gqlStatus @@ -130,7 +127,6 @@ class Neo4jError extends Error { * The GQL Status Description * * @type {string} - * @experimental This is a preview feature. * @public */ this.gqlStatusDescription = gqlStatusDescription @@ -138,7 +134,6 @@ class Neo4jError extends Error { * The GQL diagnostic record * * @type {ErrorDiagnosticRecord} - * @experimental This is a preview feature. * @public */ this.diagnosticRecord = diagnosticRecord @@ -146,7 +141,6 @@ class Neo4jError extends Error { * The GQL error classification, extracted from the diagnostic record * * @type {ErrorClassification} - * @experimental This is a preview feature. * @public */ this.classification = extractClassification(this.diagnosticRecord) @@ -154,7 +148,6 @@ class Neo4jError extends Error { * The GQL error classification, extracted from the diagnostic record as a raw string * * @type {string} - * @experimental This is a preview feature. * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined @@ -245,6 +238,9 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +/** + * extracts a typed classification from the diagnostic record. + */ function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' @@ -252,6 +248,9 @@ function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): Error return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } +/** + * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. + */ interface ErrorDiagnosticRecord { OPERATION: string OPERATION_CODE: string diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 23178d9fc..3dea93ac0 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -106,7 +106,6 @@ class Neo4jError extends Error { * The Neo4j Error code * * @type {string} - * @deprecated This property will be removed in 7.0. * @public */ this.code = code @@ -114,7 +113,6 @@ class Neo4jError extends Error { * Optional, nested error which caused the error * * @type {Error?} - * @experimental This is a preview feature. * @public */ this.cause = cause != null ? cause : undefined @@ -122,7 +120,6 @@ class Neo4jError extends Error { * The GQL Status code * * @type {string} - * @experimental This is a preview feature. * @public */ this.gqlStatus = gqlStatus @@ -130,7 +127,6 @@ class Neo4jError extends Error { * The GQL Status Description * * @type {string} - * @experimental This is a preview feature. * @public */ this.gqlStatusDescription = gqlStatusDescription @@ -138,7 +134,6 @@ class Neo4jError extends Error { * The GQL diagnostic record * * @type {ErrorDiagnosticRecord} - * @experimental This is a preview feature. * @public */ this.diagnosticRecord = diagnosticRecord @@ -146,7 +141,6 @@ class Neo4jError extends Error { * The GQL error classification, extracted from the diagnostic record * * @type {ErrorClassification} - * @experimental This is a preview feature. * @public */ this.classification = extractClassification(this.diagnosticRecord) @@ -154,7 +148,6 @@ class Neo4jError extends Error { * The GQL error classification, extracted from the diagnostic record as a raw string * * @type {string} - * @experimental This is a preview feature. * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined @@ -245,6 +238,9 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { return code === 'Neo.ClientError.Security.AuthorizationExpired' } +/** + * extracts a typed classification from the diagnostic record. + */ function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' @@ -252,6 +248,10 @@ function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): Error return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } + +/** + * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. + */ interface ErrorDiagnosticRecord { OPERATION: string OPERATION_CODE: string From 86a4d80641576458dac81927a67b5ed800139bb8 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:40:09 +0200 Subject: [PATCH 28/35] very important deno build --- packages/neo4j-driver-deno/lib/core/error.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 3dea93ac0..4548ad1b6 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -248,7 +248,6 @@ function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): Error return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } - /** * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. */ From 9dfa3e601d45b8c14848a3960a1380c5ae95a82e Mon Sep 17 00:00:00 2001 From: Max Gustafsson <61233757+MaxAake@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:25:23 +0200 Subject: [PATCH 29/35] Update packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio Barcélos --- packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js index a54c38360..d1177d28b 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x6.js @@ -31,7 +31,7 @@ const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ CURRENT_SCHEMA: '/' }) -export default class BoltProtocolV5x6 extends BoltProtocolV5x5 { +export default class BoltProtocol extends BoltProtocolV5x5 { get version () { return BOLT_PROTOCOL_V5_6 } From bb31dcdb9dc830e2c8ad5cb8bd2dcca8db3af7df Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:39:02 +0200 Subject: [PATCH 30/35] addressing comments from code review --- .../src/bolt/response-handler.js | 12 +- .../bolt-protocol-v5x7.test.js.snap | 61 + .../test/bolt/bolt-protocol-v5x7.test.js | 1579 +++++++++++++++++ .../test/bolt/response-handler.test.js | 8 +- .../connection/connection-channel.test.js | 4 +- packages/core/src/error.ts | 150 +- packages/core/src/gql-constants.ts | 27 + packages/core/src/index.ts | 6 + packages/core/src/notification.ts | 32 +- .../core/test/__snapshots__/json.test.ts.snap | 6 +- packages/core/test/error.test.ts | 6 +- .../bolt/bolt-protocol-v5x6.js | 2 +- .../bolt-connection/bolt/response-handler.js | 12 +- packages/neo4j-driver-deno/lib/core/error.ts | 157 +- .../lib/core/gql-constants.ts | 28 + packages/neo4j-driver-deno/lib/core/index.ts | 6 + .../lib/core/notification.ts | 32 +- .../test/internal/connection-channel.test.js | 2 +- 18 files changed, 1926 insertions(+), 204 deletions(-) create mode 100644 packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x7.test.js.snap create mode 100644 packages/bolt-connection/test/bolt/bolt-protocol-v5x7.test.js create mode 100644 packages/core/src/gql-constants.ts create mode 100644 packages/neo4j-driver-deno/lib/core/gql-constants.ts diff --git a/packages/bolt-connection/src/bolt/response-handler.js b/packages/bolt-connection/src/bolt/response-handler.js index b46599f55..c846067f5 100644 --- a/packages/bolt-connection/src/bolt/response-handler.js +++ b/packages/bolt-connection/src/bolt/response-handler.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError, json } from 'neo4j-driver-core' +import { newError, newGQLError, json } from 'neo4j-driver-core' // Signature bytes for each response message type const SUCCESS = 0x70 // 0111 0000 // SUCCESS @@ -196,12 +196,20 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) - const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined + const cause = payload.cause != null ? this._handleErrorCause(payload.cause) : undefined const error = newError(payload.message, standardizedCode, cause, payload.gql_status, payload.description, payload.diagnostic_record) return this._observer.onErrorApplyTransformation( error ) } + + _handleErrorCause (payload) { + const cause = payload.cause != null ? this._handleErrorCause(payload.cause) : undefined + const error = newGQLError(payload.message, cause, payload.gql_status, payload.description, payload.diagnostic_record) + return this._observer.onErrorApplyTransformation( + error + ) + } } /** diff --git a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x7.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x7.test.js.snap new file mode 100644 index 000000000..e04e65b76 --- /dev/null +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v5x7.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit BoltProtocolV5x7 .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 BoltProtocolV5x7 .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 BoltProtocolV5x7 .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 BoltProtocolV5x7 .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 BoltProtocolV5x7 .unpack() should not unpack with wrong size (Date with less fields) 1`] = `"Wrong struct size for Date, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Date with more fields) 1`] = `"Wrong struct size for Date, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Duration with less fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Duration with more fields) 1`] = `"Wrong struct size for Duration, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (LocalDateTime with less fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (LocalDateTime with more fields) 1`] = `"Wrong struct size for LocalDateTime, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (LocalTime with less fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 0"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (LocalTime with more fields) 1`] = `"Wrong struct size for LocalTime, expected 1 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Node with less fields) 1`] = `"Wrong struct size for Node, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Node with more fields) 1`] = `"Wrong struct size for Node, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Path with less fields) 1`] = `"Wrong struct size for Path, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Path with more fields) 1`] = `"Wrong struct size for Path, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Point with less fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Point with more fields) 1`] = `"Wrong struct size for Point2D, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Point3D with less fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Point3D with more fields) 1`] = `"Wrong struct size for Point3D, expected 4 but was 5"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Relationship with less fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 5"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Relationship with more fields) 1`] = `"Wrong struct size for Relationship, expected 8 but was 9"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Time with less fields) 1`] = `"Wrong struct size for Time, expected 2 but was 1"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (Time with more fileds) 1`] = `"Wrong struct size for Time, expected 2 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 3"`; + +exports[`#unit BoltProtocolV5x7 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 4 but was 5"`; diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x7.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x7.test.js new file mode 100644 index 000000000..23727925d --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x7.test.js @@ -0,0 +1,1579 @@ +/** + * 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 BoltProtocolV5x7 from '../../src/bolt/bolt-protocol-v5x7' +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 BoltProtocolV5x7', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + telemetryBehaviour.protocolSupportsTelemetry(newProtocol) + + it('should enrich error metadata', () => { + const protocol = new BoltProtocolV5x7() + const enrichedData = protocol.enrichErrorMetadata({ neo4j_code: 'hello', diagnostic_record: {} }) + expect(enrichedData.code).toBe('hello') + expect(enrichedData.diagnostic_record.OPERATION).toBe('') + expect(enrichedData.diagnostic_record.OPERATION_CODE).toBe('0') + expect(enrichedData.diagnostic_record.CURRENT_SCHEMA).toBe('/') + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(null, null, false) + + expect(protocol.version).toBe(5.7) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x7(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 BoltProtocolV5x7(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const boltAgent = { + product: 'neo4j-javascript/5.7', + 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.7.0', + '', + undefined, + null + ])('should always use the user agent set by the user', (userAgent) => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x7(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const boltAgent = { + product: 'neo4j-javascript/5.7', + 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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(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 BoltProtocolV5x7(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v2.Packer) + }) + + it('should configure v2 unpacker', () => { + const protocol = new BoltProtocolV5x7(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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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 BoltProtocolV5x7( + 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) + }) + }) + + describe('result metadata enrichment', () => { + it('run should configure BoltProtocolV5x7._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 BoltProtocolV5x7(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(protocol._enrichMetadata) + }) + + describe('BoltProtocolV5x7._enrichMetadata', () => { + const protocol = newProtocol() + + it('should handle empty metadata', () => { + const metadata = protocol._enrichMetadata({}) + + expect(metadata).toEqual({}) + }) + + it('should handle metadata with random objects', () => { + const metadata = protocol._enrichMetadata({ + a: 1133, + b: 345 + }) + + expect(metadata).toEqual({ + a: 1133, + b: 345 + }) + }) + + it('should handle metadata not change notifications ', () => { + const metadata = protocol._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: '/' + }, + { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + })], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }), + statusesWithDiagnosticRecord({ + OPERATION: 'A', + OPERATION_CODE: 'B', + CURRENT_SCHEMA: '/C' + }) + ], + [ + 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' } + }) + ], + [ + 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' + }) + ], + [ + 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' + }) + ], + [ + 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: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: '/' + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }), + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null + }) + ], + [ + statusesWithDiagnosticRecord({ + OPERATION: null, + OPERATION_CODE: null, + CURRENT_SCHEMA: null, + _status_parameters: null + }), + 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: null + }), + 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: null + }), + 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: 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 + }) + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + description: 'description', + title: 'Mitt title', + diagnostic_record: { + _classification: 'SOME', + _severity: 'INFORMATION' + } + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + neo4j_code: 'Neo.Info.My.Code', + title: 'Mitt title', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/', + _classification: 'SOME', + _severity: 'INFORMATION' + } + }] + ], + [ + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description' + }], + [{ + gql_status: '03N33', + status_description: 'info: description', + description: 'description', + diagnostic_record: { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + } + }] + ] + ])('should handle statuses (%o) ', (statuses, expectedStatuses) => { + const metadata = protocol._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 BoltProtocolV5x7(recorder, null, false, undefined, undefined, () => {}) + } +}) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index f17851bab..30e32bddb 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -16,6 +16,7 @@ */ import ResponseHandler from '../../src/bolt/response-handler' +import BoltProtocolV1 from '../../src/bolt/bolt-protocol-v1' import { internal, newError } from 'neo4j-driver-core' /* eslint-disable camelcase */ @@ -107,7 +108,7 @@ describe('response-handler', () => { code: 'Neo.ClientError.Test.Error', gql_status: '13N37', description: 'I made this error up, for fun and profit!', - diagnostic_record: { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR', additional_thing: 5268 }, + diagnostic_record: { OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: 'CLIENT_ERROR', additional_thing: 5268 }, cause: { message: 'old cause message', gql_status: '13N38', @@ -119,7 +120,8 @@ describe('response-handler', () => { capturedErrors: [], onFailure: error => observer.capturedErrors.push(error) } - const responseHandler = new ResponseHandler({ observer, log: Logger.noOp() }) + const enrichErrorMetadata = new BoltProtocolV1().enrichErrorMetadata + const responseHandler = new ResponseHandler({ observer, enrichErrorMetadata, log: Logger.noOp() }) responseHandler._queueObserver({}) const errorMessage = { @@ -142,7 +144,7 @@ describe('response-handler', () => { }) function testDiagnosticRecord (diagnostic_record, expected_diagnostic_record) { - expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION) + expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION ?? '') expect(diagnostic_record.CURRENT_SCHEMA).toBe(expected_diagnostic_record.CURRENT_SCHEMA) expect(diagnostic_record.OPERATION_CODE).toBe(expected_diagnostic_record.OPERATION_CODE) expect(diagnostic_record.additional_thing).toBe(expected_diagnostic_record.additional_thing) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 4677379b0..d14630fab 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -371,7 +371,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: some error ` + - '({"code":"C","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","diagnosticRecord":{"OPERATION":"","OPERATION_CODE":"0","CURRENT_SCHEMA":"/"},"classification":"UNKNOWN","name":"Neo4jError","code":"C","retriable":false})' ) }) }) @@ -419,7 +419,7 @@ describe('ChannelConnection', () => { expect(loggerFunction).toHaveBeenCalledWith( 'error', `${connection} experienced a fatal error caused by Neo4jError: current failure ` + - '({"code":"ongoing","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. current failure","classification":"UNKNOWN","name":"Neo4jError","retriable":false})' + '({"gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. current failure","diagnosticRecord":{"OPERATION":"","OPERATION_CODE":"0","CURRENT_SCHEMA":"/"},"classification":"UNKNOWN","name":"Neo4jError","code":"ongoing","retriable":false})' ) }) }) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index a084ec70f..f60a7a2cf 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -17,8 +17,9 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. -import { NumberOrInteger } from './graph-types' + import * as json from './json' +import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants' export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' /** @@ -69,46 +70,32 @@ type Neo4jErrorCode = | typeof NOT_AVAILABLE /// TODO: Remove definitions of this.constructor and this.__proto__ -/** - * Class for all errors thrown/returned by the driver. - */ -class Neo4jError extends Error { - /** - * Optional error code. Will be populated when error originates in the database. - */ - code: Neo4jErrorCode + +class GQLError extends Error { gqlStatus: string gqlStatusDescription: string - diagnosticRecord: ErrorDiagnosticRecord | undefined + diagnosticRecord: DiagnosticRecord | undefined classification: ErrorClassification rawClassification?: string cause?: Error retriable: boolean - __proto__: Neo4jError + __proto__: GQLError /** * @constructor * @param {string} message - the error message - * @param {string} code - Optional error code. Will be populated when error originates in the database. - * @param {Neo4jError} cause - Optional error code. Will be populated when error originates in the database. - * @param {string} gqlStatus - the error message - * @param {string} gqlStatusDescription - the error message - * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {string} gqlStatus - the GQL status code of the error + * @param {string} gqlStatusDescription - the GQL status description of the error + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error diagnostic record + * @param {Error} cause - Optional nested error, the cause of the error */ - constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Error) { + constructor (message: string, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: DiagnosticRecord, cause?: Error) { // eslint-disable-next-line // @ts-ignore: not available in ES6 yet super(message, cause != null ? { cause } : undefined) - this.constructor = Neo4jError + this.constructor = GQLError // eslint-disable-next-line no-proto - this.__proto__ = Neo4jError.prototype - /** - * The Neo4j Error code - * - * @type {string} - * @public - */ - this.code = code + this.__proto__ = GQLError.prototype /** * Optional, nested error which caused the error * @@ -133,7 +120,7 @@ class Neo4jError extends Error { /** * The GQL diagnostic record * - * @type {ErrorDiagnosticRecord} + * @type {DiagnosticRecord} * @public */ this.diagnosticRecord = diagnosticRecord @@ -151,14 +138,60 @@ class Neo4jError extends Error { * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined - this.name = 'Neo4jError' + this.name = 'GQLError' + } + + /** + * 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, { useCustomToString: true }) + } +} + +/** + * Class for all errors thrown/returned by the driver. + */ +class Neo4jError extends GQLError { + /** + * Optional error code. Will be populated when error originates in the database. + */ + code: string + + /** + * @constructor + * @param {string} message - the error message + * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {string} gqlStatus - the GQL status code of the error + * @param {string} gqlStatusDescription - the GQL status description of the error + * @param {DiagnosticRecord} diagnosticRecord - the error diagnostic record + * @param {Error} cause - Optional nested error, the cause of the error + */ + constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: DiagnosticRecord, cause?: Error) { + super(message, gqlStatus, gqlStatusDescription, diagnosticRecord, cause) + this.constructor = Neo4jError + // eslint-disable-next-line no-proto + this.__proto__ = Neo4jError.prototype /** - * Indicates if the error is retriable. - * @type {boolean} - true if the error is retriable + * The Neo4j Error code + * + * @type {string} + * @public */ + this.code = code + + this.name = 'Neo4jError' this.retriable = _isRetriableCode(code) } + toString (): string { + return this.name + ': ' + this.message + } + /** * Verifies if the given error is retriable. * @@ -168,35 +201,38 @@ class Neo4jError extends Error { static isRetriable (error?: any | null): boolean { return error !== null && error !== undefined && - error instanceof Neo4jError && + error instanceof GQLError && error.retriable } - - /** - * 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, { useCustomToString: true }) - } } /** - * Create a new error from a message and error code + * Create a new error from a message and optional data * @param message the error message * @param {Neo4jErrorCode} [code] the error code * @param {Neo4jError} [cause] * @param {String} [gqlStatus] * @param {String} [gqlStatusDescription] - * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {DiagnosticRecord} diagnosticRecord - the error message + * @return {Neo4jError} an {@link Neo4jError} + * @private + */ +function newError (message: string, code?: Neo4jErrorCode, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): Neo4jError { + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord ?? rawPolyfilledDiagnosticRecord, cause) +} + +/** + * Create a new GQL error from a message and optional data + * @param message the error message + * @param {Neo4jError} [cause] + * @param {String} [gqlStatus] + * @param {String} [gqlStatusDescription] + * @param {DiagnosticRecord} diagnosticRecord - the error message * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord, cause) +function newGQLError (message: string, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): GQLError { + return new GQLError(message, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord ?? rawPolyfilledDiagnosticRecord, cause) } /** @@ -241,35 +277,19 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { /** * extracts a typed classification from the diagnostic record. */ -function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { +function extractClassification (diagnosticRecord?: any): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' } return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } -/** - * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. - */ -interface ErrorDiagnosticRecord { - OPERATION: string - OPERATION_CODE: string - CURRENT_SCHEMA: string - _severity?: string - _classification?: ErrorClassification - _position?: { - offset: NumberOrInteger - line: NumberOrInteger - column: NumberOrInteger - } - _status_parameters?: Record - [key: string]: unknown -} - export { newError, + newGQLError, isRetriableError, Neo4jError, + GQLError, SERVICE_UNAVAILABLE, SESSION_EXPIRED, PROTOCOL_ERROR diff --git a/packages/core/src/gql-constants.ts b/packages/core/src/gql-constants.ts new file mode 100644 index 000000000..c12e562bb --- /dev/null +++ b/packages/core/src/gql-constants.ts @@ -0,0 +1,27 @@ +import { NumberOrInteger } from './graph-types' + +/** + * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. + */ +export interface DiagnosticRecord { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: string + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record + [key: string]: unknown +} + +export const rawPolyfilledDiagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +} + +Object.freeze(rawPolyfilledDiagnosticRecord) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f9604cd87..5c3871b9e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,6 +18,8 @@ import { newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE, @@ -116,6 +118,8 @@ const forExport = { authTokenManagers, newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, error, Integer, @@ -189,6 +193,8 @@ export { authTokenManagers, newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, error, Integer, diff --git a/packages/core/src/notification.ts b/packages/core/src/notification.ts index b1823b523..4104293fa 100644 --- a/packages/core/src/notification.ts +++ b/packages/core/src/notification.ts @@ -16,7 +16,7 @@ */ import * as json from './json' import { util } from './internal' -import { NumberOrInteger } from './graph-types' +import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants' interface NotificationPosition { offset?: number @@ -224,21 +224,6 @@ class Notification { } } -interface NotificationDiagnosticRecord { - OPERATION: string - OPERATION_CODE: string - CURRENT_SCHEMA: string - _severity?: string - _classification?: string - _position?: { - offset: NumberOrInteger - line: NumberOrInteger - column: NumberOrInteger - } - _status_parameters?: Record - [key: string]: unknown -} - /** * Representation for GqlStatusObject found when executing a query. *

@@ -251,7 +236,7 @@ interface NotificationDiagnosticRecord { class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string - public readonly diagnosticRecord: NotificationDiagnosticRecord + public readonly diagnosticRecord: DiagnosticRecord public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string @@ -420,7 +405,7 @@ function polyfillNotification (status: any): Notification | undefined { */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION - const polyfilledRawObj: any & { diagnostic_record: NotificationDiagnosticRecord } = { + const polyfilledRawObj: any & { diagnostic_record: DiagnosticRecord } = { gql_status: defaultStatus.gql_status, status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, @@ -445,14 +430,6 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { return new GqlStatusObject(polyfilledRawObj) } -const rawPolyfilledDiagnosticRecord = { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/' -} - -Object.freeze(rawPolyfilledDiagnosticRecord) - /** * This objects are used for polyfilling the first status on the status list * @@ -598,6 +575,5 @@ export type { NotificationPosition, NotificationSeverityLevel, NotificationCategory, - NotificationClassification, - NotificationDiagnosticRecord + NotificationClassification } diff --git a/packages/core/test/__snapshots__/json.test.ts.snap b/packages/core/test/__snapshots__/json.test.ts.snap index 666171e5b..195fb37ba 100644 --- a/packages/core/test/__snapshots__/json.test.ts.snap +++ b/packages/core/test/__snapshots__/json.test.ts.snap @@ -102,11 +102,11 @@ exports[`json .stringify should handle object with custom toString in list 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","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}"`; +exports[`json .stringify should handle objects created with createBrokenObject 1`] = `"{"__isBrokenObject__":true,"__reason__":{"gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","diagnosticRecord":{"OPERATION":"","OPERATION_CODE":"0","CURRENT_SCHEMA":"/"},"classification":"UNKNOWN","name":"Neo4jError","code":"N/A","retriable":false}}"`; -exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}]"`; +exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{"__isBrokenObject__":true,"__reason__":{"gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","diagnosticRecord":{"OPERATION":"","OPERATION_CODE":"0","CURRENT_SCHEMA":"/"},"classification":"UNKNOWN","name":"Neo4jError","code":"N/A","retriable":false}}]"`; -exports[`json .stringify should handle objects created with createBrokenObject inside other object 1`] = `"{"number":1,"broken":{"__isBrokenObject__":true,"__reason__":{"code":"N/A","gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","classification":"UNKNOWN","name":"Neo4jError","retriable":false}}}"`; +exports[`json .stringify should handle objects created with createBrokenObject inside other object 1`] = `"{"number":1,"broken":{"__isBrokenObject__":true,"__reason__":{"gqlStatus":"50N42","gqlStatusDescription":"error: general processing exception - unexpected error. some error","diagnosticRecord":{"OPERATION":"","OPERATION_CODE":"0","CURRENT_SCHEMA":"/"},"classification":"UNKNOWN","name":"Neo4jError","code":"N/A","retriable":false}}}"`; exports[`json .stringify should handle string 1`] = `""my string""`; diff --git a/packages/core/test/error.test.ts b/packages/core/test/error.test.ts index 6386b3191..8f06745a6 100644 --- a/packages/core/test/error.test.ts +++ b/packages/core/test/error.test.ts @@ -18,6 +18,7 @@ import { Neo4jError, isRetriableError, newError, + newGQLError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE, SESSION_EXPIRED @@ -65,7 +66,7 @@ describe('newError', () => { }) test('should create Neo4jError with cause', () => { - const cause = newError('cause') + const cause = newGQLError('cause') const error: Neo4jError = newError('some error', undefined, cause, 'some status', 'some description', undefined) expect(error.message).toEqual('some error') @@ -80,7 +81,7 @@ describe('newError', () => { }) test('should create Neo4jError with nested cause', () => { - const cause = newError('cause', undefined, newError('nested'), undefined, undefined, undefined) + const cause = newGQLError('cause', newGQLError('nested'), undefined, undefined, undefined) const error: Neo4jError = newError('some error', undefined, cause, 'some status', 'some description', undefined) expect(error.message).toEqual('some error') @@ -116,7 +117,6 @@ describe('newError', () => { 'CLIENT_ERROR', 'DATABASE_ERROR' ])('should create Neo4jError with diagnosticRecord with classification (%s)', (classification) => { - // @ts-expect-error const error: Neo4jError = newError('some error', undefined, undefined, undefined, undefined, { OPERATION: '', OPERATION_CODE: '0', CURRENT_SCHEMA: '/', _classification: classification }) expect(error.classification).toEqual(classification) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js index fa2dfc385..ea5bd840b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x6.js @@ -31,7 +31,7 @@ const DEFAULT_DIAGNOSTIC_RECORD = Object.freeze({ CURRENT_SCHEMA: '/' }) -export default class BoltProtocolV5x6 extends BoltProtocolV5x5 { +export default class BoltProtocol extends BoltProtocolV5x5 { get version () { return BOLT_PROTOCOL_V5_6 } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js index fe3fa9cad..93839c2c3 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError, json } from '../../core/index.ts' +import { newError, newGQLError, json } from '../../core/index.ts' // Signature bytes for each response message type const SUCCESS = 0x70 // 0111 0000 // SUCCESS @@ -196,12 +196,20 @@ export default class ResponseHandler { _handleErrorPayload (payload) { const standardizedCode = _standardizeCode(payload.code) - const cause = payload.cause != null ? this._handleErrorPayload(payload.cause) : undefined + const cause = payload.cause != null ? this._handleErrorCause(payload.cause) : undefined const error = newError(payload.message, standardizedCode, cause, payload.gql_status, payload.description, payload.diagnostic_record) return this._observer.onErrorApplyTransformation( error ) } + + _handleErrorCause (payload) { + const cause = payload.cause != null ? this._handleErrorCause(payload.cause) : undefined + const error = newGQLError(payload.message, cause, payload.gql_status, payload.description, payload.diagnostic_record) + return this._observer.onErrorApplyTransformation( + error + ) + } } /** diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 4548ad1b6..78507e329 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -17,8 +17,10 @@ // A common place for constructing error objects, to keep them // uniform across the driver surface. -import { NumberOrInteger } from './graph-types.ts' + import * as json from './json.ts' +import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants.ts' + export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' /** @@ -69,46 +71,33 @@ type Neo4jErrorCode = | typeof NOT_AVAILABLE /// TODO: Remove definitions of this.constructor and this.__proto__ -/** - * Class for all errors thrown/returned by the driver. - */ -class Neo4jError extends Error { - /** - * Optional error code. Will be populated when error originates in the database. - */ - code: Neo4jErrorCode + + +class GQLError extends Error { gqlStatus: string gqlStatusDescription: string - diagnosticRecord: ErrorDiagnosticRecord | undefined + diagnosticRecord: DiagnosticRecord | undefined classification: ErrorClassification rawClassification?: string cause?: Error retriable: boolean - __proto__: Neo4jError + __proto__: GQLError /** * @constructor * @param {string} message - the error message - * @param {string} code - Optional error code. Will be populated when error originates in the database. - * @param {Neo4jError} cause - Optional error code. Will be populated when error originates in the database. - * @param {string} gqlStatus - the error message - * @param {string} gqlStatusDescription - the error message - * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {string} gqlStatus - the GQL status code of the error + * @param {string} gqlStatusDescription - the GQL status description of the error + * @param {ErrorDiagnosticRecord} diagnosticRecord - the error diagnostic record + * @param {Error} cause - Optional nested error, the cause of the error */ - constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: ErrorDiagnosticRecord, cause?: Error) { + constructor (message: string, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: DiagnosticRecord, cause?: Error) { // eslint-disable-next-line // @ts-ignore: not available in ES6 yet super(message, cause != null ? { cause } : undefined) - this.constructor = Neo4jError + this.constructor = GQLError // eslint-disable-next-line no-proto - this.__proto__ = Neo4jError.prototype - /** - * The Neo4j Error code - * - * @type {string} - * @public - */ - this.code = code + this.__proto__ = GQLError.prototype /** * Optional, nested error which caused the error * @@ -133,7 +122,7 @@ class Neo4jError extends Error { /** * The GQL diagnostic record * - * @type {ErrorDiagnosticRecord} + * @type {DiagnosticRecord} * @public */ this.diagnosticRecord = diagnosticRecord @@ -151,12 +140,59 @@ class Neo4jError extends Error { * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined - this.name = 'Neo4jError' + this.name = 'GQLError' + } + + /** + * 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, { useCustomToString: true }) + } +} + + +/** + * Class for all errors thrown/returned by the driver. + */ +class Neo4jError extends GQLError { + /** + * Optional error code. Will be populated when error originates in the database. + */ + code: string + + /** + * @constructor + * @param {string} message - the error message + * @param {string} code - Optional error code. Will be populated when error originates in the database. + * @param {string} gqlStatus - the GQL status code of the error + * @param {string} gqlStatusDescription - the GQL status description of the error + * @param {DiagnosticRecord} diagnosticRecord - the error diagnostic record + * @param {Error} cause - Optional nested error, the cause of the error + */ + constructor (message: string, code: Neo4jErrorCode, gqlStatus: string, gqlStatusDescription: string, diagnosticRecord?: DiagnosticRecord, cause?: Error) { + super(message, gqlStatus, gqlStatusDescription, diagnosticRecord, cause) + this.constructor = Neo4jError + // eslint-disable-next-line no-proto + this.__proto__ = Neo4jError.prototype /** - * Indicates if the error is retriable. - * @type {boolean} - true if the error is retriable + * The Neo4j Error code + * + * @type {string} + * @public */ - this.retriable = _isRetriableCode(code) + this.code = code + + this.name = 'Neo4jError' + this.retriable = _isRetriableCode(code) + } + + toString(): string { + return this.name + ": " + this.message } /** @@ -168,37 +204,42 @@ class Neo4jError extends Error { static isRetriable (error?: any | null): boolean { return error !== null && error !== undefined && - error instanceof Neo4jError && + error instanceof GQLError && error.retriable } - - /** - * 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, { useCustomToString: true }) - } } + /** - * Create a new error from a message and error code + * Create a new error from a message and optional data * @param message the error message * @param {Neo4jErrorCode} [code] the error code * @param {Neo4jError} [cause] * @param {String} [gqlStatus] * @param {String} [gqlStatusDescription] - * @param {ErrorDiagnosticRecord} diagnosticRecord - the error message + * @param {DiagnosticRecord} diagnosticRecord - the error message + * @return {Neo4jError} an {@link Neo4jError} + * @private + */ +function newError (message: string, code?: Neo4jErrorCode, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): Neo4jError { + return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord ?? rawPolyfilledDiagnosticRecord, cause) +} + +/** + * Create a new GQL error from a message and optional data + * @param message the error message + * @param {Neo4jError} [cause] + * @param {String} [gqlStatus] + * @param {String} [gqlStatusDescription] + * @param {DiagnosticRecord} diagnosticRecord - the error message * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode, cause?: Neo4jError, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: ErrorDiagnosticRecord): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord, cause) +function newGQLError (message: string, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): GQLError { + return new GQLError(message, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord ?? rawPolyfilledDiagnosticRecord, cause) } + /** * Verifies if the given error is retriable. * @@ -241,35 +282,19 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { /** * extracts a typed classification from the diagnostic record. */ -function extractClassification (diagnosticRecord?: ErrorDiagnosticRecord): ErrorClassification { +function extractClassification (diagnosticRecord?: any): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' } return classifications.includes(diagnosticRecord._classification) ? diagnosticRecord?._classification : 'UNKNOWN' } -/** - * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. - */ -interface ErrorDiagnosticRecord { - OPERATION: string - OPERATION_CODE: string - CURRENT_SCHEMA: string - _severity?: string - _classification?: ErrorClassification - _position?: { - offset: NumberOrInteger - line: NumberOrInteger - column: NumberOrInteger - } - _status_parameters?: Record - [key: string]: unknown -} - export { newError, + newGQLError, isRetriableError, Neo4jError, + GQLError, SERVICE_UNAVAILABLE, SESSION_EXPIRED, PROTOCOL_ERROR diff --git a/packages/neo4j-driver-deno/lib/core/gql-constants.ts b/packages/neo4j-driver-deno/lib/core/gql-constants.ts new file mode 100644 index 000000000..e9c926b09 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/gql-constants.ts @@ -0,0 +1,28 @@ +import { NumberOrInteger } from './graph-types.ts' + +/** + * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. + */ +export interface DiagnosticRecord { + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: string + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger + } + _status_parameters?: Record + [key: string]: unknown + } + + export const rawPolyfilledDiagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' + } + + Object.freeze(rawPolyfilledDiagnosticRecord) + \ No newline at end of file diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 15813a6a2..1a0a7203f 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -18,6 +18,8 @@ import { newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE, @@ -116,6 +118,8 @@ const forExport = { authTokenManagers, newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, error, Integer, @@ -189,6 +193,8 @@ export { authTokenManagers, newError, Neo4jError, + newGQLError, + GQLError, isRetriableError, error, Integer, diff --git a/packages/neo4j-driver-deno/lib/core/notification.ts b/packages/neo4j-driver-deno/lib/core/notification.ts index 97b43baec..ff8b3e898 100644 --- a/packages/neo4j-driver-deno/lib/core/notification.ts +++ b/packages/neo4j-driver-deno/lib/core/notification.ts @@ -16,7 +16,7 @@ */ import * as json from './json.ts' import { util } from './internal/index.ts' -import { NumberOrInteger } from './graph-types.ts' +import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants.ts' interface NotificationPosition { offset?: number @@ -224,21 +224,6 @@ class Notification { } } -interface NotificationDiagnosticRecord { - OPERATION: string - OPERATION_CODE: string - CURRENT_SCHEMA: string - _severity?: string - _classification?: string - _position?: { - offset: NumberOrInteger - line: NumberOrInteger - column: NumberOrInteger - } - _status_parameters?: Record - [key: string]: unknown -} - /** * Representation for GqlStatusObject found when executing a query. *

@@ -251,7 +236,7 @@ interface NotificationDiagnosticRecord { class GqlStatusObject { public readonly gqlStatus: string public readonly statusDescription: string - public readonly diagnosticRecord: NotificationDiagnosticRecord + public readonly diagnosticRecord: DiagnosticRecord public readonly position?: NotificationPosition public readonly severity: NotificationSeverityLevel public readonly rawSeverity?: string @@ -420,7 +405,7 @@ function polyfillNotification (status: any): Notification | undefined { */ function polyfillGqlStatusObject (notification: any): GqlStatusObject { const defaultStatus = notification.severity === notificationSeverityLevel.WARNING ? unknownGqlStatus.WARNING : unknownGqlStatus.INFORMATION - const polyfilledRawObj: any & { diagnostic_record: NotificationDiagnosticRecord } = { + const polyfilledRawObj: any & { diagnostic_record: DiagnosticRecord } = { gql_status: defaultStatus.gql_status, status_description: notification.description ?? defaultStatus.status_description, neo4j_code: notification.code, @@ -445,14 +430,6 @@ function polyfillGqlStatusObject (notification: any): GqlStatusObject { return new GqlStatusObject(polyfilledRawObj) } -const rawPolyfilledDiagnosticRecord = { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/' -} - -Object.freeze(rawPolyfilledDiagnosticRecord) - /** * This objects are used for polyfilling the first status on the status list * @@ -598,6 +575,5 @@ export type { NotificationPosition, NotificationSeverityLevel, NotificationCategory, - NotificationClassification, - NotificationDiagnosticRecord + NotificationClassification } diff --git a/packages/neo4j-driver/test/internal/connection-channel.test.js b/packages/neo4j-driver/test/internal/connection-channel.test.js index a4aa40610..8a8bd9e91 100644 --- a/packages/neo4j-driver/test/internal/connection-channel.test.js +++ b/packages/neo4j-driver/test/internal/connection-channel.test.js @@ -300,7 +300,7 @@ describe('#integration ChannelConnection', () => { .then(() => done.fail('Should fail')) .catch(error => { expect(error.message).toEqual( - 'Received FAILURE as a response for RESET: Neo4jError' + 'Received FAILURE as a response for RESET: Neo4jError: Hello' ) expect(connection._isBroken).toBeTruthy() expect(connection.isOpen()).toBeFalsy() From 54df47b2c730d3ec02ee60e494077fb43588f77d Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:50:01 +0200 Subject: [PATCH 31/35] cleanup --- packages/neo4j-driver-deno/lib/core/error.ts | 11 ++---- .../lib/core/gql-constants.ts | 39 +++++++++---------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 78507e329..2fe8eabb8 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -21,7 +21,6 @@ import * as json from './json.ts' import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants.ts' - export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' /** * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification @@ -72,7 +71,6 @@ type Neo4jErrorCode = /// TODO: Remove definitions of this.constructor and this.__proto__ - class GQLError extends Error { gqlStatus: string gqlStatusDescription: string @@ -155,7 +153,6 @@ class GQLError extends Error { } } - /** * Class for all errors thrown/returned by the driver. */ @@ -188,11 +185,11 @@ class Neo4jError extends GQLError { this.code = code this.name = 'Neo4jError' - this.retriable = _isRetriableCode(code) + this.retriable = _isRetriableCode(code) } - toString(): string { - return this.name + ": " + this.message + toString (): string { + return this.name + ': ' + this.message } /** @@ -209,7 +206,6 @@ class Neo4jError extends GQLError { } } - /** * Create a new error from a message and optional data * @param message the error message @@ -239,7 +235,6 @@ function newGQLError (message: string, cause?: Error, gqlStatus?: string, gqlSta return new GQLError(message, gqlStatus ?? '50N42', gqlStatusDescription ?? 'error: general processing exception - unexpected error. ' + message, diagnosticRecord ?? rawPolyfilledDiagnosticRecord, cause) } - /** * Verifies if the given error is retriable. * diff --git a/packages/neo4j-driver-deno/lib/core/gql-constants.ts b/packages/neo4j-driver-deno/lib/core/gql-constants.ts index e9c926b09..ad61eb0bf 100644 --- a/packages/neo4j-driver-deno/lib/core/gql-constants.ts +++ b/packages/neo4j-driver-deno/lib/core/gql-constants.ts @@ -4,25 +4,24 @@ import { NumberOrInteger } from './graph-types.ts' * Class for the DiagnosticRecord in a {@link Neo4jError}, including commonly used fields. */ export interface DiagnosticRecord { - OPERATION: string - OPERATION_CODE: string - CURRENT_SCHEMA: string - _severity?: string - _classification?: string - _position?: { - offset: NumberOrInteger - line: NumberOrInteger - column: NumberOrInteger - } - _status_parameters?: Record - [key: string]: unknown + OPERATION: string + OPERATION_CODE: string + CURRENT_SCHEMA: string + _severity?: string + _classification?: string + _position?: { + offset: NumberOrInteger + line: NumberOrInteger + column: NumberOrInteger } + _status_parameters?: Record + [key: string]: unknown +} - export const rawPolyfilledDiagnosticRecord = { - OPERATION: '', - OPERATION_CODE: '0', - CURRENT_SCHEMA: '/' - } - - Object.freeze(rawPolyfilledDiagnosticRecord) - \ No newline at end of file +export const rawPolyfilledDiagnosticRecord = { + OPERATION: '', + OPERATION_CODE: '0', + CURRENT_SCHEMA: '/' +} + +Object.freeze(rawPolyfilledDiagnosticRecord) From 780dcef37d62d05955d1d5ee7c3e31ae4601ae53 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:31:17 +0200 Subject: [PATCH 32/35] small error --- packages/core/src/error.ts | 4 ++-- packages/neo4j-driver-deno/lib/core/error.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index f60a7a2cf..d621d9f94 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -78,7 +78,6 @@ class GQLError extends Error { classification: ErrorClassification rawClassification?: string cause?: Error - retriable: boolean __proto__: GQLError /** @@ -161,6 +160,7 @@ class Neo4jError extends GQLError { * Optional error code. Will be populated when error originates in the database. */ code: string + retriable: boolean /** * @constructor @@ -201,7 +201,7 @@ class Neo4jError extends GQLError { static isRetriable (error?: any | null): boolean { return error !== null && error !== undefined && - error instanceof GQLError && + error instanceof Neo4jError && error.retriable } } diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 2fe8eabb8..4ce653b80 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -78,7 +78,6 @@ class GQLError extends Error { classification: ErrorClassification rawClassification?: string cause?: Error - retriable: boolean __proto__: GQLError /** @@ -161,6 +160,7 @@ class Neo4jError extends GQLError { * Optional error code. Will be populated when error originates in the database. */ code: string + retriable: boolean /** * @constructor @@ -201,7 +201,7 @@ class Neo4jError extends GQLError { static isRetriable (error?: any | null): boolean { return error !== null && error !== undefined && - error instanceof GQLError && + error instanceof Neo4jError && error.retriable } } From 4cdddde6968cb0108a61b4b7af90841aadb4d640 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:12:27 +0200 Subject: [PATCH 33/35] connection channel integration test correction --- packages/bolt-connection/src/connection/connection-channel.js | 2 +- packages/core/src/error.ts | 4 ---- .../lib/bolt-connection/connection/connection-channel.js | 2 +- packages/neo4j-driver-deno/lib/core/error.ts | 4 ---- .../neo4j-driver/test/internal/connection-channel.test.js | 2 +- 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index b056e3e1b..bf4e65ca7 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -441,7 +441,7 @@ export default class ChannelConnection extends Connection { reject(error) } else { const neo4jError = this._handleProtocolError( - 'Received FAILURE as a response for RESET: ' + error + `Received FAILURE as a response for RESET: ${error}` ) reject(neo4jError) } diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index d621d9f94..4c97dc1e0 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -188,10 +188,6 @@ class Neo4jError extends GQLError { this.retriable = _isRetriableCode(code) } - toString (): string { - return this.name + ': ' + this.message - } - /** * Verifies if the given error is retriable. * diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 12ad7fc86..0da4592db 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -441,7 +441,7 @@ export default class ChannelConnection extends Connection { reject(error) } else { const neo4jError = this._handleProtocolError( - 'Received FAILURE as a response for RESET: ' + error + `Received FAILURE as a response for RESET: ${error}` ) reject(neo4jError) } diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index 4ce653b80..c24c9e65a 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -188,10 +188,6 @@ class Neo4jError extends GQLError { this.retriable = _isRetriableCode(code) } - toString (): string { - return this.name + ': ' + this.message - } - /** * Verifies if the given error is retriable. * diff --git a/packages/neo4j-driver/test/internal/connection-channel.test.js b/packages/neo4j-driver/test/internal/connection-channel.test.js index 8a8bd9e91..34e94663d 100644 --- a/packages/neo4j-driver/test/internal/connection-channel.test.js +++ b/packages/neo4j-driver/test/internal/connection-channel.test.js @@ -42,7 +42,7 @@ const { SERVICE_UNAVAILABLE } = error const ILLEGAL_MESSAGE = { signature: 42, fields: [] } const SUCCESS_MESSAGE = { signature: 0x70, fields: [{}] } -const FAILURE_MESSAGE = { signature: 0x7f, fields: [newError('Hello')] } +const FAILURE_MESSAGE = { signature: 0x7f, fields: [{ message: 'Hello' }] } const RECORD_MESSAGE = { signature: 0x71, fields: [{ value: 'Hello' }] } const BOLT_AGENT = { From 5b67b671fb774a4d9df9b08a65dfa2ffcdf1f9da Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:50:50 +0200 Subject: [PATCH 34/35] Update testkit.json --- testkit/testkit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testkit/testkit.json b/testkit/testkit.json index bd5e1f7c1..931900356 100644 --- a/testkit/testkit.json +++ b/testkit/testkit.json @@ -1,6 +1,6 @@ { "testkit": { "uri": "https://github.com/neo4j-drivers/testkit.git", - "ref": "bolt-5x7-and-gql-errors" + "ref": "5.0" } } From 1dfa32c9cfacfdd34380d640476e938c3fce607f Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:01:21 +0200 Subject: [PATCH 35/35] minor test change and marking changes as preview --- .../test/bolt/response-handler.test.js | 4 ++-- packages/core/src/error.ts | 16 ++++++++++++++-- packages/neo4j-driver-deno/lib/core/error.ts | 16 ++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/test/bolt/response-handler.test.js b/packages/bolt-connection/test/bolt/response-handler.test.js index 30e32bddb..196ea65fb 100644 --- a/packages/bolt-connection/test/bolt/response-handler.test.js +++ b/packages/bolt-connection/test/bolt/response-handler.test.js @@ -136,7 +136,7 @@ describe('response-handler', () => { expect(receivedError.message).toBe(errorPayload.message) expect(receivedError.gqlStatus).toBe(errorPayload.gql_status) expect(receivedError.gqlStatusDescription).toBe(errorPayload.description) - testDiagnosticRecord(receivedError.diagnosticRecord, errorPayload.diagnostic_record) + testDiagnosticRecord(receivedError.diagnosticRecord, { ...errorPayload.diagnostic_record, OPERATION: '' }) testDiagnosticRecord(receivedError.cause.diagnosticRecord, errorPayload.cause.diagnostic_record) expect(receivedError.classification).toBe(errorPayload.diagnostic_record._classification) expect(receivedError.cause.classification).toBe(errorPayload.cause.diagnostic_record._classification) @@ -144,7 +144,7 @@ describe('response-handler', () => { }) function testDiagnosticRecord (diagnostic_record, expected_diagnostic_record) { - expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION ?? '') + expect(diagnostic_record.OPERATION).toBe(expected_diagnostic_record.OPERATION) expect(diagnostic_record.CURRENT_SCHEMA).toBe(expected_diagnostic_record.CURRENT_SCHEMA) expect(diagnostic_record.OPERATION_CODE).toBe(expected_diagnostic_record.OPERATION_CODE) expect(diagnostic_record.additional_thing).toBe(expected_diagnostic_record.additional_thing) diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 4c97dc1e0..40386ddf8 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -24,6 +24,7 @@ import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' /** * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification + * @experimental this is part of the preview of GQL-compliant errors */ const errorClassification: { [key in ErrorClassification]: key } = { @@ -71,6 +72,10 @@ type Neo4jErrorCode = /// TODO: Remove definitions of this.constructor and this.__proto__ +/** + * Class for nested errors, to be used as causes in {@link Neo4jError} + * @experimental this class is part of the preview of GQL-compliant errors + */ class GQLError extends Error { gqlStatus: string gqlStatusDescription: string @@ -106,6 +111,7 @@ class GQLError extends Error { * The GQL Status code * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.gqlStatus = gqlStatus @@ -113,6 +119,7 @@ class GQLError extends Error { * The GQL Status Description * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.gqlStatusDescription = gqlStatusDescription @@ -120,6 +127,7 @@ class GQLError extends Error { * The GQL diagnostic record * * @type {DiagnosticRecord} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.diagnosticRecord = diagnosticRecord @@ -127,13 +135,15 @@ class GQLError extends Error { * The GQL error classification, extracted from the diagnostic record * * @type {ErrorClassification} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ - this.classification = extractClassification(this.diagnosticRecord) + this.classification = _extractClassification(this.diagnosticRecord) /** * The GQL error classification, extracted from the diagnostic record as a raw string * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined @@ -145,6 +155,7 @@ class GQLError extends Error { * The goal of this method is provide a serialized object for human inspection. * * @type {string} + * @experimental this is part of the preview of GQL-compliant errors * @public */ public get diagnosticRecordAsJsonString (): string { @@ -225,6 +236,7 @@ function newError (message: string, code?: Neo4jErrorCode, cause?: Error, gqlSta * @param {String} [gqlStatusDescription] * @param {DiagnosticRecord} diagnosticRecord - the error message * @return {Neo4jError} an {@link Neo4jError} + * @experimental this is part of the preview of GQL-compliant errors * @private */ function newGQLError (message: string, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): GQLError { @@ -273,7 +285,7 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { /** * extracts a typed classification from the diagnostic record. */ -function extractClassification (diagnosticRecord?: any): ErrorClassification { +function _extractClassification (diagnosticRecord?: any): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' } diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts index c24c9e65a..b00bb92dc 100644 --- a/packages/neo4j-driver-deno/lib/core/error.ts +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -24,6 +24,7 @@ import { DiagnosticRecord, rawPolyfilledDiagnosticRecord } from './gql-constants export type ErrorClassification = 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' /** * @typedef { 'DATABASE_ERROR' | 'CLIENT_ERROR' | 'TRANSIENT_ERROR' | 'UNKNOWN' } ErrorClassification + * @experimental this is part of the preview of GQL-compliant errors */ const errorClassification: { [key in ErrorClassification]: key } = { @@ -71,6 +72,10 @@ type Neo4jErrorCode = /// TODO: Remove definitions of this.constructor and this.__proto__ +/** + * Class for nested errors, to be used as causes in {@link Neo4jError} + * @experimental this class is part of the preview of GQL-compliant errors + */ class GQLError extends Error { gqlStatus: string gqlStatusDescription: string @@ -106,6 +111,7 @@ class GQLError extends Error { * The GQL Status code * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.gqlStatus = gqlStatus @@ -113,6 +119,7 @@ class GQLError extends Error { * The GQL Status Description * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.gqlStatusDescription = gqlStatusDescription @@ -120,6 +127,7 @@ class GQLError extends Error { * The GQL diagnostic record * * @type {DiagnosticRecord} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.diagnosticRecord = diagnosticRecord @@ -127,13 +135,15 @@ class GQLError extends Error { * The GQL error classification, extracted from the diagnostic record * * @type {ErrorClassification} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ - this.classification = extractClassification(this.diagnosticRecord) + this.classification = _extractClassification(this.diagnosticRecord) /** * The GQL error classification, extracted from the diagnostic record as a raw string * * @type {string} + * @experimental this property is part of the preview of GQL-compliant errors * @public */ this.rawClassification = diagnosticRecord?._classification ?? undefined @@ -145,6 +155,7 @@ class GQLError extends Error { * The goal of this method is provide a serialized object for human inspection. * * @type {string} + * @experimental this is part of the preview of GQL-compliant errors * @public */ public get diagnosticRecordAsJsonString (): string { @@ -225,6 +236,7 @@ function newError (message: string, code?: Neo4jErrorCode, cause?: Error, gqlSta * @param {String} [gqlStatusDescription] * @param {DiagnosticRecord} diagnosticRecord - the error message * @return {Neo4jError} an {@link Neo4jError} + * @experimental this is part of the preview of GQL-compliant errors * @private */ function newGQLError (message: string, cause?: Error, gqlStatus?: string, gqlStatusDescription?: string, diagnosticRecord?: DiagnosticRecord): GQLError { @@ -273,7 +285,7 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { /** * extracts a typed classification from the diagnostic record. */ -function extractClassification (diagnosticRecord?: any): ErrorClassification { +function _extractClassification (diagnosticRecord?: any): ErrorClassification { if (diagnosticRecord === undefined || diagnosticRecord._classification === undefined) { return 'UNKNOWN' }