From 3f856b50f637d52df56be6a23a2dc3404cebb8a2 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 23 Mar 2021 15:28:06 +0100 Subject: [PATCH 1/5] Add support for BigInt Recent V8 release adds a new type - BigInt. It is thus available in latest NodeJS (v10+) and Chrome (v67+). Driver could use this type instead of the custom Integer to represent lossy integer numbers. This is configurable because BigInt and Number are not fully compatible, so driver can't always return BitInts. For example `BigInt(1) / Number(1)` results in a TypeError, as well as `JSON.stringify({v: BigInt(1)})`. The configuration property is `useBigInt` and it's `false` by default, in manner to break drivers user on update. This configuration take precedence over `disableLosslessIntegers` --- bolt-connection/jest.config.js | 8 +- bolt-connection/package-lock.json | 1 + bolt-connection/src/bolt/bolt-protocol-v1.js | 12 +- bolt-connection/src/bolt/bolt-protocol-v2.js | 4 +- .../src/bolt/bolt-protocol-v4x1.js | 8 +- bolt-connection/src/bolt/create.js | 20 +- .../src/connection/connection-channel.js | 1 + .../src/packstream/packstream-v1.js | 14 +- .../src/packstream/packstream-v2.js | 80 +++++--- .../test}/bolt/bolt-protocol-v1.test.js | 36 +++- .../test}/bolt/bolt-protocol-v2.test.js | 25 ++- .../test}/bolt/bolt-protocol-v3.test.js | 40 +++- .../test}/bolt/bolt-protocol-v4x0.test.js | 32 +++- .../test/bolt/bolt-protocol-v4x1.test.js | 43 +++++ .../test/bolt/bolt-protocol-v4x2.test.js | 43 +++++ .../test}/bolt/bolt-protocol-v4x3.test.js | 32 +++- .../test}/bolt/index.test.js | 51 +++-- .../test}/bolt/request-message.test.js | 14 +- .../test}/bolt/routing-table-raw.test.js | 2 +- .../test}/bolt/stream-observer.test.js | 4 +- bolt-connection/test/dummy-channel.js | 62 ++++++ .../test/packstream/packstream-v1.test.js | 179 ++++++++++++++++++ bolt-connection/test/test-utils.js | 139 ++++++++++++++ core/src/graph-types.ts | 2 +- core/src/integer.ts | 35 +++- core/src/internal/temporal-util.ts | 132 +++++++------ core/src/internal/util.ts | 5 +- core/src/result-summary.ts | 12 +- core/src/spatial-types.ts | 5 +- core/src/temporal-types.ts | 6 +- core/test/integer.test.ts | 6 + neo4j-driver-lite/src/index.ts | 4 + src/index.js | 4 + test/internal/packstream-v1.test.js | 86 --------- test/internal/temporal-util.test.js | 13 ++ testkit-backend/src/cypher-native-binders.js | 5 +- testkit-backend/src/request-handlers.js | 2 +- 37 files changed, 903 insertions(+), 264 deletions(-) rename {test/internal => bolt-connection/test}/bolt/bolt-protocol-v1.test.js (90%) rename {test/internal => bolt-connection/test}/bolt/bolt-protocol-v2.test.js (57%) rename {test/internal => bolt-connection/test}/bolt/bolt-protocol-v3.test.js (88%) rename {test/internal => bolt-connection/test}/bolt/bolt-protocol-v4x0.test.js (84%) create mode 100644 bolt-connection/test/bolt/bolt-protocol-v4x1.test.js create mode 100644 bolt-connection/test/bolt/bolt-protocol-v4x2.test.js rename {test/internal => bolt-connection/test}/bolt/bolt-protocol-v4x3.test.js (88%) rename {test/internal => bolt-connection/test}/bolt/index.test.js (87%) rename {test/internal => bolt-connection/test}/bolt/request-message.test.js (95%) rename {test/internal => bolt-connection/test}/bolt/routing-table-raw.test.js (98%) rename {test/internal => bolt-connection/test}/bolt/stream-observer.test.js (99%) create mode 100644 bolt-connection/test/dummy-channel.js create mode 100644 bolt-connection/test/packstream/packstream-v1.test.js create mode 100644 bolt-connection/test/test-utils.js delete mode 100644 test/internal/packstream-v1.test.js diff --git a/bolt-connection/jest.config.js b/bolt-connection/jest.config.js index 6768d6d3b..cd92d7f01 100644 --- a/bolt-connection/jest.config.js +++ b/bolt-connection/jest.config.js @@ -137,7 +137,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: 'node' + testEnvironment: 'node', // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, @@ -172,9 +172,9 @@ module.exports = { // timers: "real", // A map from regular expressions to paths to transformers - // transform: { - // "^.+\\.(ts|tsx)$": "ts-jest" - // } + transform: { + '^.+\\.(ts|tsx|js)$': 'ts-jest' + } // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "/node_modules/", diff --git a/bolt-connection/package-lock.json b/bolt-connection/package-lock.json index a12889a6a..178e42851 100644 --- a/bolt-connection/package-lock.json +++ b/bolt-connection/package-lock.json @@ -21,6 +21,7 @@ } }, "../core": { + "name": "neo4j-driver-core", "version": "4.3.0", "license": "Apache-2.0", "dependencies": { diff --git a/bolt-connection/src/bolt/bolt-protocol-v1.js b/bolt-connection/src/bolt/bolt-protocol-v1.js index 36e236b6d..a5302ba63 100644 --- a/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -52,7 +52,9 @@ export default class BoltProtocol { * @constructor * @param {Object} server the server informatio. * @param {Chunker} chunker the chunker. - * @param {boolean} disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {Object} packstreamConfig Packstream configuration + * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {OnProtocolError} onProtocolError handles protocol errors @@ -60,7 +62,7 @@ export default class BoltProtocol { constructor ( server, chunker, - disableLosslessIntegers, + { disableLosslessIntegers, useBigInt } = {}, createResponseHandler = () => null, log, onProtocolError @@ -68,7 +70,7 @@ export default class BoltProtocol { this._server = server || {} this._chunker = chunker this._packer = this._createPacker(chunker) - this._unpacker = this._createUnpacker(disableLosslessIntegers) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) this._responseHandler = createResponseHandler(this) this._log = log this._onProtocolError = onProtocolError @@ -317,8 +319,8 @@ export default class BoltProtocol { return new v1.Packer(chunker) } - _createUnpacker (disableLosslessIntegers) { - return new v1.Unpacker(disableLosslessIntegers) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt) } /** diff --git a/bolt-connection/src/bolt/bolt-protocol-v2.js b/bolt-connection/src/bolt/bolt-protocol-v2.js index bb16b0727..13d007d87 100644 --- a/bolt-connection/src/bolt/bolt-protocol-v2.js +++ b/bolt-connection/src/bolt/bolt-protocol-v2.js @@ -29,8 +29,8 @@ export default class BoltProtocol extends BoltProtocolV1 { return new v2.Packer(chunker) } - _createUnpacker (disableLosslessIntegers) { - return new v2.Unpacker(disableLosslessIntegers) + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt) } get version () { diff --git a/bolt-connection/src/bolt/bolt-protocol-v4x1.js b/bolt-connection/src/bolt/bolt-protocol-v4x1.js index f175e5ba1..9bd0439d2 100644 --- a/bolt-connection/src/bolt/bolt-protocol-v4x1.js +++ b/bolt-connection/src/bolt/bolt-protocol-v4x1.js @@ -30,7 +30,9 @@ export default class BoltProtocol extends BoltProtocolV4 { * @constructor * @param {Object} server the server informatio. * @param {Chunker} chunker the chunker. - * @param {boolean} disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {Object} packstreamConfig Packstream configuration + * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler * @param {Logger} log the logger * @param {Object} serversideRouting @@ -39,7 +41,7 @@ export default class BoltProtocol extends BoltProtocolV4 { constructor ( server, chunker, - disableLosslessIntegers, + packstreamConfig, createResponseHandler = () => null, log, onProtocolError, @@ -48,7 +50,7 @@ export default class BoltProtocol extends BoltProtocolV4 { super( server, chunker, - disableLosslessIntegers, + packstreamConfig, createResponseHandler, log, onProtocolError diff --git a/bolt-connection/src/bolt/create.js b/bolt-connection/src/bolt/create.js index ce90a5f80..0f8721b25 100644 --- a/bolt-connection/src/bolt/create.js +++ b/bolt-connection/src/bolt/create.js @@ -39,6 +39,7 @@ import ResponseHandler from './response-handler' * @param {Logger} config.log The logger * @param {ResponseHandler~Observer} config.observer Observer * @param {boolean} config.disableLosslessIntegers Disable the lossless integers + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. * @param {boolean} config.serversideRouting It's using server side routing */ export default function create ({ @@ -47,6 +48,7 @@ export default function create ({ dechunker, channel, disableLosslessIntegers, + useBigInt, serversideRouting, server, // server info log, @@ -77,7 +79,7 @@ export default function create ({ version, server, chunker, - disableLosslessIntegers, + { disableLosslessIntegers, useBigInt }, serversideRouting, createResponseHandler, observer.onProtocolError.bind(observer), @@ -89,7 +91,7 @@ function createProtocol ( version, server, chunker, - disableLosslessIntegers, + packingConfig, serversideRouting, createResponseHandler, onProtocolError, @@ -100,7 +102,7 @@ function createProtocol ( return new BoltProtocolV1( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError @@ -109,7 +111,7 @@ function createProtocol ( return new BoltProtocolV2( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError @@ -118,7 +120,7 @@ function createProtocol ( return new BoltProtocolV3( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError @@ -127,7 +129,7 @@ function createProtocol ( return new BoltProtocolV4x0( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError @@ -136,7 +138,7 @@ function createProtocol ( return new BoltProtocolV4x1( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError, @@ -146,7 +148,7 @@ function createProtocol ( return new BoltProtocolV4x2( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError, @@ -156,7 +158,7 @@ function createProtocol ( return new BoltProtocolV4x3( server, chunker, - disableLosslessIntegers, + packingConfig, createResponseHandler, log, onProtocolError, diff --git a/bolt-connection/src/connection/connection-channel.js b/bolt-connection/src/connection/connection-channel.js index 979c7fdf6..77136dfff 100644 --- a/bolt-connection/src/connection/connection-channel.js +++ b/bolt-connection/src/connection/connection-channel.js @@ -62,6 +62,7 @@ export function createChannelConnection ( chunker, dechunker, disableLosslessIntegers: config.disableLosslessIntegers, + useBigInt: config.useBigInt, serversideRouting, server: conn.server, log, diff --git a/bolt-connection/src/packstream/packstream-v1.js b/bolt-connection/src/packstream/packstream-v1.js index b5610ac90..d2b438b68 100644 --- a/bolt-connection/src/packstream/packstream-v1.js +++ b/bolt-connection/src/packstream/packstream-v1.js @@ -126,6 +126,8 @@ class Packer { return () => this.packFloat(x) } else if (typeof x === 'string') { return () => this.packString(x) + } else if (typeof x === 'bigint') { + return () => this.packInteger(int(x)) } else if (isInt(x)) { return () => this.packInteger(x) } else if (x instanceof Int8Array) { @@ -368,9 +370,11 @@ class Unpacker { /** * @constructor * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. + * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false) { + constructor (disableLosslessIntegers = false, useBigInt = false) { this._disableLosslessIntegers = disableLosslessIntegers + this._useBigInt = useBigInt } unpack (buffer) { @@ -389,8 +393,12 @@ class Unpacker { const numberOrInteger = this._unpackNumberOrInteger(marker, buffer) if (numberOrInteger !== null) { - if (this._disableLosslessIntegers && isInt(numberOrInteger)) { - return numberOrInteger.toNumberOrInfinity() + if (isInt(numberOrInteger)) { + if (this._useBigInt) { + return numberOrInteger.toBigInt() + } else if (this._disableLosslessIntegers) { + return numberOrInteger.toNumberOrInfinity() + } } return numberOrInteger } diff --git a/bolt-connection/src/packstream/packstream-v2.js b/bolt-connection/src/packstream/packstream-v2.js index a12135317..d1c1b8ed2 100644 --- a/bolt-connection/src/packstream/packstream-v2.js +++ b/bolt-connection/src/packstream/packstream-v2.js @@ -106,9 +106,10 @@ export class Unpacker extends v1.Unpacker { /** * @constructor * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. + * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint */ - constructor (disableLosslessIntegers = false) { - super(disableLosslessIntegers) + constructor (disableLosslessIntegers = false, useBigInt = false) { + super(disableLosslessIntegers, useBigInt) } _unpackUnknownStruct (signature, structSize, buffer) { @@ -123,39 +124,56 @@ export class Unpacker extends v1.Unpacker { this, structSize, buffer, - this._disableLosslessIntegers + this._disableLosslessIntegers, + this._useBigInt ) } else if (signature === TIME) { - return unpackTime(this, structSize, buffer, this._disableLosslessIntegers) + return unpackTime( + this, + structSize, + buffer, + this._disableLosslessIntegers, + this._useBigInt + ) } else if (signature === DATE) { - return unpackDate(this, structSize, buffer, this._disableLosslessIntegers) + return unpackDate( + this, + structSize, + buffer, + this._disableLosslessIntegers, + this._useBigInt + ) } else if (signature === LOCAL_DATE_TIME) { return unpackLocalDateTime( this, structSize, buffer, - this._disableLosslessIntegers + this._disableLosslessIntegers, + this._useBigInt ) } else if (signature === DATE_TIME_WITH_ZONE_OFFSET) { return unpackDateTimeWithZoneOffset( this, structSize, buffer, - this._disableLosslessIntegers + this._disableLosslessIntegers, + this._useBigInt ) } else if (signature === DATE_TIME_WITH_ZONE_ID) { return unpackDateTimeWithZoneId( this, structSize, buffer, - this._disableLosslessIntegers + this._disableLosslessIntegers, + this._useBigInt ) } else { return super._unpackUnknownStruct( signature, structSize, buffer, - this._disableLosslessIntegers + this._disableLosslessIntegers, + this._useBigInt ) } } @@ -345,7 +363,13 @@ function packTime (value, packer) { * @param {boolean} disableLosslessIntegers if integer properties in the result time should be native JS numbers. * @return {Time} the unpacked time value. */ -function unpackTime (unpacker, structSize, buffer, disableLosslessIntegers) { +function unpackTime ( + unpacker, + structSize, + buffer, + disableLosslessIntegers, + useBigInt +) { unpacker._verifyStructSize('Time', TIME_STRUCT_SIZE, structSize) const nanoOfDay = unpacker.unpackInteger(buffer) @@ -359,7 +383,7 @@ function unpackTime (unpacker, structSize, buffer, disableLosslessIntegers) { localTime.nanosecond, offsetSeconds ) - return convertIntegerPropsIfNeeded(result, disableLosslessIntegers) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) } /** @@ -382,12 +406,18 @@ function packDate (value, packer) { * @param {boolean} disableLosslessIntegers if integer properties in the result date should be native JS numbers. * @return {Date} the unpacked neo4j date value. */ -function unpackDate (unpacker, structSize, buffer, disableLosslessIntegers) { +function unpackDate ( + unpacker, + structSize, + buffer, + disableLosslessIntegers, + useBigInt +) { unpacker._verifyStructSize('Date', DATE_STRUCT_SIZE, structSize) const epochDay = unpacker.unpackInteger(buffer) const result = epochDayToDate(epochDay) - return convertIntegerPropsIfNeeded(result, disableLosslessIntegers) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) } /** @@ -426,7 +456,8 @@ function unpackLocalDateTime ( unpacker, structSize, buffer, - disableLosslessIntegers + disableLosslessIntegers, + useBigInt ) { unpacker._verifyStructSize( 'LocalDateTime', @@ -437,7 +468,7 @@ function unpackLocalDateTime ( const epochSecond = unpacker.unpackInteger(buffer) const nano = unpacker.unpackInteger(buffer) const result = epochSecondAndNanoToLocalDateTime(epochSecond, nano) - return convertIntegerPropsIfNeeded(result, disableLosslessIntegers) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) } /** @@ -491,7 +522,8 @@ function unpackDateTimeWithZoneOffset ( unpacker, structSize, buffer, - disableLosslessIntegers + disableLosslessIntegers, + useBigInt ) { unpacker._verifyStructSize( 'DateTimeWithZoneOffset', @@ -515,7 +547,7 @@ function unpackDateTimeWithZoneOffset ( timeZoneOffsetSeconds, null ) - return convertIntegerPropsIfNeeded(result, disableLosslessIntegers) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) } /** @@ -556,7 +588,8 @@ function unpackDateTimeWithZoneId ( unpacker, structSize, buffer, - disableLosslessIntegers + disableLosslessIntegers, + useBigInt ) { unpacker._verifyStructSize( 'DateTimeWithZoneId', @@ -580,19 +613,22 @@ function unpackDateTimeWithZoneId ( null, timeZoneId ) - return convertIntegerPropsIfNeeded(result, disableLosslessIntegers) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) } -function convertIntegerPropsIfNeeded (obj, disableLosslessIntegers) { - if (!disableLosslessIntegers) { +function convertIntegerPropsIfNeeded (obj, disableLosslessIntegers, useBigInt) { + if (!disableLosslessIntegers && !useBigInt) { return obj } + const convert = value => + useBigInt ? value.toBigInt() : value.toNumberOrInfinity() + const clone = Object.create(Object.getPrototypeOf(obj)) for (const prop in obj) { if (obj.hasOwnProperty(prop)) { const value = obj[prop] - clone[prop] = isInt(value) ? value.toNumberOrInfinity() : value + clone[prop] = isInt(value) ? convert(value) : value } } Object.freeze(clone) diff --git a/test/internal/bolt/bolt-protocol-v1.test.js b/bolt-connection/test/bolt/bolt-protocol-v1.test.js similarity index 90% rename from test/internal/bolt/bolt-protocol-v1.test.js rename to bolt-connection/test/bolt/bolt-protocol-v1.test.js index 1c3f4b593..b7e831e80 100644 --- a/test/internal/bolt/bolt-protocol-v1.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -17,12 +17,11 @@ * limitations under the License. */ -import BoltProtocolV1 from '../../../bolt-connection/lib/bolt/bolt-protocol-v1' -import RequestMessage from '../../../bolt-connection/lib/bolt/request-message' -import { WRITE } from '../../../src/driver' -import utils from '../test-utils' -import { LoginObserver } from '../../../bolt-connection/lib/bolt/stream-observers' +import BoltProtocolV1 from '../../src/bolt/bolt-protocol-v1' +import RequestMessage from '../../src/bolt/request-message' import { internal } from 'neo4j-driver-core' +import utils from '../test-utils' +import { LoginObserver } from '../../src/bolt/stream-observers' const { bookmark: { Bookmark }, @@ -31,7 +30,7 @@ const { describe('#unit BoltProtocolV1', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should not change metadata', () => { @@ -97,7 +96,7 @@ describe('#unit BoltProtocolV1', () => { const observer = protocol.run(query, parameters, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), - mode: WRITE + mode: 'WRITE' }) protocol.verifyMessageCount(2) @@ -135,7 +134,7 @@ describe('#unit BoltProtocolV1', () => { const observer = protocol.beginTransaction({ bookmark: bookmark, txConfig: TxConfig.empty(), - mode: WRITE + mode: 'WRITE' }) protocol.verifyMessageCount(2) @@ -275,4 +274,25 @@ describe('#unit BoltProtocolV1', () => { }) }) }) + + 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 BoltProtocolV1(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) }) diff --git a/test/internal/bolt/bolt-protocol-v2.test.js b/bolt-connection/test/bolt/bolt-protocol-v2.test.js similarity index 57% rename from test/internal/bolt/bolt-protocol-v2.test.js rename to bolt-connection/test/bolt/bolt-protocol-v2.test.js index 4a3da5a5c..c409f8026 100644 --- a/test/internal/bolt/bolt-protocol-v2.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v2.test.js @@ -17,12 +17,12 @@ * limitations under the License. */ -import BoltProtocolV2 from '../../../bolt-connection/lib/bolt/bolt-protocol-v2' +import BoltProtocolV2 from '../../src/bolt/bolt-protocol-v2' import utils from '../test-utils' describe('#unit BoltProtocolV2', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should return correct bolt version number', () => { @@ -30,4 +30,25 @@ describe('#unit BoltProtocolV2', () => { expect(protocol.version).toBe(2) }) + + 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 BoltProtocolV2(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) }) diff --git a/test/internal/bolt/bolt-protocol-v3.test.js b/bolt-connection/test/bolt/bolt-protocol-v3.test.js similarity index 88% rename from test/internal/bolt/bolt-protocol-v3.test.js rename to bolt-connection/test/bolt/bolt-protocol-v3.test.js index 8967eb55f..87a863c22 100644 --- a/test/internal/bolt/bolt-protocol-v3.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -17,15 +17,14 @@ * limitations under the License. */ -import BoltProtocolV3 from '../../../bolt-connection/lib/bolt/bolt-protocol-v3' -import RequestMessage from '../../../bolt-connection/lib/bolt/request-message' +import BoltProtocolV3 from '../../src/bolt/bolt-protocol-v3' +import RequestMessage from '../../src/bolt/request-message' import utils from '../test-utils' -import { WRITE } from '../../../src/driver' +import { internal } from 'neo4j-driver-core' import { ProcedureRouteObserver, ResultStreamObserver -} from '../../../bolt-connection/lib/bolt/stream-observers' -import { internal } from 'neo4j-driver-core' +} from '../../src/bolt/stream-observers' const { bookmark: { Bookmark }, @@ -34,7 +33,7 @@ const { describe('#unit BoltProtocolV3', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should update metadata', () => { @@ -88,7 +87,7 @@ describe('#unit BoltProtocolV3', () => { const observer = protocol.run(query, parameters, { bookmark, txConfig, - mode: WRITE + mode: 'WRITE' }) protocol.verifyMessageCount(2) @@ -97,7 +96,7 @@ describe('#unit BoltProtocolV3', () => { RequestMessage.runWithMetadata(query, parameters, { bookmark, txConfig, - mode: WRITE + mode: 'WRITE' }) ) expect(protocol.messages[1]).toBeMessage(RequestMessage.pullAll()) @@ -121,12 +120,12 @@ describe('#unit BoltProtocolV3', () => { const observer = protocol.beginTransaction({ bookmark, txConfig, - mode: WRITE + mode: 'WRITE' }) protocol.verifyMessageCount(1) expect(protocol.messages[0]).toBeMessage( - RequestMessage.begin({ bookmark, txConfig, mode: WRITE }) + RequestMessage.begin({ bookmark, txConfig, mode: 'WRITE' }) ) expect(protocol.observers).toEqual([observer]) expect(protocol.flushes).toEqual([true]) @@ -232,6 +231,27 @@ describe('#unit BoltProtocolV3', () => { }) }) }) + + 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 BoltProtocolV3(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) }) class SpiedBoltProtocolV3 extends BoltProtocolV3 { diff --git a/test/internal/bolt/bolt-protocol-v4x0.test.js b/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js similarity index 84% rename from test/internal/bolt/bolt-protocol-v4x0.test.js rename to bolt-connection/test/bolt/bolt-protocol-v4x0.test.js index bfa0f8bc8..46473d379 100644 --- a/test/internal/bolt/bolt-protocol-v4x0.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v4x0.test.js @@ -17,17 +17,18 @@ * limitations under the License. */ -import BoltProtocolV4x0 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x0' -import RequestMessage from '../../../bolt-connection/lib/bolt/request-message' +import BoltProtocolV4x0 from '../../src/bolt/bolt-protocol-v4x0' +import RequestMessage from '../../src/bolt/request-message' import utils from '../test-utils' -import { WRITE } from '../../../src/driver' import { ProcedureRouteObserver, ResultStreamObserver -} from '../../../bolt-connection/lib/bolt/stream-observers' +} from '../../src/bolt/stream-observers' import { internal } from 'neo4j-driver-core' +const WRITE = 'WRITE' + const { txConfig: { TxConfig }, bookmark: { Bookmark } @@ -35,7 +36,7 @@ const { describe('#unit BoltProtocolV4x0', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should run a query', () => { @@ -151,6 +152,27 @@ describe('#unit BoltProtocolV4x0', () => { { ...sessionContext, txConfig: TxConfig.empty() } ]) }) + + 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 BoltProtocolV4x0(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) }) class SpiedBoltProtocolV4x0 extends BoltProtocolV4x0 { diff --git a/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js b/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js new file mode 100644 index 000000000..e4d56bb4b --- /dev/null +++ b/bolt-connection/test/bolt/bolt-protocol-v4x1.test.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV4x1 from '../../src/bolt/bolt-protocol-v4x1' + +describe('#unit BoltProtocolV4x1', () => { + 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 BoltProtocolV4x1(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) +}) diff --git a/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js b/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js new file mode 100644 index 000000000..48d829c13 --- /dev/null +++ b/bolt-connection/test/bolt/bolt-protocol-v4x2.test.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BoltProtocolV4x2 from '../../src/bolt/bolt-protocol-v4x2' + +describe('#unit BoltProtocolV4x2', () => { + 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 BoltProtocolV4x2(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) +}) diff --git a/test/internal/bolt/bolt-protocol-v4x3.test.js b/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js similarity index 88% rename from test/internal/bolt/bolt-protocol-v4x3.test.js rename to bolt-connection/test/bolt/bolt-protocol-v4x3.test.js index 2d8e35f9b..756ed15ac 100644 --- a/test/internal/bolt/bolt-protocol-v4x3.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -17,13 +17,14 @@ * limitations under the License. */ -import BoltProtocolV4x3 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x3' -import RequestMessage from '../../../bolt-connection/lib/bolt/request-message' +import BoltProtocolV4x3 from '../../src/bolt/bolt-protocol-v4x3' +import RequestMessage from '../../src/bolt/request-message' import utils from '../test-utils' -import { WRITE } from '../../../src/driver' -import { RouteObserver } from '../../../bolt-connection/lib/bolt/stream-observers' +import { RouteObserver } from '../../src/bolt/stream-observers' import { internal } from 'neo4j-driver-core' +const WRITE = 'WRITE' + const { txConfig: { TxConfig }, bookmark: { Bookmark } @@ -31,7 +32,7 @@ const { describe('#unit BoltProtocolV4x3', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should request routing information', () => { @@ -212,4 +213,25 @@ describe('#unit BoltProtocolV4x3', () => { expect(protocol.observers).toEqual([observer]) expect(protocol.flushes).toEqual([true]) }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV4x3(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) }) diff --git a/test/internal/bolt/index.test.js b/bolt-connection/test/bolt/index.test.js similarity index 87% rename from test/internal/bolt/index.test.js rename to bolt-connection/test/bolt/index.test.js index 7f0dd2981..78ba0fea2 100644 --- a/test/internal/bolt/index.test.js +++ b/bolt-connection/test/bolt/index.test.js @@ -16,22 +16,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Bolt from '../../../bolt-connection/lib/bolt' +import Bolt from '../../src/bolt' import DummyChannel from '../dummy-channel' -import { alloc } from '../../../bolt-connection/lib/channel' +import { alloc } from '../../src/channel' import { newError, internal } from 'neo4j-driver-core' -import { - Chunker, - Dechunker -} from '../../../bolt-connection/lib/channel/chunking' - -import BoltProtocolV1 from '../../../bolt-connection/lib/bolt/bolt-protocol-v1' -import BoltProtocolV2 from '../../../bolt-connection/lib/bolt/bolt-protocol-v2' -import BoltProtocolV3 from '../../../bolt-connection/lib/bolt/bolt-protocol-v3' -import BoltProtocolV4x0 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x0' -import BoltProtocolV4x1 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x1' -import BoltProtocolV4x2 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x2' -import BoltProtocolV4x3 from '../../../bolt-connection/lib/bolt/bolt-protocol-v4x3' +import { Chunker, Dechunker } from '../../src/channel/chunking' + +import BoltProtocolV1 from '../../src/bolt/bolt-protocol-v1' +import BoltProtocolV2 from '../../src/bolt/bolt-protocol-v2' +import BoltProtocolV3 from '../../src/bolt/bolt-protocol-v3' +import BoltProtocolV4x0 from '../../src/bolt/bolt-protocol-v4x0' +import BoltProtocolV4x1 from '../../src/bolt/bolt-protocol-v4x1' +import BoltProtocolV4x2 from '../../src/bolt/bolt-protocol-v4x2' +import BoltProtocolV4x3 from '../../src/bolt/bolt-protocol-v4x3' const { logger: { Logger } @@ -183,7 +180,31 @@ describe('#unit Bolt', () => { expect(protocol._server).toBe(params.server) expect(protocol._packer).toEqual(protocol._createPacker(params.chunker)) expect(protocol._unpacker).toEqual( - protocol._createUnpacker(params.disableLosslessIntegers) + protocol._createUnpacker( + params.disableLosslessIntegers, + params.useBigInt + ) + ) + expect(protocol._log).toEqual(params.log) + const expectedError = 'Some error' + protocol._onProtocolError(expectedError) + expect(params.observer.protocolErrors).toEqual([expectedError]) + }) + + it(`it should create protocol ${version} with useBigInt=true`, () => { + const params = createBoltCreateParams({ version, useBigInt: true }) + + const protocol = Bolt.create(params) + + expect(protocol.version).toEqual(version) + expect(protocol).toEqual(jasmine.any(protocolClass)) + expect(protocol._server).toBe(params.server) + expect(protocol._packer).toEqual(protocol._createPacker(params.chunker)) + expect(protocol._unpacker).toEqual( + protocol._createUnpacker( + params.disableLosslessIntegers, + params.useBigInt + ) ) expect(protocol._log).toEqual(params.log) const expectedError = 'Some error' diff --git a/test/internal/bolt/request-message.test.js b/bolt-connection/test/bolt/request-message.test.js similarity index 95% rename from test/internal/bolt/request-message.test.js rename to bolt-connection/test/bolt/request-message.test.js index 168053640..445c467da 100644 --- a/test/internal/bolt/request-message.test.js +++ b/bolt-connection/test/bolt/request-message.test.js @@ -17,10 +17,8 @@ * limitations under the License. */ -import RequestMessage from '../../../bolt-connection/lib/bolt/request-message' -import { int } from '../../../src' -import { READ, WRITE } from '../../../src/driver' -import { internal } from 'neo4j-driver-core' +import RequestMessage from '../../src/bolt/request-message' +import { internal, int } from 'neo4j-driver-core' const { bookmark: { Bookmark }, @@ -84,7 +82,7 @@ describe('#unit RequestMessage', () => { }) it('should create BEGIN message', () => { - ;[READ, WRITE].forEach(mode => { + ;['READ', 'WRITE'].forEach(mode => { const bookmark = new Bookmark([ 'neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx10' @@ -98,7 +96,7 @@ describe('#unit RequestMessage', () => { tx_timeout: int(42), tx_metadata: { key: 42 } } - if (mode === READ) { + if (mode === 'READ') { expectedMetadata.mode = 'r' } @@ -127,7 +125,7 @@ describe('#unit RequestMessage', () => { }) it('should create RUN with metadata message', () => { - ;[READ, WRITE].forEach(mode => { + ;['READ', 'WRITE'].forEach(mode => { const query = 'RETURN $x' const parameters = { x: 42 } const bookmark = new Bookmark([ @@ -151,7 +149,7 @@ describe('#unit RequestMessage', () => { tx_timeout: int(999), tx_metadata: { a: 'a', b: 'b' } } - if (mode === READ) { + if (mode === 'READ') { expectedMetadata.mode = 'r' } diff --git a/test/internal/bolt/routing-table-raw.test.js b/bolt-connection/test/bolt/routing-table-raw.test.js similarity index 98% rename from test/internal/bolt/routing-table-raw.test.js rename to bolt-connection/test/bolt/routing-table-raw.test.js index bcf19a7d7..353b070a0 100644 --- a/test/internal/bolt/routing-table-raw.test.js +++ b/bolt-connection/test/bolt/routing-table-raw.test.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import RawRoutingTable from '../../../bolt-connection/lib/bolt/routing-table-raw' +import RawRoutingTable from '../../src/bolt/routing-table-raw' import { Record } from 'neo4j-driver-core' describe('#unit RawRoutingTable', () => { diff --git a/test/internal/bolt/stream-observer.test.js b/bolt-connection/test/bolt/stream-observer.test.js similarity index 99% rename from test/internal/bolt/stream-observer.test.js rename to bolt-connection/test/bolt/stream-observer.test.js index dc2a02a8a..a92227dad 100644 --- a/test/internal/bolt/stream-observer.test.js +++ b/bolt-connection/test/bolt/stream-observer.test.js @@ -21,8 +21,8 @@ import { ResultStreamObserver, RouteObserver, ProcedureRouteObserver -} from '../../../bolt-connection/lib/bolt/stream-observers' -import { RawRoutingTable } from '../../../bolt-connection/lib/bolt' +} from '../../src/bolt/stream-observers' +import { RawRoutingTable } from '../../src/bolt' import { error, newError, Record } from 'neo4j-driver-core' const { PROTOCOL_ERROR } = error diff --git a/bolt-connection/test/dummy-channel.js b/bolt-connection/test/dummy-channel.js new file mode 100644 index 000000000..3e57bebc5 --- /dev/null +++ b/bolt-connection/test/dummy-channel.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import CombinedBuffer from '../../bolt-connection/lib/channel/combined-buf' + +export default class DummyChannel { + /** + * @constructor + * @param {ChannelConfig} config - configuration for the new channel. + */ + constructor (config) { + this.written = [] + } + + isEncrypted () { + return false + } + + write (buf) { + this.written.push(buf) + } + + toHex () { + let out = '' + for (let i = 0; i < this.written.length; i++) { + out += this.written[i].toHex() + if (i !== this.written.length - 1) { + out += ' ' + } + } + return out + } + + toBuffer () { + return new CombinedBuffer(this.written) + } + + close () { + this.clear() + return Promise.resolve() + } + + clear () { + this.written = [] + } +} diff --git a/bolt-connection/test/packstream/packstream-v1.test.js b/bolt-connection/test/packstream/packstream-v1.test.js new file mode 100644 index 000000000..16ccd93b5 --- /dev/null +++ b/bolt-connection/test/packstream/packstream-v1.test.js @@ -0,0 +1,179 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { int } from 'neo4j-driver-core' +import { alloc } from '../../src/channel' +import { Packer, Structure, Unpacker } from '../../src/packstream/packstream-v1' + +describe('#unit PackStreamV1', () => { + it('should pack integers with small numbers', () => { + let n, i + // test small numbers + for (n = -999; n <= 999; n += 1) { + i = int(n) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + expect( + packAndUnpack(i, { disableLosslessIntegers: true }).toString() + ).toBe(i.toString()) + expect(packAndUnpack(i, { useBigInt: true }).toString()).toBe( + i.toString() + ) + } + }) + + it('should pack integers with positive numbers', () => { + let n, i + // positive numbers + for (n = 16; n <= 16; n += 1) { + i = int(Math.pow(2, n)) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + + const unpackedLossyInteger = packAndUnpack(i, { + disableLosslessIntegers: true + }) + expect(typeof unpackedLossyInteger).toBe('number') + expect(unpackedLossyInteger.toString()).toBe( + i.inSafeRange() ? i.toString() : 'Infinity' + ) + + const bigint = packAndUnpack(i, { useBigInt: true }) + expect(typeof bigint).toBe('bigint') + expect(bigint.toString()).toBe(i.toString()) + } + }) + + it('should pack integer with negative numbers', () => { + let n, i + // negative numbers + for (n = 0; n <= 63; n += 1) { + i = int(-Math.pow(2, n)) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + + const unpackedLossyInteger = packAndUnpack(i, { + disableLosslessIntegers: true + }) + expect(typeof unpackedLossyInteger).toBe('number') + expect(unpackedLossyInteger.toString()).toBe( + i.inSafeRange() ? i.toString() : '-Infinity' + ) + + const bigint = packAndUnpack(i, { useBigInt: true }) + expect(typeof bigint).toBe('bigint') + expect(bigint.toString()).toBe(i.toString()) + } + }) + + it('should pack BigInt with small numbers', () => { + let n, i + // test small numbers + for (n = -999; n <= 999; n += 1) { + i = BigInt(n) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + expect( + packAndUnpack(i, { disableLosslessIntegers: true }).toString() + ).toBe(i.toString()) + expect(packAndUnpack(i, { useBigInt: true }).toString()).toBe( + i.toString() + ) + } + }) + + it('should pack BigInt with positive numbers', () => { + let n, i + // positive numbers + for (n = 16; n <= 16; n += 1) { + i = BigInt(Math.pow(2, n)) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + + const unpackedLossyInteger = packAndUnpack(i, { + disableLosslessIntegers: true + }) + expect(typeof unpackedLossyInteger).toBe('number') + expect(unpackedLossyInteger.toString()).toBe( + int(i).inSafeRange() ? i.toString() : 'Infinity' + ) + + const bigint = packAndUnpack(i, { useBigInt: true }) + expect(typeof bigint).toBe('bigint') + expect(bigint.toString()).toBe(i.toString()) + } + }) + + it('should pack BigInt with negative numbers', () => { + let n, i + // negative numbers + for (n = 0; n <= 63; n += 1) { + i = BigInt(-Math.pow(2, n)) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + + const unpackedLossyInteger = packAndUnpack(i, { + disableLosslessIntegers: true + }) + expect(typeof unpackedLossyInteger).toBe('number') + expect(unpackedLossyInteger.toString()).toBe( + int(i).inSafeRange() ? i.toString() : '-Infinity' + ) + + const bigint = packAndUnpack(i, { useBigInt: true }) + expect(typeof bigint).toBe('bigint') + expect(bigint.toString()).toBe(i.toString()) + } + }) + + it('should pack strings', () => { + expect(packAndUnpack('')).toBe('') + expect(packAndUnpack('abcdefg123567')).toBe('abcdefg123567') + const str = Array(65536 + 1).join('a') // 2 ^ 16 + 1 + expect(packAndUnpack(str, { bufferSize: str.length + 8 })).toBe(str) + }) + + it('should pack structures', () => { + expect(packAndUnpack(new Structure(1, ['Hello, world!!!'])).fields[0]).toBe( + 'Hello, world!!!' + ) + }) + + it('should pack lists', () => { + const list = ['a', 'b'] + const unpacked = packAndUnpack(list) + expect(unpacked[0]).toBe(list[0]) + expect(unpacked[1]).toBe(list[1]) + }) + + it('should pack long lists', () => { + const listLength = 256 + const list = [] + for (let i = 0; i < listLength; i++) { + list.push(null) + } + const unpacked = packAndUnpack(list, { bufferSize: 1400 }) + expect(unpacked[0]).toBe(list[0]) + expect(unpacked[1]).toBe(list[1]) + }) +}) + +function packAndUnpack ( + val, + { bufferSize = 128, disableLosslessIntegers = false, useBigInt = false } = {} +) { + const buffer = alloc(bufferSize) + new Packer(buffer).packable(val)() + buffer.reset() + return new Unpacker(disableLosslessIntegers, useBigInt).unpack(buffer) +} diff --git a/bolt-connection/test/test-utils.js b/bolt-connection/test/test-utils.js new file mode 100644 index 000000000..42301b6cc --- /dev/null +++ b/bolt-connection/test/test-utils.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Connection from '../../bolt-connection/src/connection/connection' + +function isClient () { + return typeof window !== 'undefined' && window.document +} + +function isServer () { + return !isClient() +} + +function fakeStandardDateWithOffset (offsetMinutes) { + const date = new Date() + date.getTimezoneOffset = () => offsetMinutes + return date +} + +const matchers = { + toBeElementOf: function (actual, expected) { + if (expected === undefined) { + expected = [] + } + + const result = {} + + result.pass = expected in actual + if (result.pass) { + result.message = `Expected '${actual}' to be an element of '[${expected}]'` + } else { + result.message = `Expected '${actual}' to be an element of '[${expected}]', but it wasn't` + } + }, + toBeMessage: function (actual, expected) { + if (expected === undefined) { + expected = {} + } + + const result = {} + const failures = [] + + if (expected.signature !== actual.signature) { + failures.push( + `signature '${actual.signature}' to match '${expected.signature}'` + ) + } + + if (JSON.stringify(expected.fields) !== JSON.stringify(actual.fields)) { + failures.push( + `fields '[${JSON.stringify( + actual.fields + )}]' to match '[${JSON.stringify(expected.fields)}]'` + ) + } + + result.pass = failures.length === 0 + if (result.pass) { + result.message = () => + `Expected message '${actual}' to match '${expected}'` + } else { + result.message = () => `Expected message '[${failures}]', but it didn't` + } + return result + } +} + +class MessageRecordingConnection extends Connection { + constructor () { + super(null) + + this.messages = [] + this.observers = [] + this.flushes = [] + this.fatalErrors = [] + } + + write (message, observer, flush) { + this.messages.push(message) + this.observers.push(observer) + this.flushes.push(flush) + } + + _handleFatalError (error) { + this.fatalErrors.push(error) + } + + verifyMessageCount (expected) { + expect(this.messages.length).toEqual(expected) + expect(this.observers.length).toEqual(expected) + expect(this.flushes.length).toEqual(expected) + } +} + +function spyProtocolWrite (protocol, callRealMethod = false) { + protocol.messages = [] + protocol.observers = [] + protocol.flushes = [] + + const write = callRealMethod ? protocol.write.bind(protocol) : () => true + protocol.write = (message, observer, flush) => { + protocol.messages.push(message) + protocol.observers.push(observer) + protocol.flushes.push(flush) + return write(message, observer, flush) + } + + protocol.verifyMessageCount = expected => { + expect(protocol.messages.length).toEqual(expected) + expect(protocol.observers.length).toEqual(expected) + expect(protocol.flushes.length).toEqual(expected) + } + + return protocol +} + +export default { + isClient, + isServer, + fakeStandardDateWithOffset, + matchers, + MessageRecordingConnection, + spyProtocolWrite +} diff --git a/core/src/graph-types.ts b/core/src/graph-types.ts index e554ac3b8..746119bd4 100644 --- a/core/src/graph-types.ts +++ b/core/src/graph-types.ts @@ -19,7 +19,7 @@ import Integer from './integer' type StandardDate = Date -type NumberOrInteger = number | Integer +type NumberOrInteger = number | Integer | bigint const IDENTIFIER_PROPERTY_ATTRIBUTES = { value: true, diff --git a/core/src/integer.ts b/core/src/integer.ts index 004b17269..8941f51fd 100644 --- a/core/src/integer.ts +++ b/core/src/integer.ts @@ -107,6 +107,29 @@ class Integer { return this.high * TWO_PWR_32_DBL + (this.low >>> 0) } + /** + * Converts the Integer to a BigInt representation of this value + * @returns {bigint} + * @expose + */ + toBigInt (): bigint { + if (this.isZero()) { + return BigInt(0) + } else if (this.isPositive()) { + return ( + BigInt(this.high >>> 0) * BigInt(TWO_PWR_32_DBL) + + BigInt(this.low >>> 0) + ) + } else { + const negate = this.negate() + return ( + BigInt(-1) * + (BigInt(negate.high >>> 0) * BigInt(TWO_PWR_32_DBL) + + BigInt(negate.low >>> 0)) + ) + } + } + /** * Converts the Integer to native number or -Infinity/+Infinity when it does not fit. * @return {number} @@ -857,7 +880,7 @@ class Integer { /** * Converts the specified value to a Integer. * @access private - * @param {!Integer|number|string|!{low: number, high: number}} val Value + * @param {!Integer|number|string|bigint|!{low: number, high: number}} val Value * @returns {!Integer} * @expose */ @@ -871,6 +894,9 @@ class Integer { if (typeof val === 'string') { return Integer.fromString(val) } + if (typeof val === 'bigint') { + return Integer.fromString(val.toString()) + } // Throws for non-objects, converts non-instanceof Integer: return new Integer(val.low, val.high) } @@ -911,7 +937,12 @@ class Integer { } } -type Integerable = number | string | Integer | { low: number; high: number } +type Integerable = + | number + | string + | Integer + | { low: number; high: number } + | bigint Object.defineProperty(Integer.prototype, '__isInteger__', { value: true, diff --git a/core/src/internal/temporal-util.ts b/core/src/internal/temporal-util.ts index 610966388..e243f9418 100644 --- a/core/src/internal/temporal-util.ts +++ b/core/src/internal/temporal-util.ts @@ -20,6 +20,7 @@ import Integer, { int, isInt } from '../integer' import { newError } from '../error' import { assertNumberOrInteger } from './util' +import { NumberOrInteger } from '../graph-types' /* Code in this util should be compatible with code in the database that uses JSR-310 java.time APIs. @@ -44,12 +45,18 @@ class ValueRange { this._maxInteger = int(max) } - contains (value: number | Integer) { + contains (value: number | Integer | bigint) { if (isInt(value) && value instanceof Integer) { return ( value.greaterThanOrEqual(this._minInteger) && value.lessThanOrEqual(this._maxInteger) ) + } else if (typeof value === 'bigint') { + const intValue = int(value) + return ( + intValue.greaterThanOrEqual(this._minInteger) && + intValue.lessThanOrEqual(this._maxInteger) + ) } else { return value >= this._minNumber && value <= this._maxNumber } @@ -80,14 +87,14 @@ export const DAYS_PER_400_YEAR_CYCLE = 146097 export const SECONDS_PER_DAY = 86400 export function normalizeSecondsForDuration ( - seconds: number | Integer, - nanoseconds: number | Integer + seconds: number | Integer | bigint, + nanoseconds: number | Integer | bigint ): Integer { return int(seconds).add(floorDiv(nanoseconds, NANOS_PER_SECOND)) } export function normalizeNanosecondsForDuration ( - nanoseconds: number | Integer + nanoseconds: number | Integer | bigint ): Integer { return floorMod(nanoseconds, NANOS_PER_SECOND) } @@ -101,10 +108,10 @@ export function normalizeNanosecondsForDuration ( * @return {Integer} nanoseconds representing the given local time. */ export function localTimeToNanoOfDay ( - hour: Integer | number | string, - minute: Integer | number | string, - second: Integer | number | string, - nanosecond: Integer | number | string + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string ): Integer { hour = int(hour) minute = int(minute) @@ -129,13 +136,13 @@ export function localTimeToNanoOfDay ( * @return {Integer} epoch second in UTC representing the given local date time. */ export function localDateTimeToEpochSecond ( - year: Integer | number | string, - month: Integer | number | string, - day: Integer | number | string, - hour: Integer | number | string, - minute: Integer | number | string, - second: Integer | number | string, - nanosecond: Integer | number | string + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string, + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string ): Integer { const epochDay = dateToEpochDay(year, month, day) const localTimeSeconds = localTimeToSecondOfDay(hour, minute, second) @@ -150,9 +157,9 @@ export function localDateTimeToEpochSecond ( * @return {Integer} epoch day representing the given date. */ export function dateToEpochDay ( - year: Integer | number | string, - month: Integer | number | string, - day: Integer | number | string + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string ): Integer { year = int(year) month = int(month) @@ -202,10 +209,10 @@ export function dateToEpochDay ( * @return {string} ISO string that represents given duration. */ export function durationToIsoString ( - months: Integer | number | string, - days: Integer | number | string, - seconds: Integer | number | string, - nanoseconds: Integer | number | string + months: NumberOrInteger | string, + days: NumberOrInteger | string, + seconds: NumberOrInteger | string, + nanoseconds: NumberOrInteger | string ): string { const monthsString = formatNumber(months) const daysString = formatNumber(days) @@ -225,10 +232,10 @@ export function durationToIsoString ( * @return {string} ISO string that represents given time. */ export function timeToIsoString ( - hour: Integer | number | string, - minute: Integer | number | string, - second: Integer | number | string, - nanosecond: Integer | number | string + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string ): string { const hourString = formatNumber(hour, 2) const minuteString = formatNumber(minute, 2) @@ -243,7 +250,7 @@ export function timeToIsoString ( * @return {string} ISO string that represents given offset. */ export function timeZoneOffsetToIsoString ( - offsetSeconds: Integer | number | string + offsetSeconds: NumberOrInteger | string ): string { offsetSeconds = int(offsetSeconds) if (offsetSeconds.equals(0)) { @@ -277,9 +284,9 @@ export function timeZoneOffsetToIsoString ( * @return {string} ISO string that represents given date. */ export function dateToIsoString ( - year: Integer | number | string, - month: Integer | number | string, - day: Integer | number | string + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string ): string { year = int(year) const isNegative = year.isNegative() @@ -299,18 +306,16 @@ export function dateToIsoString ( /** * Get the total number of nanoseconds from the milliseconds of the given standard JavaScript date and optional nanosecond part. * @param {global.Date} standardDate the standard JavaScript date. - * @param {Integer|number|undefined} nanoseconds the optional number of nanoseconds. - * @return {Integer|number} the total amount of nanoseconds. + * @param {Integer|number|bigint|undefined} nanoseconds the optional number of nanoseconds. + * @return {Integer|number|bigint} the total amount of nanoseconds. */ export function totalNanoseconds ( standardDate: Date, - nanoseconds?: Integer | number -): Integer | number { + nanoseconds?: NumberOrInteger +): NumberOrInteger { nanoseconds = nanoseconds || 0 const nanosFromMillis = standardDate.getMilliseconds() * NANOS_PER_MILLISECOND - return nanoseconds instanceof Integer - ? nanoseconds.add(nanosFromMillis) - : nanoseconds + nanosFromMillis + return add(nanoseconds, nanosFromMillis) } /** @@ -338,7 +343,7 @@ export function timeZoneOffsetInSeconds (standardDate: Date): number { * @param {Integer|number} year the value to check. * @return {Integer|number} the value of the year if it is valid. Exception is thrown otherwise. */ -export function assertValidYear (year: Integer | number): Integer | number { +export function assertValidYear (year: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(year, YEAR_RANGE, 'Year') } @@ -347,7 +352,7 @@ export function assertValidYear (year: Integer | number): Integer | number { * @param {Integer|number} month the value to check. * @return {Integer|number} the value of the month if it is valid. Exception is thrown otherwise. */ -export function assertValidMonth (month: Integer | number): Integer | number { +export function assertValidMonth (month: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(month, MONTH_OF_YEAR_RANGE, 'Month') } @@ -356,7 +361,7 @@ export function assertValidMonth (month: Integer | number): Integer | number { * @param {Integer|number} day the value to check. * @return {Integer|number} the value of the day if it is valid. Exception is thrown otherwise. */ -export function assertValidDay (day: Integer | number): Integer | number { +export function assertValidDay (day: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(day, DAY_OF_MONTH_RANGE, 'Day') } @@ -365,7 +370,7 @@ export function assertValidDay (day: Integer | number): Integer | number { * @param {Integer|number} hour the value to check. * @return {Integer|number} the value of the hour if it is valid. Exception is thrown otherwise. */ -export function assertValidHour (hour: Integer | number): Integer | number { +export function assertValidHour (hour: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(hour, HOUR_OF_DAY_RANGE, 'Hour') } @@ -374,7 +379,7 @@ export function assertValidHour (hour: Integer | number): Integer | number { * @param {Integer|number} minute the value to check. * @return {Integer|number} the value of the minute if it is valid. Exception is thrown otherwise. */ -export function assertValidMinute (minute: Integer | number): Integer | number { +export function assertValidMinute (minute: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(minute, MINUTE_OF_HOUR_RANGE, 'Minute') } @@ -383,7 +388,7 @@ export function assertValidMinute (minute: Integer | number): Integer | number { * @param {Integer|number} second the value to check. * @return {Integer|number} the value of the second if it is valid. Exception is thrown otherwise. */ -export function assertValidSecond (second: Integer | number): Integer | number { +export function assertValidSecond (second: NumberOrInteger): NumberOrInteger { return assertValidTemporalValue(second, SECOND_OF_MINUTE_RANGE, 'Second') } @@ -393,8 +398,8 @@ export function assertValidSecond (second: Integer | number): Integer | number { * @return {Integer|number} the value of the nanosecond if it is valid. Exception is thrown otherwise. */ export function assertValidNanosecond ( - nanosecond: Integer | number -): Integer | number { + nanosecond: NumberOrInteger +): NumberOrInteger { return assertValidTemporalValue( nanosecond, NANOSECOND_OF_SECOND_RANGE, @@ -410,10 +415,10 @@ export function assertValidNanosecond ( * @return {Integer|number} the value if valid. Exception is thrown otherwise. */ function assertValidTemporalValue ( - value: Integer | number, + value: NumberOrInteger, range: ValueRange, name: string -): Integer | number { +): NumberOrInteger { assertNumberOrInteger(value, name) if (!range.contains(value)) { throw newError( @@ -431,9 +436,9 @@ function assertValidTemporalValue ( * @return {Integer} seconds representing the given local time. */ function localTimeToSecondOfDay ( - hour: Integer | number | string, - minute: Integer | number | string, - second: Integer | number | string + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string ): Integer { hour = int(hour) minute = int(minute) @@ -449,7 +454,7 @@ function localTimeToSecondOfDay ( * @param {Integer|number|string} year the year to check. Will be converted to {@link Integer} for all calculations. * @return {boolean} `true` if given year is a leap year, `false` otherwise. */ -function isLeapYear (year: Integer | number | string): boolean { +function isLeapYear (year: NumberOrInteger | string): boolean { year = int(year) if (!year.modulo(4).equals(0)) { @@ -469,8 +474,8 @@ function isLeapYear (year: Integer | number | string): boolean { * @return {Integer} the result. */ export function floorDiv ( - x: Integer | number | string, - y: Integer | number | string + x: NumberOrInteger | string, + y: NumberOrInteger | string ): Integer { x = int(x) y = int(y) @@ -488,8 +493,8 @@ export function floorDiv ( * @return {Integer} the result. */ export function floorMod ( - x: Integer | number | string, - y: Integer | number | string + x: NumberOrInteger | string, + y: NumberOrInteger | string ): Integer { x = int(x) y = int(y) @@ -503,8 +508,8 @@ export function floorMod ( * @return {string} formatted value. */ function formatSecondsAndNanosecondsForDuration ( - seconds: Integer | number | string, - nanoseconds: Integer | number | string + seconds: NumberOrInteger | string, + nanoseconds: NumberOrInteger | string ): string { seconds = int(seconds) nanoseconds = int(nanoseconds) @@ -546,7 +551,7 @@ function formatSecondsAndNanosecondsForDuration ( * @param {Integer|number|string} value the number of nanoseconds to format. * @return {string} formatted and possibly left-padded nanoseconds part as string. */ -function formatNanosecond (value: Integer | number | string): string { +function formatNanosecond (value: NumberOrInteger | string): string { value = int(value) return value.equals(0) ? '' : '.' + formatNumber(value, 9) } @@ -557,7 +562,7 @@ function formatNanosecond (value: Integer | number | string): string { * @return {string} formatted and possibly left-padded number as string. */ function formatNumber ( - num: Integer | number | string, + num: NumberOrInteger | string, stringLength?: number ): string { num = int(num) @@ -575,3 +580,12 @@ function formatNumber ( } return isNegative ? '-' + numString : numString } + +function add (x: NumberOrInteger, y: number): NumberOrInteger { + if (x instanceof Integer) { + return x.add(y) + } else if (typeof x === 'bigint') { + return x + BigInt(y) + } + return x + y +} diff --git a/core/src/internal/util.ts b/core/src/internal/util.ts index b3af11159..3f10e3c08 100644 --- a/core/src/internal/util.ts +++ b/core/src/internal/util.ts @@ -18,6 +18,7 @@ */ import Integer, { isInt } from '../integer' +import { NumberOrInteger } from '../graph-types' import { EncryptionLevel } from '../types' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' @@ -146,8 +147,8 @@ function assertNumber(obj: any, objName: string): number { * @returns {number|Integer} The subject object * @throws {TypeError} when the supplied param is not a number or integer */ -function assertNumberOrInteger(obj: any, objName: string): number | Integer { - if (typeof obj !== 'number' && !isInt(obj)) { +function assertNumberOrInteger(obj: any, objName: string): NumberOrInteger { + if (typeof obj !== 'number' && typeof obj !== 'bigint' && !isInt(obj)) { throw new TypeError( objName + ' expected to be either a number or an Integer object but was: ' + diff --git a/core/src/result-summary.ts b/core/src/result-summary.ts index 81c231327..1d5166f5f 100644 --- a/core/src/result-summary.ts +++ b/core/src/result-summary.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import Integer from './integer' +import Integer, { int } from './integer' import { NumberOrInteger } from './graph-types' /** @@ -469,7 +469,13 @@ class ServerInfo { } function intValue(value: NumberOrInteger): number { - return value instanceof Integer ? value.toInt() : value + if (value instanceof Integer) { + return value.toInt() + } else if (typeof value == 'bigint') { + return int(value).toInt() + } else { + return value + } } function valueOrDefault( @@ -479,7 +485,7 @@ function valueOrDefault( ): number { if (key in values) { const value = values[key] - return value instanceof Integer ? value.toInt() : value + return intValue(value) } else { return defaultValue } diff --git a/core/src/spatial-types.ts b/core/src/spatial-types.ts index 9ed6087b5..3de918736 100644 --- a/core/src/spatial-types.ts +++ b/core/src/spatial-types.ts @@ -17,6 +17,7 @@ * limitations under the License. */ import { assertNumber, assertNumberOrInteger } from './internal/util' +import { NumberOrInteger } from './graph-types' import Integer from './integer' const POINT_IDENTIFIER_PROPERTY = '__isPoint__' @@ -25,7 +26,7 @@ const POINT_IDENTIFIER_PROPERTY = '__isPoint__' * Represents a single two or three-dimensional point in a particular coordinate reference system. * Created `Point` objects are frozen with `Object.freeze()` in constructor and thus immutable. */ -export class Point { +export class Point { readonly srid: T readonly x: number readonly y: number @@ -76,7 +77,7 @@ export class Point { } } -function formatAsFloat(number: number | Integer) { +function formatAsFloat(number: NumberOrInteger) { return Number.isInteger(number) ? number + '.0' : number.toString() } diff --git a/core/src/temporal-types.ts b/core/src/temporal-types.ts index 7f88b6f7b..792bb6aef 100644 --- a/core/src/temporal-types.ts +++ b/core/src/temporal-types.ts @@ -25,7 +25,7 @@ import { assertValidDate } from './internal/util' import { newError } from './error' -import Integer, { toNumber } from './integer' +import Integer, { int, toNumber } from './integer' const IDENTIFIER_PROPERTY_ATTRIBUTES = { value: true, @@ -165,7 +165,7 @@ export class LocalTime { ): LocalTime { verifyStandardDateAndNanos(standardDate, nanosecond) - const totalNanoseconds: number | Integer = util.totalNanoseconds( + const totalNanoseconds: number | Integer | bigint = util.totalNanoseconds( standardDate, nanosecond ) @@ -176,6 +176,8 @@ export class LocalTime { standardDate.getSeconds(), totalNanoseconds instanceof Integer ? totalNanoseconds.toInt() + : typeof totalNanoseconds === 'bigint' + ? int(totalNanoseconds).toInt() : totalNanoseconds ) } diff --git a/core/test/integer.test.ts b/core/test/integer.test.ts index 82c64e975..7035a297c 100644 --- a/core/test/integer.test.ts +++ b/core/test/integer.test.ts @@ -299,6 +299,12 @@ describe('Integer', () => { test(`inSafeRange(${input}) toEqual ${expectedOutput}`, () => expect(inSafeRange(input)).toEqual(expectedOutput)) ) + + test('Integer.toBigInt', () => { + expect(Integer.MAX_SAFE_VALUE.toBigInt().toString()).toEqual( + Integer.MAX_SAFE_VALUE.toString() + ) + }) }) function forEachToNumberOrInfinityScenarios( diff --git a/neo4j-driver-lite/src/index.ts b/neo4j-driver-lite/src/index.ts index 9c3b281d3..3abe3cc00 100644 --- a/neo4j-driver-lite/src/index.ts +++ b/neo4j-driver-lite/src/index.ts @@ -171,6 +171,10 @@ const { * // in loss of precision in the general case. * disableLosslessIntegers: false, * + * // Make this driver always return native Javascript {@link BigInt} for integer values, instead of the dedicated {@link Integer} class or {@link Number}. + * // Default value for this option is `false` for backwards compatibility while {@link Integer} is not depreacted + * useBigInt: false, + * * // Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * // * // Property `level` represents the logging level which should be one of: 'error', 'warn', 'info' or 'debug'. This property is optional and diff --git a/src/index.js b/src/index.js index 300b03f0a..457e0edba 100644 --- a/src/index.js +++ b/src/index.js @@ -147,6 +147,10 @@ const { * // in loss of precision in the general case. * disableLosslessIntegers: false, * + * // Make this driver always return native Javascript {@link BigInt} for integer values, instead of the dedicated {@link Integer} class or {@link Number}. + * // Default value for this option is `false` for backwards compatibility while {@link Integer} is not depreacted + * useBigInt: false, + * * // Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. * // * // Property `level` represents the logging level which should be one of: 'error', 'warn', 'info' or 'debug'. This property is optional and diff --git a/test/internal/packstream-v1.test.js b/test/internal/packstream-v1.test.js deleted file mode 100644 index 07cf46199..000000000 --- a/test/internal/packstream-v1.test.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { alloc } from '../../bolt-connection/lib/channel' -import { - Packer, - Structure, - Unpacker -} from '../../bolt-connection/lib/packstream/packstream-v1' -import { int } from '../../src' - -describe('#unit PackStreamV1', () => { - it('should pack integers', () => { - let n, i - // test small numbers - for (n = -999; n <= 999; n += 1) { - i = int(n) - expect(packAndUnpack(i).toString()).toBe(i.toString()) - } - // positive numbers - for (n = 16; n <= 16; n += 1) { - i = int(Math.pow(2, n)) - expect(packAndUnpack(i).toString()).toBe(i.toString()) - } - // negative numbers - for (n = 0; n <= 63; n += 1) { - i = int(-Math.pow(2, n)) - expect(packAndUnpack(i).toString()).toBe(i.toString()) - } - }) - - it('should pack strings', () => { - expect(packAndUnpack('')).toBe('') - expect(packAndUnpack('abcdefg123567')).toBe('abcdefg123567') - const str = Array(65536 + 1).join('a') // 2 ^ 16 + 1 - expect(packAndUnpack(str, str.length + 8)).toBe(str) - }) - - it('should pack structures', () => { - expect(packAndUnpack(new Structure(1, ['Hello, world!!!'])).fields[0]).toBe( - 'Hello, world!!!' - ) - }) - - it('should pack lists', () => { - const list = ['a', 'b'] - const unpacked = packAndUnpack(list) - expect(unpacked[0]).toBe(list[0]) - expect(unpacked[1]).toBe(list[1]) - }) - - it('should pack long lists', () => { - const listLength = 256 - const list = [] - for (let i = 0; i < listLength; i++) { - list.push(null) - } - const unpacked = packAndUnpack(list, 1400) - expect(unpacked[0]).toBe(list[0]) - expect(unpacked[1]).toBe(list[1]) - }) -}) - -function packAndUnpack (val, bufferSize) { - bufferSize = bufferSize || 128 - const buffer = alloc(bufferSize) - new Packer(buffer).packable(val)() - buffer.reset() - return new Unpacker().unpack(buffer) -} diff --git a/test/internal/temporal-util.test.js b/test/internal/temporal-util.test.js index 4edf3bb38..22b1785bf 100644 --- a/test/internal/temporal-util.test.js +++ b/test/internal/temporal-util.test.js @@ -249,6 +249,19 @@ describe('#unit temporal-util', () => { expect( util.totalNanoseconds(new Date(2000, 1, 1, 1, 1, 1, 999), int(111)) ).toEqual(int(999000111)) + + expect( + util.totalNanoseconds(new Date(2000, 1, 1, 1, 1, 1, 0), BigInt(1)) + ).toEqual(BigInt(1)) + expect( + util.totalNanoseconds(new Date(2000, 1, 1, 1, 1, 1, 0), BigInt(999)) + ).toEqual(BigInt(999)) + expect( + util.totalNanoseconds(new Date(2000, 1, 1, 1, 1, 1, 1), BigInt(999)) + ).toEqual(BigInt(1000999)) + expect( + util.totalNanoseconds(new Date(2000, 1, 1, 1, 1, 1, 999), BigInt(111)) + ).toEqual(BigInt(999000111)) }) it('should get timezone offset in seconds from standard date', () => { diff --git a/testkit-backend/src/cypher-native-binders.js b/testkit-backend/src/cypher-native-binders.js index c8a0e3a42..6247c2bfd 100644 --- a/testkit-backend/src/cypher-native-binders.js +++ b/testkit-backend/src/cypher-native-binders.js @@ -14,6 +14,8 @@ export function nativeToCypher (x) { return valueResponse('CypherInt', x) } return valueResponse('CypherFloat', x) + case 'bigint': + return valueResponse('CypherInt', neo4j.int(x).toNumber()) case 'string': return valueResponse('CypherString', x) case 'boolean': @@ -21,7 +23,6 @@ export function nativeToCypher (x) { case 'object': if (neo4j.isInt(x)) { // TODO: Broken!!! - console.log(x) return valueResponse('CypherInt', x.toInt()) } if (Array.isArray(x)) { @@ -58,7 +59,7 @@ export function cypherToNative (c) { case 'CypherString': return value case 'CypherInt': - return int(value) + return BigInt(value) case 'CypherFloat': return value case 'CypherNull': diff --git a/testkit-backend/src/request-handlers.js b/testkit-backend/src/request-handlers.js index 8bfc5d232..7d8ea5fea 100644 --- a/testkit-backend/src/request-handlers.js +++ b/testkit-backend/src/request-handlers.js @@ -8,7 +8,7 @@ export function NewDriver (context, data, { writeResponse }) { authorizationToken: { data: authToken }, userAgent } = data - const driver = neo4j.driver(uri, authToken, { userAgent }) + const driver = neo4j.driver(uri, authToken, { userAgent, useBigInt: true }) const id = context.addDriver(driver) writeResponse('Driver', { id }) } From cfbbcc8ff84d4100d2645d496b7dd21e1515022f Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 30 Mar 2021 10:51:15 +0200 Subject: [PATCH 2/5] use bigint to the core types --- core/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/types.ts b/core/src/types.ts index 68af97572..c8bdc659e 100644 --- a/core/src/types.ts +++ b/core/src/types.ts @@ -60,6 +60,7 @@ export interface Config { connectionAcquisitionTimeout?: number connectionTimeout?: number disableLosslessIntegers?: boolean + useBigInt?: boolean logging?: LoggingConfig resolver?: (address: string) => string[] | Promise userAgent?: string From e61eef09585efd01950f7bf66f2333f5f3f071d8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 30 Mar 2021 17:56:12 +0200 Subject: [PATCH 3/5] Fix bigint json serialization and remove wrong null/undefined checks --- bolt-connection/src/bolt/request-message.js | 18 +++++------ bolt-connection/src/bolt/response-handler.js | 10 +++--- bolt-connection/src/bolt/stream-observers.js | 12 +++---- .../src/connection/connection-channel.js | 4 +-- .../src/rediscovery/routing-table.js | 13 ++++++-- .../test/bolt/request-message.test.js | 32 +++++++++++++++---- .../test/bolt/stream-observer.test.js | 14 ++++---- .../test/packstream/packstream-v1.test.js | 17 +++++++++- bolt-connection/test/test-utils.js | 7 ++-- core/src/graph-types.ts | 7 ++-- core/src/index.ts | 7 ++-- core/src/internal/util.ts | 13 ++++---- core/src/json.ts | 30 +++++++++++++++++ test/driver.test.js | 1 + test/internal/shared-neo4j.js | 1 + test/session.test.js | 3 +- 16 files changed, 134 insertions(+), 55 deletions(-) create mode 100644 core/src/json.ts diff --git a/bolt-connection/src/bolt/request-message.js b/bolt-connection/src/bolt/request-message.js index 441d1a4e9..f2f18ed53 100644 --- a/bolt-connection/src/bolt/request-message.js +++ b/bolt-connection/src/bolt/request-message.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { int, internal } from 'neo4j-driver-core' +import { int, internal, json } from 'neo4j-driver-core' const { constants: { ACCESS_MODE_READ, FETCH_ALL }, @@ -79,7 +79,7 @@ export default class RequestMessage { return new RequestMessage( RUN, [query, parameters], - () => `RUN ${query} ${JSON.stringify(parameters)}` + () => `RUN ${query} ${json.stringify(parameters)}` ) } @@ -131,7 +131,7 @@ export default class RequestMessage { return new RequestMessage( BEGIN, [metadata], - () => `BEGIN ${JSON.stringify(metadata)}` + () => `BEGIN ${json.stringify(metadata)}` ) } @@ -171,7 +171,7 @@ export default class RequestMessage { RUN, [query, parameters, metadata], () => - `RUN ${query} ${JSON.stringify(parameters)} ${JSON.stringify(metadata)}` + `RUN ${query} ${json.stringify(parameters)} ${json.stringify(metadata)}` ) } @@ -191,13 +191,13 @@ export default class RequestMessage { */ static pull ({ stmtId = NO_STATEMENT_ID, n = FETCH_ALL } = {}) { const metadata = buildStreamMetadata( - stmtId || NO_STATEMENT_ID, + stmtId === null || stmtId === undefined ? NO_STATEMENT_ID : stmtId, n || FETCH_ALL ) return new RequestMessage( PULL, [metadata], - () => `PULL ${JSON.stringify(metadata)}` + () => `PULL ${json.stringify(metadata)}` ) } @@ -209,13 +209,13 @@ export default class RequestMessage { */ static discard ({ stmtId = NO_STATEMENT_ID, n = FETCH_ALL } = {}) { const metadata = buildStreamMetadata( - stmtId || NO_STATEMENT_ID, + stmtId === null || stmtId === undefined ? NO_STATEMENT_ID : stmtId, n || FETCH_ALL ) return new RequestMessage( DISCARD, [metadata], - () => `DISCARD ${JSON.stringify(metadata)}` + () => `DISCARD ${json.stringify(metadata)}` ) } @@ -230,7 +230,7 @@ export default class RequestMessage { return new RequestMessage( ROUTE, [routingContext, databaseName], - () => `ROUTE ${JSON.stringify(routingContext)} ${databaseName}` + () => `ROUTE ${json.stringify(routingContext)} ${databaseName}` ) } } diff --git a/bolt-connection/src/bolt/response-handler.js b/bolt-connection/src/bolt/response-handler.js index ea257f148..6936c9fdb 100644 --- a/bolt-connection/src/bolt/response-handler.js +++ b/bolt-connection/src/bolt/response-handler.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError } from 'neo4j-driver-core' +import { newError, json } from 'neo4j-driver-core' // Signature bytes for each response message type const SUCCESS = 0x70 // 0111 0000 // SUCCESS @@ -92,13 +92,13 @@ export default class ResponseHandler { switch (msg.signature) { case RECORD: if (this._log.isDebugEnabled()) { - this._log.debug(`${this} S: RECORD ${JSON.stringify(msg)}`) + this._log.debug(`${this} S: RECORD ${json.stringify(msg)}`) } this._currentObserver.onNext(payload) break case SUCCESS: if (this._log.isDebugEnabled()) { - this._log.debug(`${this} S: SUCCESS ${JSON.stringify(msg)}`) + this._log.debug(`${this} S: SUCCESS ${json.stringify(msg)}`) } try { const metadata = this._transformMetadata(payload) @@ -109,7 +109,7 @@ export default class ResponseHandler { break case FAILURE: if (this._log.isDebugEnabled()) { - this._log.debug(`${this} S: FAILURE ${JSON.stringify(msg)}`) + this._log.debug(`${this} S: FAILURE ${json.stringify(msg)}`) } try { const error = newError(payload.message, payload.code) @@ -125,7 +125,7 @@ export default class ResponseHandler { break case IGNORED: if (this._log.isDebugEnabled()) { - this._log.debug(`${this} S: IGNORED ${JSON.stringify(msg)}`) + this._log.debug(`${this} S: IGNORED ${json.stringify(msg)}`) } try { if (this._currentFailure && this._currentObserver.onError) { diff --git a/bolt-connection/src/bolt/stream-observers.js b/bolt-connection/src/bolt/stream-observers.js index 3c9815569..34eab02c0 100644 --- a/bolt-connection/src/bolt/stream-observers.js +++ b/bolt-connection/src/bolt/stream-observers.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError, error, Integer, Record } from 'neo4j-driver-core' +import { newError, error, Integer, Record, json } from 'neo4j-driver-core' import { ALL } from './request-message' import RawRoutingTable from './routing-table-raw' @@ -268,7 +268,7 @@ class ResultStreamObserver extends StreamObserver { // Extract server generated query id for use in requestMore and discard // functions - if (meta.qid) { + if (meta.qid !== null && meta.qid !== undefined) { this._queryId = meta.qid // remove qid from metadata object @@ -392,7 +392,7 @@ class LoginObserver extends StreamObserver { onNext (record) { this.onError( - newError('Received RECORD when initializing ' + JSON.stringify(record)) + newError('Received RECORD when initializing ' + json.stringify(record)) ) } @@ -429,7 +429,7 @@ class ResetObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - JSON.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) @@ -500,7 +500,7 @@ class ProcedureRouteObserver extends StreamObserver { 'Illegal response from router. Received ' + this._records.length + ' records but expected only one.\n' + - JSON.stringify(this._records), + json.stringify(this._records), PROTOCOL_ERROR ) ) @@ -533,7 +533,7 @@ class RouteObserver extends StreamObserver { this.onError( newError( 'Received RECORD when resetting: received record is: ' + - JSON.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) ) diff --git a/bolt-connection/src/connection/connection-channel.js b/bolt-connection/src/connection/connection-channel.js index 77136dfff..bdcde7d58 100644 --- a/bolt-connection/src/connection/connection-channel.js +++ b/bolt-connection/src/connection/connection-channel.js @@ -18,7 +18,7 @@ */ import { Chunker, Dechunker, ChannelConfig, Channel } from '../channel' -import { newError, error } from 'neo4j-driver-core' +import { newError, error, json } from 'neo4j-driver-core' import Connection from './connection' import Bolt from '../bolt' @@ -247,7 +247,7 @@ export default class ChannelConnection extends Connection { if (this._log.isErrorEnabled()) { this._log.error( - `${this} experienced a fatal error ${JSON.stringify(this._error)}` + `${this} experienced a fatal error ${json.stringify(this._error)}` ) } diff --git a/bolt-connection/src/rediscovery/routing-table.js b/bolt-connection/src/rediscovery/routing-table.js index 313ac40d0..8184ad28b 100644 --- a/bolt-connection/src/rediscovery/routing-table.js +++ b/bolt-connection/src/rediscovery/routing-table.js @@ -16,7 +16,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError, error, Integer, int, internal } from 'neo4j-driver-core' +import { + newError, + error, + Integer, + int, + internal, + json +} from 'neo4j-driver-core' const { constants: { ACCESS_MODE_WRITE: WRITE, ACCESS_MODE_READ: READ }, @@ -189,7 +196,7 @@ function parseServers (rawRoutingTable, routerAddress) { } } catch (error) { throw newError( - `Unable to parse servers entry from router ${routerAddress} from addresses:\n${JSON.stringify( + `Unable to parse servers entry from router ${routerAddress} from addresses:\n${json.stringify( rawRoutingTable.servers )}\nError message: ${error.message}`, PROTOCOL_ERROR @@ -217,7 +224,7 @@ function calculateExpirationTime (rawRoutingTable, routerAddress) { return expires } catch (error) { throw newError( - `Unable to parse TTL entry from router ${routerAddress} from raw routing table:\n${JSON.stringify( + `Unable to parse TTL entry from router ${routerAddress} from raw routing table:\n${json.stringify( rawRoutingTable )}\nError message: ${error.message}`, PROTOCOL_ERROR diff --git a/bolt-connection/test/bolt/request-message.test.js b/bolt-connection/test/bolt/request-message.test.js index 445c467da..007bbf9f6 100644 --- a/bolt-connection/test/bolt/request-message.test.js +++ b/bolt-connection/test/bolt/request-message.test.js @@ -18,7 +18,7 @@ */ import RequestMessage from '../../src/bolt/request-message' -import { internal, int } from 'neo4j-driver-core' +import { internal, int, json } from 'neo4j-driver-core' const { bookmark: { Bookmark }, @@ -46,7 +46,7 @@ describe('#unit RequestMessage', () => { expect(message.signature).toEqual(0x10) expect(message.fields).toEqual([query, parameters]) expect(message.toString()).toEqual( - `RUN ${query} ${JSON.stringify(parameters)}` + `RUN ${query} ${json.stringify(parameters)}` ) }) @@ -103,7 +103,7 @@ describe('#unit RequestMessage', () => { expect(message.signature).toEqual(0x11) expect(message.fields).toEqual([expectedMetadata]) expect(message.toString()).toEqual( - `BEGIN ${JSON.stringify(expectedMetadata)}` + `BEGIN ${json.stringify(expectedMetadata)}` ) }) }) @@ -156,7 +156,7 @@ describe('#unit RequestMessage', () => { expect(message.signature).toEqual(0x10) expect(message.fields).toEqual([query, parameters, expectedMetadata]) expect(message.toString()).toEqual( - `RUN ${query} ${JSON.stringify(parameters)} ${JSON.stringify( + `RUN ${query} ${json.stringify(parameters)} ${json.stringify( expectedMetadata )}` ) @@ -175,7 +175,7 @@ describe('#unit RequestMessage', () => { function verify (message, signature, metadata, name) { expect(message.signature).toEqual(signature) expect(message.fields).toEqual([metadata]) - expect(message.toString()).toEqual(`${name} ${JSON.stringify(metadata)}`) + expect(message.toString()).toEqual(`${name} ${json.stringify(metadata)}`) } it('should create PULL message', () => { @@ -195,6 +195,15 @@ describe('#unit RequestMessage', () => { ) }) + it('should create PULL message with qid=0n and n', () => { + verify( + RequestMessage.pull({ stmtId: 0n, n: 1023 }), + 0x3f, + { n: int(1023), qid: int(0n) }, + 'PULL' + ) + }) + it('should create DISCARD message', () => { verify(RequestMessage.discard(), 0x2f, { n: int(-1) }, 'DISCARD') }) @@ -216,6 +225,15 @@ describe('#unit RequestMessage', () => { 'DISCARD' ) }) + + it('should create DISCARD message with qid=0n and n', () => { + verify( + RequestMessage.discard({ stmtId: 0n, n: 1023 }), + 0x2f, + { n: int(1023), qid: int(0n) }, + 'DISCARD' + ) + }) }) describe('BoltV4.3', () => { @@ -228,7 +246,7 @@ describe('#unit RequestMessage', () => { expect(message.signature).toEqual(0x66) expect(message.fields).toEqual([requestContext, database]) expect(message.toString()).toEqual( - `ROUTE ${JSON.stringify(requestContext)} ${database}` + `ROUTE ${json.stringify(requestContext)} ${database}` ) }) @@ -237,7 +255,7 @@ describe('#unit RequestMessage', () => { expect(message.signature).toEqual(0x66) expect(message.fields).toEqual([{}, null]) - expect(message.toString()).toEqual(`ROUTE ${JSON.stringify({})} ${null}`) + expect(message.toString()).toEqual(`ROUTE ${json.stringify({})} ${null}`) }) }) }) diff --git a/bolt-connection/test/bolt/stream-observer.test.js b/bolt-connection/test/bolt/stream-observer.test.js index a92227dad..dc15ce435 100644 --- a/bolt-connection/test/bolt/stream-observer.test.js +++ b/bolt-connection/test/bolt/stream-observer.test.js @@ -23,7 +23,7 @@ import { ProcedureRouteObserver } from '../../src/bolt/stream-observers' import { RawRoutingTable } from '../../src/bolt' -import { error, newError, Record } from 'neo4j-driver-core' +import { error, newError, Record, json } from 'neo4j-driver-core' const { PROTOCOL_ERROR } = error @@ -267,7 +267,7 @@ describe('#unit RouteObserver', () => { const record = new Record(['a'], ['b']) const expectedError = newError( 'Received RECORD when resetting: received record is: ' + - JSON.stringify(record), + json.stringify(record), PROTOCOL_ERROR ) @@ -287,7 +287,7 @@ describe('#unit RouteObserver', () => { const record = new Record(['a'], ['b']) const expectedErrorMessage = 'Received RECORD when resetting: received record is: ' + - JSON.stringify(record) + json.stringify(record) newRouteObserver({ onError: null, @@ -342,7 +342,7 @@ describe('#unit ProcedureRouteObserver', () => { let onErrorCalled = false const expectedError = newError( 'Illegal response from router. Received 0 records but expected only one.\n' + - JSON.stringify([]), + json.stringify([]), PROTOCOL_ERROR ) const observer = newRouteObserver({ @@ -362,7 +362,7 @@ describe('#unit ProcedureRouteObserver', () => { let onProtocolErrorCalled = false const expectedErrorMessage = 'Illegal response from router. Received 0 records but expected only one.\n' + - JSON.stringify([]) + json.stringify([]) newRouteObserver({ onError: null, @@ -380,7 +380,7 @@ describe('#unit ProcedureRouteObserver', () => { const record = new Record(['a'], ['b']) const expectedError = newError( 'Illegal response from router. Received 2 records but expected only one.\n' + - JSON.stringify([record, record]), + json.stringify([record, record]), PROTOCOL_ERROR ) const observer = newRouteObserver({ @@ -403,7 +403,7 @@ describe('#unit ProcedureRouteObserver', () => { const record = new Record(['a'], ['b']) const expectedErrorMessage = 'Illegal response from router. Received 2 records but expected only one.\n' + - JSON.stringify([record, record]) + json.stringify([record, record]) const observer = newRouteObserver({ onError: null, diff --git a/bolt-connection/test/packstream/packstream-v1.test.js b/bolt-connection/test/packstream/packstream-v1.test.js index 16ccd93b5..e8e8590b3 100644 --- a/bolt-connection/test/packstream/packstream-v1.test.js +++ b/bolt-connection/test/packstream/packstream-v1.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { int } from 'neo4j-driver-core' +import { int, Integer } from 'neo4j-driver-core' import { alloc } from '../../src/channel' import { Packer, Structure, Unpacker } from '../../src/packstream/packstream-v1' @@ -37,6 +37,21 @@ describe('#unit PackStreamV1', () => { } }) + it('should pack integers with small numbers created with Integer', () => { + let n, i + // test small numbers + for (n = -10; n <= 10; n += 1) { + i = new Integer(n, 0) + expect(packAndUnpack(i).toString()).toBe(i.toString()) + expect( + packAndUnpack(i, { disableLosslessIntegers: true }).toString() + ).toBe(i.toString()) + expect(packAndUnpack(i, { useBigInt: true }).toString()).toBe( + i.toString() + ) + } + }) + it('should pack integers with positive numbers', () => { let n, i // positive numbers diff --git a/bolt-connection/test/test-utils.js b/bolt-connection/test/test-utils.js index 42301b6cc..8e095fdfa 100644 --- a/bolt-connection/test/test-utils.js +++ b/bolt-connection/test/test-utils.js @@ -17,6 +17,7 @@ * limitations under the License. */ import Connection from '../../bolt-connection/src/connection/connection' +import { json } from 'neo4j-driver-core' function isClient () { return typeof window !== 'undefined' && window.document @@ -61,11 +62,11 @@ const matchers = { ) } - if (JSON.stringify(expected.fields) !== JSON.stringify(actual.fields)) { + if (json.stringify(expected.fields) !== json.stringify(actual.fields)) { failures.push( - `fields '[${JSON.stringify( + `fields '[${json.stringify( actual.fields - )}]' to match '[${JSON.stringify(expected.fields)}]'` + )}]' to match '[${json.stringify(expected.fields)}]'` ) } diff --git a/core/src/graph-types.ts b/core/src/graph-types.ts index 746119bd4..1526caac1 100644 --- a/core/src/graph-types.ts +++ b/core/src/graph-types.ts @@ -17,6 +17,7 @@ * limitations under the License. */ import Integer from './integer' +import { stringify } from './json' type StandardDate = Date type NumberOrInteger = number | Integer | bigint @@ -84,7 +85,7 @@ class Node { s += ' {' for (let i = 0; i < keys.length; i++) { if (i > 0) s += ',' - s += keys[i] + ':' + JSON.stringify(this.properties[keys[i]]) + s += keys[i] + ':' + stringify(this.properties[keys[i]]) } s += '}' } @@ -165,7 +166,7 @@ class Relationship { s += ' {' for (let i = 0; i < keys.length; i++) { if (i > 0) s += ',' - s += keys[i] + ':' + JSON.stringify(this.properties[keys[i]]) + s += keys[i] + ':' + stringify(this.properties[keys[i]]) } s += '}' } @@ -251,7 +252,7 @@ class UnboundRelationship { s += ' {' for (let i = 0; i < keys.length; i++) { if (i > 0) s += ',' - s += keys[i] + ':' + JSON.stringify(this.properties[keys[i]]) + s += keys[i] + ':' + stringify(this.properties[keys[i]]) } s += '}' } diff --git a/core/src/index.ts b/core/src/index.ts index f61a71022..da5e4191c 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -72,6 +72,7 @@ import Transaction from './transaction' import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import * as types from './types' +import * as json from './json' import * as internal from './internal' // todo: removed afterwards /** @@ -136,7 +137,8 @@ const forExport = { Driver, Connection, types, - driver + driver, + json } export { @@ -196,7 +198,8 @@ export { TransactionConfig, Driver, types, - driver + driver, + json } export default forExport diff --git a/core/src/internal/util.ts b/core/src/internal/util.ts index 3f10e3c08..b87e3c1dd 100644 --- a/core/src/internal/util.ts +++ b/core/src/internal/util.ts @@ -20,6 +20,7 @@ import Integer, { isInt } from '../integer' import { NumberOrInteger } from '../graph-types' import { EncryptionLevel } from '../types' +import { stringify } from '../json' const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' @@ -102,7 +103,7 @@ function validateQueryAndParameters( function assertObject(obj: any, objName: string): Object { if (!isObject(obj)) { throw new TypeError( - objName + ' expected to be an object but was: ' + JSON.stringify(obj) + objName + ' expected to be an object but was: ' + stringify(obj) ) } return obj @@ -118,7 +119,7 @@ function assertObject(obj: any, objName: string): Object { function assertString(obj: any, objName: Object): string { if (!isString(obj)) { throw new TypeError( - objName + ' expected to be string but was: ' + JSON.stringify(obj) + objName + ' expected to be string but was: ' + stringify(obj) ) } return obj @@ -134,7 +135,7 @@ function assertString(obj: any, objName: Object): string { function assertNumber(obj: any, objName: string): number { if (typeof obj !== 'number') { throw new TypeError( - objName + ' expected to be a number but was: ' + JSON.stringify(obj) + objName + ' expected to be a number but was: ' + stringify(obj) ) } return obj @@ -152,7 +153,7 @@ function assertNumberOrInteger(obj: any, objName: string): NumberOrInteger { throw new TypeError( objName + ' expected to be either a number or an Integer object but was: ' + - JSON.stringify(obj) + stringify(obj) ) } return obj @@ -170,14 +171,14 @@ function assertValidDate(obj: any, objName: string): Date { throw new TypeError( objName + ' expected to be a standard JavaScript Date but was: ' + - JSON.stringify(obj) + stringify(obj) ) } if (Number.isNaN(obj.getTime())) { throw new TypeError( objName + ' expected to be valid JavaScript Date but its time was NaN: ' + - JSON.stringify(obj) + stringify(obj) ) } return obj diff --git a/core/src/json.ts b/core/src/json.ts new file mode 100644 index 000000000..f1aa12b22 --- /dev/null +++ b/core/src/json.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides a interface to serialize values which doesn't support serialization by JSON.stringify + * @private + * @param val The value to be serialized as JSON + * @returns the json + */ +export function stringify (val: any) { + return JSON.stringify(val, (_, value) => + typeof value !== 'bigint' ? value : `${value}n` + ) +} diff --git a/test/driver.test.js b/test/driver.test.js index 3cb4ed059..b41255fa2 100644 --- a/test/driver.test.js +++ b/test/driver.test.js @@ -26,6 +26,7 @@ import { } from '../bolt-connection/lib/pool/pool-config' import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' import testUtils from './internal/test-utils' +import { json as JSON } from 'neo4j-driver-core' // As long as driver creation doesn't touch the network it's fine to run // this as a unit test. diff --git a/test/internal/shared-neo4j.js b/test/internal/shared-neo4j.js index b0d331665..93d16e4e5 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -18,6 +18,7 @@ */ import neo4j from '../../src' +import { json as JSON } from 'neo4j-driver-core' class UnsupportedPlatform { pathJoin () { diff --git a/test/session.test.js b/test/session.test.js index 7657de8d6..1cd9f11dd 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -29,7 +29,8 @@ import { error, queryType, Session, - internal + internal, + json as JSON } from 'neo4j-driver-core' const { From 8f30dccf8152f1b6e2f36938a2a2082ed2732155 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 31 Mar 2021 14:39:01 +0200 Subject: [PATCH 4/5] improve useBigInt docs --- neo4j-driver-lite/src/index.ts | 6 +++++- src/index.js | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/neo4j-driver-lite/src/index.ts b/neo4j-driver-lite/src/index.ts index 3abe3cc00..48f6c05be 100644 --- a/neo4j-driver-lite/src/index.ts +++ b/neo4j-driver-lite/src/index.ts @@ -172,7 +172,11 @@ const { * disableLosslessIntegers: false, * * // Make this driver always return native Javascript {@link BigInt} for integer values, instead of the dedicated {@link Integer} class or {@link Number}. - * // Default value for this option is `false` for backwards compatibility while {@link Integer} is not depreacted + * // + * // Default value for this option is `false` for backwards compatibility. + * // + * // **Warning:** `BigInt` doesn't implement the method `toJSON`. In maner of serialize it as `json`, It's needed to add a custom implementation of the `toJSON` on the + * // `BigInt.prototype` {@see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json} * useBigInt: false, * * // Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. diff --git a/src/index.js b/src/index.js index 457e0edba..2aa16d3b3 100644 --- a/src/index.js +++ b/src/index.js @@ -148,7 +148,11 @@ const { * disableLosslessIntegers: false, * * // Make this driver always return native Javascript {@link BigInt} for integer values, instead of the dedicated {@link Integer} class or {@link Number}. - * // Default value for this option is `false` for backwards compatibility while {@link Integer} is not depreacted + * // + * // Default value for this option is `false` for backwards compatibility. + * // + * // **Warning:** `BigInt` doesn't implement the method `toJSON`. In maner of serialize it as `json`, It's needed to add a custom implementation of the `toJSON` on the + * // `BigInt.prototype` {@see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json} * useBigInt: false, * * // Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. From 24c11af7ef05b8b1ed0bab24bcaac3d1f718405a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 1 Apr 2021 08:29:42 +0200 Subject: [PATCH 5/5] Addressing PR comments --- bolt-connection/test/bolt/bolt-protocol-v1.test.js | 6 ++++-- bolt-connection/test/bolt/bolt-protocol-v3.test.js | 10 ++++++---- bolt-connection/test/bolt/request-message.test.js | 11 +++++++---- core/src/json.ts | 8 ++++---- test/driver.test.js | 4 ++-- test/internal/shared-neo4j.js | 4 ++-- test/session.test.js | 6 +++--- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bolt-connection/test/bolt/bolt-protocol-v1.test.js b/bolt-connection/test/bolt/bolt-protocol-v1.test.js index b7e831e80..9c4205f57 100644 --- a/bolt-connection/test/bolt/bolt-protocol-v1.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -23,6 +23,8 @@ import { internal } from 'neo4j-driver-core' import utils from '../test-utils' import { LoginObserver } from '../../src/bolt/stream-observers' +const WRITE = 'WRITE' + const { bookmark: { Bookmark }, txConfig: { TxConfig } @@ -96,7 +98,7 @@ describe('#unit BoltProtocolV1', () => { const observer = protocol.run(query, parameters, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), - mode: 'WRITE' + mode: WRITE }) protocol.verifyMessageCount(2) @@ -134,7 +136,7 @@ describe('#unit BoltProtocolV1', () => { const observer = protocol.beginTransaction({ bookmark: bookmark, txConfig: TxConfig.empty(), - mode: 'WRITE' + mode: WRITE }) protocol.verifyMessageCount(2) diff --git a/bolt-connection/test/bolt/bolt-protocol-v3.test.js b/bolt-connection/test/bolt/bolt-protocol-v3.test.js index 87a863c22..543e06a52 100644 --- a/bolt-connection/test/bolt/bolt-protocol-v3.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -31,6 +31,8 @@ const { txConfig: { TxConfig } } = internal +const WRITE = 'WRITE' + describe('#unit BoltProtocolV3', () => { beforeEach(() => { expect.extend(utils.matchers) @@ -87,7 +89,7 @@ describe('#unit BoltProtocolV3', () => { const observer = protocol.run(query, parameters, { bookmark, txConfig, - mode: 'WRITE' + mode: WRITE }) protocol.verifyMessageCount(2) @@ -96,7 +98,7 @@ describe('#unit BoltProtocolV3', () => { RequestMessage.runWithMetadata(query, parameters, { bookmark, txConfig, - mode: 'WRITE' + mode: WRITE }) ) expect(protocol.messages[1]).toBeMessage(RequestMessage.pullAll()) @@ -120,12 +122,12 @@ describe('#unit BoltProtocolV3', () => { const observer = protocol.beginTransaction({ bookmark, txConfig, - mode: 'WRITE' + mode: WRITE }) protocol.verifyMessageCount(1) expect(protocol.messages[0]).toBeMessage( - RequestMessage.begin({ bookmark, txConfig, mode: 'WRITE' }) + RequestMessage.begin({ bookmark, txConfig, mode: WRITE }) ) expect(protocol.observers).toEqual([observer]) expect(protocol.flushes).toEqual([true]) diff --git a/bolt-connection/test/bolt/request-message.test.js b/bolt-connection/test/bolt/request-message.test.js index 007bbf9f6..7dc88be60 100644 --- a/bolt-connection/test/bolt/request-message.test.js +++ b/bolt-connection/test/bolt/request-message.test.js @@ -25,6 +25,9 @@ const { txConfig: { TxConfig } } = internal +const WRITE = 'WRITE' +const READ = 'READ' + describe('#unit RequestMessage', () => { it('should create INIT message', () => { const userAgent = 'my-driver/1.0.2' @@ -82,7 +85,7 @@ describe('#unit RequestMessage', () => { }) it('should create BEGIN message', () => { - ;['READ', 'WRITE'].forEach(mode => { + ;[READ, WRITE].forEach(mode => { const bookmark = new Bookmark([ 'neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx10' @@ -96,7 +99,7 @@ describe('#unit RequestMessage', () => { tx_timeout: int(42), tx_metadata: { key: 42 } } - if (mode === 'READ') { + if (mode === READ) { expectedMetadata.mode = 'r' } @@ -125,7 +128,7 @@ describe('#unit RequestMessage', () => { }) it('should create RUN with metadata message', () => { - ;['READ', 'WRITE'].forEach(mode => { + ;[READ, WRITE].forEach(mode => { const query = 'RETURN $x' const parameters = { x: 42 } const bookmark = new Bookmark([ @@ -149,7 +152,7 @@ describe('#unit RequestMessage', () => { tx_timeout: int(999), tx_metadata: { a: 'a', b: 'b' } } - if (mode === 'READ') { + if (mode === READ) { expectedMetadata.mode = 'r' } diff --git a/core/src/json.ts b/core/src/json.ts index f1aa12b22..a1eeebf91 100644 --- a/core/src/json.ts +++ b/core/src/json.ts @@ -18,13 +18,13 @@ */ /** - * Provides a interface to serialize values which doesn't support serialization by JSON.stringify + * Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt. * @private - * @param val The value to be serialized as JSON - * @returns the json + * @param val A JavaScript value, usually an object or array, to be converted. + * @returns A JSON string representing the given value. */ export function stringify (val: any) { return JSON.stringify(val, (_, value) => - typeof value !== 'bigint' ? value : `${value}n` + typeof value === 'bigint' ? `${value}n` : value ) } diff --git a/test/driver.test.js b/test/driver.test.js index b41255fa2..9dd464eb3 100644 --- a/test/driver.test.js +++ b/test/driver.test.js @@ -26,7 +26,7 @@ import { } from '../bolt-connection/lib/pool/pool-config' import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' import testUtils from './internal/test-utils' -import { json as JSON } from 'neo4j-driver-core' +import { json } from 'neo4j-driver-core' // As long as driver creation doesn't touch the network it's fine to run // this as a unit test. @@ -220,7 +220,7 @@ describe('#integration driver', () => { .run('RETURN 1') .then(result => { done.fail( - 'Should not be able to connect. Result: ' + JSON.stringify(result) + 'Should not be able to connect. Result: ' + json.stringify(result) ) }) .catch(error => { diff --git a/test/internal/shared-neo4j.js b/test/internal/shared-neo4j.js index 93d16e4e5..84a3f40ad 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -18,7 +18,7 @@ */ import neo4j from '../../src' -import { json as JSON } from 'neo4j-driver-core' +import { json } from 'neo4j-driver-core' class UnsupportedPlatform { pathJoin () { @@ -223,7 +223,7 @@ function install () { function configure (config) { console.log( - `Configuring neo4j at "${neo4jDir()}" with "${JSON.stringify(config)}"` + `Configuring neo4j at "${neo4jDir()}" with "${json.stringify(config)}"` ) const configEntries = Object.keys(config).map(key => `${key}=${config[key]}`) diff --git a/test/session.test.js b/test/session.test.js index 1cd9f11dd..092a983bb 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -30,7 +30,7 @@ import { queryType, Session, internal, - json as JSON + json } from 'neo4j-driver-core' const { @@ -1035,7 +1035,7 @@ describe('#integration session', () => { .then(result => { done.fail( 'Failure expected but query returned ' + - JSON.stringify(result.records[0].get(0)) + json.stringify(result.records[0].get(0)) ) }) .catch(error => { @@ -1160,7 +1160,7 @@ describe('#integration session', () => { session .close() .then(() => - done.fail('Retries should not succeed: ' + JSON.stringify(result)) + done.fail('Retries should not succeed: ' + json.stringify(result)) ) ) .catch(error => {