From 7b2fc59a1fb17e2452991e2ea1849b91d67b33f2 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 23 Jun 2022 15:56:30 +0200 Subject: [PATCH 1/3] Implementing new structures for DateTime The structures with signature `0x46` and `0x66` are being replaced by `0x49` and `0x69`. This new structures changes the meaning of seconds and nano seconds from `adjusted Unix epoch` to `UTC`. This changes have with goal of avoiding unexistent or ambiguos ZonedDateTime to be received or sent over Bolt. Bolt v4.3 and v4.4 were patched to support this feature if the server supports the patch. This is a backport of https://github.com/neo4j/neo4j-javascript-driver/pull/948 --- .../src/bolt/bolt-protocol-v4x3.js | 47 +++ .../src/bolt/request-message.js | 5 +- .../src/packstream/packstream-utc.js | 323 ++++++++++++++++++ .../src/packstream/packstream-v2.js | 25 +- .../test/bolt/bolt-protocol-v4x3.test.js | 73 +++- .../test/bolt/bolt-protocol-v4x4.test.js | 74 +++- packages/bolt-connection/test/test-utils.js | 4 + packages/core/src/temporal-types.ts | 26 +- .../neo4j-driver/test/temporal-types.test.js | 3 - .../src/cypher-native-binders.js | 114 ++++++- .../testkit-backend/src/request-handlers.js | 2 + .../src/skipped-tests/common.js | 12 + 12 files changed, 678 insertions(+), 30 deletions(-) create mode 100644 packages/bolt-connection/src/packstream/packstream-utc.js diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js index 8d99f572e..0a935274c 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js @@ -19,6 +19,8 @@ import BoltProtocolV42 from './bolt-protocol-v4x2' import RequestMessage from './request-message' import { RouteObserver } from './stream-observers' +import RequestMessage from './request-message' +import { LoginObserver } from './stream-observers' import { internal } from 'neo4j-driver-core' @@ -65,4 +67,49 @@ export default class BoltProtocol extends BoltProtocolV42 { return observer } + + /** + * Initialize a connection with the server + * + * @param {Object} param0 The params + * @param {string} param0.userAgent The user agent + * @param {any} param0.authToken The auth token + * @param {function(error)} param0.onError On error callback + * @param {function(onComplte)} param0.onComplete On complete callback + * @returns {LoginObserver} The Login observer + */ + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => { + if (metadata.patch_bolt !== undefined) { + this._applyPatches(metadata.patch_bolt) + } + return this._onLoginCompleted(metadata, onComplete) + } + }) + + this.write( + RequestMessage.hello(userAgent, authToken, this._serversideRouting, ['utc']), + observer, + true + ) + + return observer + } + + /** + * + * @param {string[]} patches Patches to be applied to the protocol + */ + _applyPatches (patches) { + if (patches.includes('utc')) { + this._applyUtcPatch() + } + } + + _applyUtcPatch () { + this._packer.useUtc = true + this._unpacker.useUtc = true + } } diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index 11b29852f..1538f4a1c 100644 --- a/packages/bolt-connection/src/bolt/request-message.js +++ b/packages/bolt-connection/src/bolt/request-message.js @@ -106,11 +106,14 @@ export default class RequestMessage { * @param {Object} optional server side routing, set to routing context to turn on server side routing (> 4.1) * @return {RequestMessage} new HELLO message. */ - static hello (userAgent, authToken, routing = null) { + static hello (userAgent, authToken, routing = null, patchs = null) { const metadata = Object.assign({ user_agent: userAgent }, authToken) if (routing) { metadata.routing = routing } + if (patchs) { + metadata.patch_bolt = patchs + } return new RequestMessage( HELLO, [metadata], diff --git a/packages/bolt-connection/src/packstream/packstream-utc.js b/packages/bolt-connection/src/packstream/packstream-utc.js new file mode 100644 index 000000000..377ff95a2 --- /dev/null +++ b/packages/bolt-connection/src/packstream/packstream-utc.js @@ -0,0 +1,323 @@ +/** + * 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 { + DateTime, + isInt, + int, + internal + } from 'neo4j-driver-core' + + + import { + epochSecondAndNanoToLocalDateTime + } from './temporal-factory' + + const { + temporalUtil: { + localDateTimeToEpochSecond + } + } = internal + + export const DATE_TIME_WITH_ZONE_OFFSET = 0x49 + const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3 + + export const DATE_TIME_WITH_ZONE_ID = 0x69 + const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3 + + /** + * Unpack date time with zone offset value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @param {boolean} disableLosslessIntegers if integer properties in the result date-time should be native JS numbers. + * @return {DateTime} the unpacked date time with zone offset value. + */ +export function unpackDateTimeWithZoneOffset ( + unpacker, + structSize, + buffer, + disableLosslessIntegers, + useBigInt +) { + unpacker._verifyStructSize( + 'DateTimeWithZoneOffset', + DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE, + structSize + ) + + const utcSecond = unpacker.unpackInteger(buffer) + const nano = unpacker.unpackInteger(buffer) + const timeZoneOffsetSeconds = unpacker.unpackInteger(buffer) + + const epochSecond = int(utcSecond).add(timeZoneOffsetSeconds) + const localDateTime = epochSecondAndNanoToLocalDateTime(epochSecond, nano) + const result = new DateTime( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + localDateTime.nanosecond, + timeZoneOffsetSeconds, + null + ) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) +} + +/** + * Unpack date time with zone id value using the given unpacker. + * @param {Unpacker} unpacker the unpacker to use. + * @param {number} structSize the retrieved struct size. + * @param {BaseBuffer} buffer the buffer to unpack from. + * @param {boolean} disableLosslessIntegers if integer properties in the result date-time should be native JS numbers. + * @return {DateTime} the unpacked date time with zone id value. + */ + export function unpackDateTimeWithZoneId ( + unpacker, + structSize, + buffer, + disableLosslessIntegers, + useBigInt +) { + unpacker._verifyStructSize( + 'DateTimeWithZoneId', + DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE, + structSize + ) + + const epochSecond = unpacker.unpackInteger(buffer) + const nano = unpacker.unpackInteger(buffer) + const timeZoneId = unpacker.unpack(buffer) + + const localDateTime = getTimeInZoneId(timeZoneId, epochSecond, nano) + + const result = new DateTime( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + int(nano), + localDateTime.timeZoneOffsetSeconds, + timeZoneId + ) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) +} + +/* +* Pack given date time. +* @param {DateTime} value the date time value to pack. +* @param {Packer} packer the packer to use. +*/ +export function packDateTime (value, packer) { + if (value.timeZoneId) { + packDateTimeWithZoneId(value, packer) + } else { + packDateTimeWithZoneOffset(value, packer) + } +} + +/** + * Pack given date time with zone id. + * @param {DateTime} value the date time value to pack. + * @param {Packer} packer the packer to use. + */ + function packDateTimeWithZoneId (value, packer) { + + const epochSecond = localDateTimeToEpochSecond( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.nanosecond + ) + + const offset = value.timeZoneOffsetSeconds != null + ? value.timeZoneOffsetSeconds + : getOffsetFromZoneId(value.timeZoneId, epochSecond, value.nanosecond) + + const utc = epochSecond.subtract(offset) + const nano = int(value.nanosecond) + const timeZoneId = value.timeZoneId + + const packableStructFields = [ + packer.packable(utc), + packer.packable(nano), + packer.packable(timeZoneId) + ] + packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields) +} + +/** + * Pack given date time with zone offset. + * @param {DateTime} value the date time value to pack. + * @param {Packer} packer the packer to use. + */ + function packDateTimeWithZoneOffset (value, packer) { + const epochSecond = localDateTimeToEpochSecond( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.nanosecond + ) + const nano = int(value.nanosecond) + const timeZoneOffsetSeconds = int(value.timeZoneOffsetSeconds) + const utcSecond = epochSecond.subtract(timeZoneOffsetSeconds) + + const packableStructFields = [ + packer.packable(utcSecond), + packer.packable(nano), + packer.packable(timeZoneOffsetSeconds) + ] + packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields) +} + + + /** + * Returns the offset for a given timezone id + * + * Javascript doesn't have support for direct getting the timezone offset from a given + * TimeZoneId and DateTime in the given TimeZoneId. For solving this issue, + * + * 1. The ZoneId is applied to the timestamp, so we could make the difference between the + * given timestamp and the new calculated one. This is the offset for the timezone + * in the utc is equal to epoch (some time in the future or past) + * 2. The offset is subtracted from the timestamp, so we have an estimated utc timestamp. + * 3. The ZoneId is applied to the new timestamp, se we could could make the difference + * between the new timestamp and the calculated one. This is the offset for the given timezone. + * + * Example: + * Input: 2022-3-27 1:59:59 'Europe/Berlin' + * Apply 1, 2022-3-27 1:59:59 => 2022-3-27 3:59:59 'Europe/Berlin' +2:00 + * Apply 2, 2022-3-27 1:59:59 - 2:00 => 2022-3-26 23:59:59 + * Apply 3, 2022-3-26 23:59:59 => 2022-3-27 00:59:59 'Europe/Berlin' +1:00 + * The offset is +1 hour. + * + * @param {string} timeZoneId The timezone id + * @param {Integer} epochSecond The epoch second in the timezone id + * @param {Integerable} nanosecond The nanoseconds in the timezone id + * @returns The timezone offset + */ + function getOffsetFromZoneId (timeZoneId, epochSecond, nanosecond) { + const dateTimeWithZoneAppliedTwice = getTimeInZoneId(timeZoneId, epochSecond, nanosecond) + + // The wallclock form the current date time + const epochWithZoneAppliedTwice = localDateTimeToEpochSecond( + dateTimeWithZoneAppliedTwice.year, + dateTimeWithZoneAppliedTwice.month, + dateTimeWithZoneAppliedTwice.day, + dateTimeWithZoneAppliedTwice.hour, + dateTimeWithZoneAppliedTwice.minute, + dateTimeWithZoneAppliedTwice.second, + nanosecond) + + const offsetOfZoneInTheFutureUtc = epochWithZoneAppliedTwice.subtract(epochSecond) + const guessedUtc = epochSecond.subtract(offsetOfZoneInTheFutureUtc) + + const zonedDateTimeFromGuessedUtc = getTimeInZoneId(timeZoneId, guessedUtc, nanosecond) + + const zonedEpochFromGuessedUtc = localDateTimeToEpochSecond( + zonedDateTimeFromGuessedUtc.year, + zonedDateTimeFromGuessedUtc.month, + zonedDateTimeFromGuessedUtc.day, + zonedDateTimeFromGuessedUtc.hour, + zonedDateTimeFromGuessedUtc.minute, + zonedDateTimeFromGuessedUtc.second, + nanosecond) + + const offset = zonedEpochFromGuessedUtc.subtract(guessedUtc) + return offset + } + + function getTimeInZoneId (timeZoneId, epochSecond, nano) { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timeZoneId, + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false + }) + + const l = epochSecondAndNanoToLocalDateTime(epochSecond, nano) + const utc = Date.UTC( + int(l.year).toNumber(), + int(l.month).toNumber() - 1, + int(l.day).toNumber(), + int(l.hour).toNumber(), + int(l.minute).toNumber(), + int(l.second).toNumber() + ) + + const formattedUtcParts = formatter.formatToParts(utc) + + const localDateTime = formattedUtcParts.reduce((obj, currentValue) => { + if (currentValue.type !== 'literal') { + obj[currentValue.type] = int(currentValue.value) + } + return obj + }, {}) + + const epochInTimeZone = localDateTimeToEpochSecond( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + localDateTime.nanosecond + ) + + localDateTime.timeZoneOffsetSeconds = epochInTimeZone.subtract(epochSecond) + localDateTime.hour = localDateTime.hour.modulo(24) + + return localDateTime + } + + + 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 (Object.prototype.hasOwnProperty.call(obj, prop) === true) { + const value = obj[prop] + clone[prop] = isInt(value) ? convert(value) : value + } + } + Object.freeze(clone) + return clone + } + + diff --git a/packages/bolt-connection/src/packstream/packstream-v2.js b/packages/bolt-connection/src/packstream/packstream-v2.js index d1c1b8ed2..0126f3677 100644 --- a/packages/bolt-connection/src/packstream/packstream-v2.js +++ b/packages/bolt-connection/src/packstream/packstream-v2.js @@ -18,6 +18,7 @@ */ import * as v1 from './packstream-v1' +import * as utc from './packstream-utc' import { int, isInt, @@ -94,7 +95,9 @@ export class Packer extends v1.Packer { return () => packDate(obj, this) } else if (isLocalDateTime(obj)) { return () => packLocalDateTime(obj, this) - } else if (isDateTime(obj)) { + } else if (isDateTime(obj) && this.useUtc) { + return () => utc.packDateTime(obj, this) + } else if (isDateTime(obj) && !this.useUtc) { return () => packDateTime(obj, this) } else { return super.packable(obj) @@ -151,7 +154,15 @@ export class Unpacker extends v1.Unpacker { this._disableLosslessIntegers, this._useBigInt ) - } else if (signature === DATE_TIME_WITH_ZONE_OFFSET) { + } else if (signature === utc.DATE_TIME_WITH_ZONE_OFFSET && this.useUtc) { + return utc.unpackDateTimeWithZoneOffset( + this, + structSize, + buffer, + this._disableLosslessIntegers, + this._useBigInt + ) + } else if (signature === DATE_TIME_WITH_ZONE_OFFSET && !this.useUtc) { return unpackDateTimeWithZoneOffset( this, structSize, @@ -159,7 +170,15 @@ export class Unpacker extends v1.Unpacker { this._disableLosslessIntegers, this._useBigInt ) - } else if (signature === DATE_TIME_WITH_ZONE_ID) { + } else if (signature === utc.DATE_TIME_WITH_ZONE_ID && this.useUtc) { + return utc.unpackDateTimeWithZoneId( + this, + structSize, + buffer, + this._disableLosslessIntegers, + this._useBigInt + ) + } else if (signature === DATE_TIME_WITH_ZONE_ID && !this.useUtc) { return unpackDateTimeWithZoneId( this, structSize, diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js index 41c5f4de1..b2d34e86d 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -22,12 +22,14 @@ import RequestMessage from '../../src/bolt/request-message' import utils from '../test-utils' import { RouteObserver } from '../../src/bolt/stream-observers' import { internal } from 'neo4j-driver-core' +import { alloc } from '../../src/channel' const WRITE = 'WRITE' const { txConfig: { TxConfig }, - bookmark: { Bookmark } + bookmark: { Bookmark }, + logger: { Logger } } = internal describe('#unit BoltProtocolV4x3', () => { @@ -179,7 +181,7 @@ describe('#unit BoltProtocolV4x3', () => { protocol.verifyMessageCount(1) expect(protocol.messages[0]).toBeMessage( - RequestMessage.hello(clientName, authToken) + RequestMessage.hello(clientName, authToken, null, ['utc']) ) expect(protocol.observers).toEqual([observer]) expect(protocol.flushes).toEqual([true]) @@ -299,4 +301,71 @@ describe('#unit BoltProtocolV4x3', () => { } ) }) + + describe('utc patch', () => { + describe('the server accepted the patch', () => { + let protocol + let buffer + let loggerFunction + + beforeEach(() => { + buffer = alloc(256) + loggerFunction = jest.fn() + protocol = new BoltProtocolV4x3( + new utils.MessageRecordingConnection(), + buffer, + { disableLosslessIntegers: true }, + undefined, + new Logger('debug', loggerFunction) + ) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + observer.onCompleted({ patch_bolt: ['utc'] }) + + buffer.reset() + }) + + it('should enable utc in the packer and unpacker', () => { + expect(protocol._packer.useUtc).toBe(true) + expect(protocol._unpacker.useUtc).toBe(true) + }) + }) + + describe('the server does not the patch', () => { + let protocol + let buffer + let loggerFunction + + beforeEach(() => { + buffer = alloc(256) + loggerFunction = jest.fn() + protocol = new BoltProtocolV4x3( + new utils.MessageRecordingConnection(), + buffer, + { disableLosslessIntegers: true }, + undefined, + new Logger('debug', loggerFunction) + ) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + observer.onCompleted({}) + + buffer.reset() + }) + it('should not enable utc in the packer and unpacker', () => { + expect(protocol._packer.useUtc).not.toBe(true) + expect(protocol._unpacker.useUtc).not.toBe(true) + }) + }) + }) }) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js index 2314922ed..12e8f887f 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js @@ -22,12 +22,14 @@ import RequestMessage from '../../src/bolt/request-message' import utils from '../test-utils' import { RouteObserver } from '../../src/bolt/stream-observers' import { internal } from 'neo4j-driver-core' +import { alloc } from '../../src/channel' const WRITE = 'WRITE' const { txConfig: { TxConfig }, - bookmark: { Bookmark } + bookmark: { Bookmark }, + logger: { Logger } } = internal describe('#unit BoltProtocolV4x4', () => { @@ -253,7 +255,7 @@ describe('#unit BoltProtocolV4x4', () => { protocol.verifyMessageCount(1) expect(protocol.messages[0]).toBeMessage( - RequestMessage.hello(clientName, authToken) + RequestMessage.hello(clientName, authToken, null, ['utc']) ) expect(protocol.observers).toEqual([observer]) expect(protocol.flushes).toEqual([true]) @@ -332,4 +334,72 @@ describe('#unit BoltProtocolV4x4', () => { } ) }) + + describe('utc patch', () => { + describe('the server accepted the patch', () => { + let protocol + let buffer + let loggerFunction + + beforeEach(() => { + buffer = alloc(256) + loggerFunction = jest.fn() + protocol = new BoltProtocolV4x4( + new utils.MessageRecordingConnection(), + buffer, + { disableLosslessIntegers: true }, + undefined, + new Logger('debug', loggerFunction) + ) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + observer.onCompleted({ patch_bolt: ['utc'] }) + + buffer.reset() + }) + + it('should enable utc in the packer and unpacker', () => { + expect(protocol._packer.useUtc).toBe(true) + expect(protocol._unpacker.useUtc).toBe(true) + }) + }) + + describe('the server does not the patch', () => { + let protocol + let buffer + let loggerFunction + + beforeEach(() => { + buffer = alloc(256) + loggerFunction = jest.fn() + protocol = new BoltProtocolV4x4( + new utils.MessageRecordingConnection(), + buffer, + { disableLosslessIntegers: true }, + undefined, + new Logger('debug', loggerFunction) + ) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + observer.onCompleted({}) + + buffer.reset() + }) + it('should not enable utc in the packer and unpacker', () => { + expect(protocol._packer.useUtc).not.toBe(true) + expect(protocol._unpacker.useUtc).not.toBe(true) + }) + }) + + }) }) diff --git a/packages/bolt-connection/test/test-utils.js b/packages/bolt-connection/test/test-utils.js index 8e095fdfa..b9e112e03 100644 --- a/packages/bolt-connection/test/test-utils.js +++ b/packages/bolt-connection/test/test-utils.js @@ -106,6 +106,10 @@ class MessageRecordingConnection extends Connection { expect(this.observers.length).toEqual(expected) expect(this.flushes.length).toEqual(expected) } + + get version () { + return 4.3 + } } function spyProtocolWrite (protocol, callRealMethod = false) { diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index a120e464f..d687f250a 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -707,21 +707,25 @@ function verifyTimeZoneArguments( const offsetDefined = timeZoneOffsetSeconds || timeZoneOffsetSeconds === 0 const idDefined = timeZoneId && timeZoneId !== '' - if (offsetDefined && !idDefined) { - assertNumberOrInteger(timeZoneOffsetSeconds, 'Time zone offset in seconds') - return [timeZoneOffsetSeconds, undefined] - } else if (!offsetDefined && idDefined) { - assertString(timeZoneId, 'Time zone ID') - return [undefined, timeZoneId] - } else if (offsetDefined && idDefined) { - throw newError( - `Unable to create DateTime with both time zone offset and id. Please specify either of them. Given offset: ${timeZoneOffsetSeconds} and id: ${timeZoneId}` - ) - } else { + if (!offsetDefined && !idDefined) { throw newError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Unable to create DateTime without either time zone offset or id. Please specify either of them. Given offset: ${timeZoneOffsetSeconds} and id: ${timeZoneId}` ) } + + const result: [NumberOrInteger | undefined | null, string | undefined | null] = [undefined, undefined] + if (offsetDefined) { + assertNumberOrInteger(timeZoneOffsetSeconds, 'Time zone offset in seconds') + result[0] = timeZoneOffsetSeconds + } + + if (idDefined) { + assertString(timeZoneId, 'Time zone ID') + result[1] = timeZoneId + } + + return result } /** diff --git a/packages/neo4j-driver/test/temporal-types.test.js b/packages/neo4j-driver/test/temporal-types.test.js index d074aaee1..5398e4121 100644 --- a/packages/neo4j-driver/test/temporal-types.test.js +++ b/packages/neo4j-driver/test/temporal-types.test.js @@ -1008,9 +1008,6 @@ describe('#integration temporal-types', () => { expect( () => new neo4j.types.DateTime(1, 2, 3, 4, 5, 6, 7, null, null) ).toThrow() - expect( - () => new neo4j.types.DateTime(1, 2, 3, 4, 5, 6, 7, 8, 'UK') - ).toThrow() }, 60000) it('should convert standard Date to neo4j LocalTime', () => { diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index f7852e174..96b647252 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -103,6 +103,42 @@ export function nativeToCypher (x) { } } } + + if (neo4j.isDate(x)) { + return structResponse('CypherDate', { + year: x.year, + month: x.month, + day: x.day + }) + } else if (neo4j.isDateTime(x) || neo4j.isLocalDateTime(x)) { + return structResponse('CypherDateTime', { + year: x.year, + month: x.month, + day: x.day, + hour: x.hour, + minute: x.minute, + second: x.second, + nanosecond: x.nanosecond, + utc_offset_s: x.timeZoneOffsetSeconds || (x.timeZoneId == null ? undefined : 0), + timezone_id: x.timeZoneId + }) + } else if (neo4j.isTime(x) || neo4j.isLocalTime(x)) { + return structResponse('CypherTime', { + hour: x.hour, + minute: x.minute, + second: x.second, + nanosecond: x.nanosecond, + utc_offset_s: x.timeZoneOffsetSeconds + }) + } else if (neo4j.isDuration(x)) { + return structResponse('CypherDuration', { + months: x.months, + days: x.days, + seconds: x.seconds, + nanoseconds: x.nanoseconds + }) + } + // If all failed, interpret as a map const map = {} for (const [key, value] of Object.entries(x)) { @@ -116,26 +152,88 @@ export function nativeToCypher (x) { throw Error(err) } +function structResponse (name, data) { + const map = {} + for (const [key, value] of Object.entries(data)) { + map[key] = typeof value === 'bigint' || neo4j.isInt(value) + ? neo4j.int(value).toNumber() + : value + } + return { name, data: map } +} + export function cypherToNative (c) { const { name, - data: { value } + data } = c switch (name) { case 'CypherString': - return value + return data.value case 'CypherInt': - return BigInt(value) + return BigInt(data.value) case 'CypherFloat': - return value + return data.value case 'CypherNull': - return value + return data.value case 'CypherBool': - return value + return data.value case 'CypherList': - return value.map(cypherToNative) + return data.value.map(cypherToNative) + case 'CypherDateTime': + if (data.utc_offset_s == null && data.timezone_id == null) { + return new neo4j.LocalDateTime( + data.year, + data.month, + data.day, + data.hour, + data.minute, + data.second, + data.nanosecond + ) + } + return new neo4j.DateTime( + data.year, + data.month, + data.day, + data.hour, + data.minute, + data.second, + data.nanosecond, + data.utc_offset_s, + data.timezone_id + ) + case 'CypherTime': + if (data.utc_offset_s == null) { + return new neo4j.LocalTime( + data.hour, + data.minute, + data.second, + data.nanosecond + ) + } + return new neo4j.Time( + data.hour, + data.minute, + data.second, + data.nanosecond, + data.utc_offset_s + ) + case 'CypherDate': + return new neo4j.Date( + data.year, + data.month, + data.day + ) + case 'CypherDuration': + return new neo4j.Duration( + data.months, + data.days, + data.seconds, + data.nanoseconds + ) case 'CypherMap': - return Object.entries(value).reduce((acc, [key, val]) => { + return Object.entries(data.value).reduce((acc, [key, val]) => { acc[key] = cypherToNative(val) return acc }, {}) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 681df2ca4..8f5264bf4 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -331,6 +331,8 @@ export function GetFeatures (_context, _params, wire) { 'Feature:Auth:Bearer', 'Feature:API:SSLConfig', 'Feature:API:SSLSchemes', + 'Feature:API:Type.Temporal', + 'Feature:Bolt:Patch:UTC', 'AuthorizationExpiredTreatment', 'ConfHint:connection.recv_timeout_seconds', 'Feature:Impersonation', diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index c72fe2db2..9efe41d7e 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -1,6 +1,18 @@ import skip, { ifEquals, ifEndsWith } from './skip' const skippedTests = [ + skip( + 'Driver does not return offset for old DateTime implementations', + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_nested_datetime'), + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_should_echo_all_timezone_ids'), + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_cypher_created_datetime') + ), + skip( + 'Using numbers out of bound', + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_should_echo_temporal_type'), + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_nested_duration'), + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_duration_components') + ), skip( 'Fails when because tx function could start with not broken transations', ifEndsWith('test_should_write_successfully_on_leader_switch_using_tx_function'), From 894feb26382311681193f7a70948e2a3ecdc8b62 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 23 Jun 2022 17:37:05 +0200 Subject: [PATCH 2/3] Create test for packstream-v2 --- .../test/packstream/packstream-v2.test.js | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 packages/bolt-connection/test/packstream/packstream-v2.test.js diff --git a/packages/bolt-connection/test/packstream/packstream-v2.test.js b/packages/bolt-connection/test/packstream/packstream-v2.test.js new file mode 100644 index 000000000..340a2ae37 --- /dev/null +++ b/packages/bolt-connection/test/packstream/packstream-v2.test.js @@ -0,0 +1,195 @@ +/** + * 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, Unpacker } from '../../src/packstream/packstream-v2' +import { Structure } from '../../src/packstream/packstream-v1' + +describe('#unit PackStreamV2', () => { + 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) +} From ac559119e2ae1434dcaf4d8f2ebb29f3e9e8c0e5 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 23 Jun 2022 18:03:09 +0200 Subject: [PATCH 3/3] Test UTC in packstream --- .../__snapshots__/packstream-v2.test.js.snap | 9 + .../test/packstream/packstream-v2.test.js | 229 +++++++++++++++++- 2 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 packages/bolt-connection/test/packstream/__snapshots__/packstream-v2.test.js.snap diff --git a/packages/bolt-connection/test/packstream/__snapshots__/packstream-v2.test.js.snap b/packages/bolt-connection/test/packstream/__snapshots__/packstream-v2.test.js.snap new file mode 100644 index 000000000..6fe417f17 --- /dev/null +++ b/packages/bolt-connection/test/packstream/__snapshots__/packstream-v2.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#unit PackStreamV2 utc should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit PackStreamV2 utc should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit PackStreamV2 utc should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit PackStreamV2 utc should not unpack with wrong size (DateTimeWithZoneOffset with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 4"`; diff --git a/packages/bolt-connection/test/packstream/packstream-v2.test.js b/packages/bolt-connection/test/packstream/packstream-v2.test.js index 340a2ae37..0be8d8c78 100644 --- a/packages/bolt-connection/test/packstream/packstream-v2.test.js +++ b/packages/bolt-connection/test/packstream/packstream-v2.test.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { int, Integer } from 'neo4j-driver-core' +import { int, Integer, DateTime } from 'neo4j-driver-core' import { alloc } from '../../src/channel' import { Packer, Unpacker } from '../../src/packstream/packstream-v2' import { Structure } from '../../src/packstream/packstream-v1' @@ -182,14 +182,235 @@ describe('#unit PackStreamV2', () => { expect(unpacked[0]).toBe(list[0]) expect(unpacked[1]).toBe(list[1]) }) + + describe('utc', () => { + it.each([ + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ] + ])('should pack temporal types (%s)', (_, object) => { + + const unpacked = packAndUnpack(object, { disableLosslessIntegers: true, useUtc: true }) + + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const unpacked = packAndUnpack(object, { disableLosslessIntegers: true, useUtc: true}) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset with less fields', + new Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + expect(() => packAndUnpack(struct, { useUtc: true })).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new Structure(0x49, [ + int(1655212878), int(183_000_000), int(120 * 60) + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneId', + new Structure(0x69, [ + int(1655212878), int(183_000_000), 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new Structure(0x69, [ + int(1655212878), int(183_000_000), 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ] + ])('should unpack temporal types (%s)', (_, struct, object) => { + const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true, useUtc: true}) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new Structure(0x66, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true, useUtc: true}) + expect(unpacked).toEqual(struct) + }) + }) + + describe('non-utc', () => { + it.each([ + [ + 'DateTimeWithZoneOffset/0x49', + new Structure(0x49, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x69', + new Structure(0x69, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack utc temporal types as unknown structs (%s)', (_, struct) => { + const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true }) + expect(unpacked).toEqual(struct) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new Structure(0x46, [int(1), int(2), int(3)]), + new DateTime(1970, 1, 1, 0, 0, 1, 2, 3) + ], + [ + 'DateTimeWithZoneId', + new Structure(0x66, [int(1), int(2), 'America/Sao Paulo']), + new DateTime(1970, 1, 1, 0, 0, 1, 2, undefined, 'America/Sao Paulo') + ] + ])('should unpack temporal types without utc fix (%s)', (_, struct, object) => { + const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true }) + expect(unpacked).toEqual(object) + }) + + it.each([ + ['DateTimeWithZoneId', new DateTime(1, 1, 1, 1, 1, 1, 1, undefined, 'America/Sao Paulo')], + ['DateTime', new DateTime(1, 1, 1, 1, 1, 1, 1, 1)] + ])('should pack temporal types (no utc) (%s)', (_, object) => { + const unpacked = packAndUnpack(object, { disableLosslessIntegers: true }) + expect(unpacked).toEqual(object) + }) + }) }) function packAndUnpack ( val, - { bufferSize = 128, disableLosslessIntegers = false, useBigInt = false } = {} + { bufferSize = 128, disableLosslessIntegers = false, useBigInt = false, useUtc = false} = {} ) { const buffer = alloc(bufferSize) - new Packer(buffer).packable(val)() + const packer = new Packer(buffer) + packer.useUtc = useUtc + packer.packable(val)() buffer.reset() - return new Unpacker(disableLosslessIntegers, useBigInt).unpack(buffer) + const unpacker = new Unpacker(disableLosslessIntegers, useBigInt) + unpacker.useUtc = useUtc + return unpacker.unpack(buffer) }