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/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 979c7fdf6..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' @@ -62,6 +62,7 @@ export function createChannelConnection ( chunker, dechunker, disableLosslessIntegers: config.disableLosslessIntegers, + useBigInt: config.useBigInt, serversideRouting, server: conn.server, log, @@ -246,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/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/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/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..9c4205f57 100644 --- a/test/internal/bolt/bolt-protocol-v1.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v1.test.js @@ -17,12 +17,13 @@ * 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 WRITE = 'WRITE' const { bookmark: { Bookmark }, @@ -31,7 +32,7 @@ const { describe('#unit BoltProtocolV1', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should not change metadata', () => { @@ -275,4 +276,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/bolt-connection/test/bolt/bolt-protocol-v2.test.js b/bolt-connection/test/bolt/bolt-protocol-v2.test.js new file mode 100644 index 000000000..c409f8026 --- /dev/null +++ b/bolt-connection/test/bolt/bolt-protocol-v2.test.js @@ -0,0 +1,54 @@ +/** + * 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 BoltProtocolV2 from '../../src/bolt/bolt-protocol-v2' +import utils from '../test-utils' + +describe('#unit BoltProtocolV2', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV2(null, null, false) + + 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 89% rename from test/internal/bolt/bolt-protocol-v3.test.js rename to bolt-connection/test/bolt/bolt-protocol-v3.test.js index 8967eb55f..543e06a52 100644 --- a/test/internal/bolt/bolt-protocol-v3.test.js +++ b/bolt-connection/test/bolt/bolt-protocol-v3.test.js @@ -17,24 +17,25 @@ * 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 }, txConfig: { TxConfig } } = internal +const WRITE = 'WRITE' + describe('#unit BoltProtocolV3', () => { beforeEach(() => { - jasmine.addMatchers(utils.matchers) + expect.extend(utils.matchers) }) it('should update metadata', () => { @@ -232,6 +233,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 87% rename from test/internal/bolt/request-message.test.js rename to bolt-connection/test/bolt/request-message.test.js index 168053640..7dc88be60 100644 --- a/test/internal/bolt/request-message.test.js +++ b/bolt-connection/test/bolt/request-message.test.js @@ -17,16 +17,17 @@ * 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, json } from 'neo4j-driver-core' const { bookmark: { Bookmark }, txConfig: { TxConfig } } = internal +const WRITE = 'WRITE' +const READ = 'READ' + describe('#unit RequestMessage', () => { it('should create INIT message', () => { const userAgent = 'my-driver/1.0.2' @@ -48,7 +49,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)}` ) }) @@ -105,7 +106,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)}` ) }) }) @@ -158,7 +159,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 )}` ) @@ -177,7 +178,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', () => { @@ -197,6 +198,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') }) @@ -218,6 +228,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', () => { @@ -230,7 +249,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}` ) }) @@ -239,7 +258,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/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 97% rename from test/internal/bolt/stream-observer.test.js rename to bolt-connection/test/bolt/stream-observer.test.js index dc2a02a8a..dc15ce435 100644 --- a/test/internal/bolt/stream-observer.test.js +++ b/bolt-connection/test/bolt/stream-observer.test.js @@ -21,9 +21,9 @@ import { ResultStreamObserver, RouteObserver, ProcedureRouteObserver -} from '../../../bolt-connection/lib/bolt/stream-observers' -import { RawRoutingTable } from '../../../bolt-connection/lib/bolt' -import { error, newError, Record } from 'neo4j-driver-core' +} from '../../src/bolt/stream-observers' +import { RawRoutingTable } from '../../src/bolt' +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/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..e8e8590b3 --- /dev/null +++ b/bolt-connection/test/packstream/packstream-v1.test.js @@ -0,0 +1,194 @@ +/** + * 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, Integer } 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 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 + 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..8e095fdfa --- /dev/null +++ b/bolt-connection/test/test-utils.js @@ -0,0 +1,140 @@ +/** + * 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' +import { json } from 'neo4j-driver-core' + +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..1526caac1 100644 --- a/core/src/graph-types.ts +++ b/core/src/graph-types.ts @@ -17,9 +17,10 @@ * limitations under the License. */ import Integer from './integer' +import { stringify } from './json' type StandardDate = Date -type NumberOrInteger = number | Integer +type NumberOrInteger = number | Integer | bigint const IDENTIFIER_PROPERTY_ATTRIBUTES = { value: true, @@ -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/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..b87e3c1dd 100644 --- a/core/src/internal/util.ts +++ b/core/src/internal/util.ts @@ -18,7 +18,9 @@ */ 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' @@ -101,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 @@ -117,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 @@ -133,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 @@ -146,12 +148,12 @@ 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: ' + - JSON.stringify(obj) + stringify(obj) ) } return obj @@ -169,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/test/internal/bolt/bolt-protocol-v2.test.js b/core/src/json.ts similarity index 61% rename from test/internal/bolt/bolt-protocol-v2.test.js rename to core/src/json.ts index 4a3da5a5c..a1eeebf91 100644 --- a/test/internal/bolt/bolt-protocol-v2.test.js +++ b/core/src/json.ts @@ -17,17 +17,14 @@ * limitations under the License. */ -import BoltProtocolV2 from '../../../bolt-connection/lib/bolt/bolt-protocol-v2' -import utils from '../test-utils' - -describe('#unit BoltProtocolV2', () => { - beforeEach(() => { - jasmine.addMatchers(utils.matchers) - }) - - it('should return correct bolt version number', () => { - const protocol = new BoltProtocolV2(null, null, false) - - expect(protocol.version).toBe(2) - }) -}) +/** + * Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt. + * @private + * @param val A JavaScript value, usually an object or array, to be converted. + * @returns A JSON string representing the given value. + */ +export function stringify (val: any) { + return JSON.stringify(val, (_, value) => + typeof value === 'bigint' ? `${value}n` : value + ) +} 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/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 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..48f6c05be 100644 --- a/neo4j-driver-lite/src/index.ts +++ b/neo4j-driver-lite/src/index.ts @@ -171,6 +171,14 @@ 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. + * // + * // **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`. * // * // 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..2aa16d3b3 100644 --- a/src/index.js +++ b/src/index.js @@ -147,6 +147,14 @@ 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. + * // + * // **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`. * // * // 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/driver.test.js b/test/driver.test.js index 3cb4ed059..9dd464eb3 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 } from 'neo4j-driver-core' // As long as driver creation doesn't touch the network it's fine to run // this as a unit test. @@ -219,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/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/shared-neo4j.js b/test/internal/shared-neo4j.js index b0d331665..84a3f40ad 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -18,6 +18,7 @@ */ import neo4j from '../../src' +import { json } from 'neo4j-driver-core' class UnsupportedPlatform { pathJoin () { @@ -222,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/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/test/session.test.js b/test/session.test.js index 7657de8d6..092a983bb 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -29,7 +29,8 @@ import { error, queryType, Session, - internal + internal, + json } from 'neo4j-driver-core' const { @@ -1034,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 => { @@ -1159,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 => { 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 }) }