From 6a436a541c24e9a54532b3689840e0d0467dc5a9 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 15 Jun 2022 16:45:43 +0200 Subject: [PATCH 01/16] Implementing new structures for DateTime in Bolt 5.0 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/bolt-protocol-v5x0.transformer.js | 9 +- .../bolt-protocol-v5x0.utc.transformer.js | 225 ++++++++++++++++++ .../test/bolt/bolt-protocol-v5x0.test.js | 86 ++++++- 3 files changed, 309 insertions(+), 11 deletions(-) create mode 100644 packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.transformer.js index 148958c08..97620408f 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.transformer.js @@ -18,8 +18,14 @@ */ import { structure } from '../packstream' -import { Node, Relationship, UnboundRelationship } from 'neo4j-driver-core' +import { + Node, + Relationship, + UnboundRelationship +} from 'neo4j-driver-core' + import v4x4 from './bolt-protocol-v4x4.transformer' +import v5x0Utc from './bolt-protocol-v5x0.utc.transformer' const NODE_STRUCT_SIZE = 4 const RELATIONSHIP_STRUCT_SIZE = 8 @@ -118,6 +124,7 @@ function createUnboundRelationshipTransformer (config) { export default { ...v4x4, + ...v5x0Utc, createNodeTransformer, createRelationshipTransformer, createUnboundRelationshipTransformer diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js new file mode 100644 index 000000000..65f3a8808 --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -0,0 +1,225 @@ +/** + * 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 { structure } from '../packstream' +import { + DateTime, + isInt, + int, + internal +} from 'neo4j-driver-core' + +import v4x4 from './bolt-protocol-v4x4.transformer' + +import { + epochSecondAndNanoToLocalDateTime +} from './temporal-factory' + +const { + temporalUtil: { + localDateTimeToEpochSecond + } +} = internal + +const DATE_TIME_WITH_ZONE_OFFSET = 0x49 +const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3 + +const DATE_TIME_WITH_ZONE_ID = 0x69 +const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3 + +function createDateTimeWithZoneIdTransformer (config) { + const { disableLosslessIntegers, useBigInt } = config + const dateTimeWithZoneIdTransformer = v4x4.createDateTimeWithZoneIdTransformer(config) + return dateTimeWithZoneIdTransformer.extendsWith({ + signature: DATE_TIME_WITH_ZONE_ID, + fromStructure: struct => { + structure.verifyStructSize( + 'DateTimeWithZoneId', + DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE, + struct.size + ) + + const [epochSecond, nano, timeZoneId] = struct.fields + + const localDateTime = getTimeInZoneId(timeZoneId, epochSecond, nano) + + const result = new DateTime( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + int(nano), + null, + timeZoneId + ) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + }, + toStructure: value => { + const epochSecond = localDateTimeToEpochSecond( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.nanosecond + ) + + const dateTimeWithZoneAppliedTwice = getTimeInZoneId(value.timeZoneId, epochSecond, value.nanosecond) + + // The wallclock form the current date time + const epochWithZoneAppliedTwice = localDateTimeToEpochSecond( + dateTimeWithZoneAppliedTwice.year, + dateTimeWithZoneAppliedTwice.month, + dateTimeWithZoneAppliedTwice.day, + dateTimeWithZoneAppliedTwice.hour, + dateTimeWithZoneAppliedTwice.minute, + dateTimeWithZoneAppliedTwice.second, + value.nanosecond) + + const offsetOfZoneInTheFutureUtc = epochSecond.subtract(epochWithZoneAppliedTwice) + const guessedUtc = epochSecond.add(offsetOfZoneInTheFutureUtc) + + const zonedDateTimeFromGuessedUtc = getTimeInZoneId(value.timeZoneId, guessedUtc, value.nanosecond) + + const zonedEpochFromGuessedUtc = localDateTimeToEpochSecond( + zonedDateTimeFromGuessedUtc.year, + zonedDateTimeFromGuessedUtc.month, + zonedDateTimeFromGuessedUtc.day, + zonedDateTimeFromGuessedUtc.hour, + zonedDateTimeFromGuessedUtc.minute, + zonedDateTimeFromGuessedUtc.second, + value.nanosecond) + + const offset = zonedEpochFromGuessedUtc.subtract(guessedUtc) + const utc = epochSecond.subtract(offset) + + const nano = int(value.nanosecond) + const timeZoneId = value.timeZoneId + + return new structure.Structure(DATE_TIME_WITH_ZONE_ID, [utc, nano, timeZoneId]) + } + }) +} + +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', + hourCycle: 'h23' + }) + + const l = epochSecondAndNanoToLocalDateTime(epochSecond, nano) + + const formattedUtc = formatter.formatToParts(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 localDateTime = formattedUtc.reduce((obj, currentValue) => { + if (currentValue.type !== 'literal') { + obj[currentValue.type] = int(currentValue.value) + return obj + } + return obj + }, {}) + return localDateTime +} + +function createDateTimeWithOffsetTransformer (config) { + const { disableLosslessIntegers, useBigInt } = config + const dateTimeWithOffsetTransformer = v4x4.createDateTimeWithOffsetTransformer(config) + return dateTimeWithOffsetTransformer.extendsWith({ + signature: DATE_TIME_WITH_ZONE_OFFSET, + toStructure: value => { + 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) + return new structure.Structure(DATE_TIME_WITH_ZONE_OFFSET, [utcSecond, nano, timeZoneOffsetSeconds]) + }, + fromStructure: struct => { + structure.verifyStructSize( + 'DateTimeWithZoneOffset', + DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE, + struct.size + ) + + const [utcSecond, nano, timeZoneOffsetSeconds] = struct.fields + + 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) + } + }) +} + +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 +} + +export default { + createDateTimeWithZoneIdTransformer, + createDateTimeWithOffsetTransformer +} diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index a8e22faf3..182f4c208 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -407,8 +407,34 @@ describe('#unit BoltProtocolV5x0', () => { ['Time', new Time(1, 1, 1, 1, 1)], ['Date', new Date(1, 1, 1)], ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], - ['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)], + [ + '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 after turn CEST', + new DateTime(2022, 3, 27, 3, 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 after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], ['Point2D', new Point(1, 1, 1)], ['Point3D', new Point(1, 1, 1, 1)] ])('should pack spatial types and temporal types (%s)', (_, object) => { @@ -621,19 +647,19 @@ describe('#unit BoltProtocolV5x0', () => { ], [ 'DateTimeWithZoneOffset with less fields', - new structure.Structure(0x46, [1, 2]) + new structure.Structure(0x49, [1, 2]) ], [ 'DateTimeWithZoneOffset with more fields', - new structure.Structure(0x46, [1, 2, 3, 4]) + new structure.Structure(0x49, [1, 2, 3, 4]) ], [ 'DateTimeWithZoneId with less fields', - new structure.Structure(0x66, [1, 2]) + new structure.Structure(0x69, [1, 2]) ], [ 'DateTimeWithZoneId with more fields', - new structure.Structure(0x66, [1, 2, 'America/Sao Paulo', 'Brasil']) + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) ] ])('should not unpack with wrong size (%s)', (_, struct) => { const buffer = alloc(256) @@ -690,13 +716,24 @@ describe('#unit BoltProtocolV5x0', () => { ], [ 'DateTimeWithZoneOffset', - new structure.Structure(0x46, [1, 2, 3]), - new DateTime(1970, 1, 1, 0, 0, 1, 2, 3) + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) ], [ 'DateTimeWithZoneId', - new structure.Structure(0x66, [1, 2, 'America/Sao Paulo']), - new DateTime(1970, 1, 1, 0, 0, 1, 2, undefined, 'America/Sao Paulo') + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, undefined, 'Australia/Eucla') ] ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { const buffer = alloc(256) @@ -717,5 +754,34 @@ describe('#unit BoltProtocolV5x0', () => { const unpacked = protocol.unpack(buffer) expect(unpacked).toEqual(object) }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) }) }) From f9d55321fd6e4bc4975ba94cdcd7c0685a0a8bb9 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 15 Jun 2022 17:49:50 +0200 Subject: [PATCH 02/16] Add CypherDateTime binder to testkit --- .../src/cypher-native-binders.js | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index a385b5d04..7b754898c 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -126,23 +126,35 @@ function valueResponseOfObject (x) { 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': + return new neo4j.DateTime( + data.year, + data.month, + data.day, + data.hour, + data.minute, + data.second, + data.nanosecond, + data.timezone_id == null ? data.utc_offset_s : null, + data.timezone_id + ) 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 }, {}) From 28486357ec6a43f014058b39f1001b5d8a02f281 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 15 Jun 2022 18:28:58 +0200 Subject: [PATCH 03/16] Docs and add more test cases --- .../bolt-protocol-v5x0.utc.transformer.js | 84 +++++++++++++------ .../test/bolt/bolt-protocol-v5x0.test.js | 48 +++++++++++ 2 files changed, 105 insertions(+), 27 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js index 65f3a8808..a97733ac7 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -83,33 +83,7 @@ function createDateTimeWithZoneIdTransformer (config) { value.nanosecond ) - const dateTimeWithZoneAppliedTwice = getTimeInZoneId(value.timeZoneId, epochSecond, value.nanosecond) - - // The wallclock form the current date time - const epochWithZoneAppliedTwice = localDateTimeToEpochSecond( - dateTimeWithZoneAppliedTwice.year, - dateTimeWithZoneAppliedTwice.month, - dateTimeWithZoneAppliedTwice.day, - dateTimeWithZoneAppliedTwice.hour, - dateTimeWithZoneAppliedTwice.minute, - dateTimeWithZoneAppliedTwice.second, - value.nanosecond) - - const offsetOfZoneInTheFutureUtc = epochSecond.subtract(epochWithZoneAppliedTwice) - const guessedUtc = epochSecond.add(offsetOfZoneInTheFutureUtc) - - const zonedDateTimeFromGuessedUtc = getTimeInZoneId(value.timeZoneId, guessedUtc, value.nanosecond) - - const zonedEpochFromGuessedUtc = localDateTimeToEpochSecond( - zonedDateTimeFromGuessedUtc.year, - zonedDateTimeFromGuessedUtc.month, - zonedDateTimeFromGuessedUtc.day, - zonedDateTimeFromGuessedUtc.hour, - zonedDateTimeFromGuessedUtc.minute, - zonedDateTimeFromGuessedUtc.second, - value.nanosecond) - - const offset = zonedEpochFromGuessedUtc.subtract(guessedUtc) + const offset = getOffsetFromZoneId(value.timeZoneId, epochSecond, value.nanosecond) const utc = epochSecond.subtract(offset) const nano = int(value.nanosecond) @@ -120,6 +94,62 @@ function createDateTimeWithZoneIdTransformer (config) { }) } +/** + * 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, diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index 182f4c208..3e16966e1 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -419,18 +419,66 @@ describe('#unit BoltProtocolV5x0', () => { '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') + ], [ 'DateTimeWithZoneOffset', new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) From 6d9798566b9c4d14c2c1e59fbc04019ac8a09e6c Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 16 Jun 2022 12:16:06 +0200 Subject: [PATCH 04/16] Enable DateTime to be initialized with Offset and ZoneId --- packages/core/src/temporal-types.ts | 44 ++++++++++++------- packages/core/test/temporal-types.test.ts | 36 ++++++++++++++- .../src/cypher-native-binders.js | 2 +- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/core/src/temporal-types.ts b/packages/core/src/temporal-types.ts index 60a5f6ddd..26cc0179e 100644 --- a/packages/core/src/temporal-types.ts +++ b/packages/core/src/temporal-types.ts @@ -670,7 +670,13 @@ export class DateTime { if (this.timeZoneOffsetSeconds === undefined) { throw new Error('Requires DateTime created with time zone offset') } - return util.isoStringToStandardDate(this.toString()) + return util.isoStringToStandardDate( + // the timezone name should be removed from the + // string, otherwise the javascript parse doesn't + // read the datetime correctly + this.toString().replace( + this.timeZoneId != null ? `[${this.timeZoneId}]` : '', '') + ) } /** @@ -686,10 +692,16 @@ export class DateTime { this.second, this.nanosecond ) + + const timeOffset = this.timeZoneOffsetSeconds != null + ? util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds ?? 0) + : '' + const timeZoneStr = this.timeZoneId != null ? `[${this.timeZoneId}]` - : util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds ?? 0) - return localDateTimeStr + timeZoneStr + : '' + + return localDateTimeStr + timeOffset + timeZoneStr } } @@ -741,23 +753,25 @@ function verifyTimeZoneArguments ( const offsetDefined = timeZoneOffsetSeconds !== null && timeZoneOffsetSeconds !== undefined const idDefined = timeZoneId !== null && timeZoneId !== undefined && 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( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `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/core/test/temporal-types.test.ts b/packages/core/test/temporal-types.test.ts index 482fbdc5f..10de7564c 100644 --- a/packages/core/test/temporal-types.test.ts +++ b/packages/core/test/temporal-types.test.ts @@ -74,7 +74,41 @@ describe('LocalDateTime', () => { }) describe('DateTime', () => { + describe('constructor', () => { + it('should be able to create a date with zone id and offset', () => { + const datetime = new DateTime(2022, 6, 16, 11, 19, 25, 400004, 2 * 60 * 60, 'Europe/Stockholm') + + expect(datetime.year).toEqual(2022) + expect(datetime.month).toEqual(6) + expect(datetime.day).toEqual(16) + expect(datetime.hour).toEqual(11) + expect(datetime.minute).toEqual(19) + expect(datetime.second).toEqual(25) + expect(datetime.nanosecond).toEqual(400004) + expect(datetime.timeZoneOffsetSeconds).toEqual(2 * 60 * 60) + expect(datetime.timeZoneId).toEqual('Europe/Stockholm') + }) + }) + describe('.toStandardDate()', () => { + it('should convert to a standard date (offset + zone id)', () => { + const datetime = new DateTime(2022, 6, 16, 11, 19, 25, 4000004, 2 * 60 * 60, 'Europe/Stockholm') + + const standardDate = datetime.toStandardDate() + + expect(standardDate.getFullYear()).toEqual(datetime.year) + expect(standardDate.getMonth()).toEqual(datetime.month - 1) + expect(standardDate.getDate()).toEqual(datetime.day) + const offsetInMinutes = offset(standardDate) + const offsetAdjust = offsetInMinutes - (datetime.timeZoneOffsetSeconds ?? 0) / 60 + const hourDiff = Math.abs(offsetAdjust / 60) + const minuteDiff = Math.abs(offsetAdjust % 60) + expect(standardDate.getHours()).toBe(datetime.hour - hourDiff) + expect(standardDate.getMinutes()).toBe(datetime.minute - minuteDiff) + expect(standardDate.getSeconds()).toBe(datetime.second) + expect(standardDate.getMilliseconds()).toBe(Math.round(datetime.nanosecond / 1000000)) + }) + it('should convert to a standard date (offset)', () => { const datetime = new DateTime(2020, 12, 15, 12, 2, 3, 4000000, 120 * 60) @@ -90,7 +124,7 @@ describe('DateTime', () => { expect(standardDate.getHours()).toBe(datetime.hour - hourDiff) expect(standardDate.getMinutes()).toBe(datetime.minute - minuteDiff) expect(standardDate.getSeconds()).toBe(datetime.second) - expect(standardDate.getMilliseconds()).toBe(datetime.nanosecond / 1000000) + expect(standardDate.getMilliseconds()).toBe(Math.round(datetime.nanosecond / 1000000)) }) it('should not convert to a standard date (zoneid)', () => { diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index 7b754898c..eb8ed4395 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -150,7 +150,7 @@ export function cypherToNative (c) { data.minute, data.second, data.nanosecond, - data.timezone_id == null ? data.utc_offset_s : null, + data.utc_offset_s, data.timezone_id ) case 'CypherMap': From a5b9d3d4b28717a3c8a76f6b2de7a2434107da64 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 16 Jun 2022 13:54:52 +0200 Subject: [PATCH 05/16] Fill DateTimeWithZoneId Offset when unpack and use timeZoneOffsetSeconds instead of calculate --- .../bolt-protocol-v5x0.utc.transformer.js | 39 ++++++++--- .../test/bolt/bolt-protocol-v5x0.test.js | 67 ++++++++++++++++--- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js index a97733ac7..c1a760529 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -67,7 +67,7 @@ function createDateTimeWithZoneIdTransformer (config) { localDateTime.minute, localDateTime.second, int(nano), - null, + localDateTime.timeZoneOffsetSeconds, timeZoneId ) return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) @@ -83,7 +83,9 @@ function createDateTimeWithZoneIdTransformer (config) { value.nanosecond ) - const offset = getOffsetFromZoneId(value.timeZoneId, epochSecond, 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) @@ -159,27 +161,46 @@ function getTimeInZoneId (timeZoneId, epochSecond, nano) { hour: 'numeric', minute: 'numeric', second: 'numeric', - hourCycle: 'h23' + hourCycle: 'h23', + timeZoneName: 'short' }) const l = epochSecondAndNanoToLocalDateTime(epochSecond, nano) - - const formattedUtc = formatter.formatToParts(Date.UTC( + 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 === 'timeZoneName') { + const parts = currentValue.value.replace('GMT', '').split(':') + const divisor = 60 + const { offset } = parts.reduce((state, value) => { + const part = int(value) + const signal = part.isPositive() ? int(1) : int(-1) - const localDateTime = formattedUtc.reduce((obj, currentValue) => { - if (currentValue.type !== 'literal') { + const offset = part.multiply(state.factor).add(state.offset) + const factor = state.factor.div(divisor).multiply(signal) + + return { + offset, + factor + } + }, { factor: int(60 * 60), offset: int(0) }) + + obj.timeZoneOffsetSeconds = offset + } else if (currentValue.type !== 'literal') { obj[currentValue.type] = int(currentValue.value) - return obj } return obj }, {}) + return localDateTime } diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index 3e16966e1..fba6eba5f 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -407,6 +407,42 @@ describe('#unit BoltProtocolV5x0', () => { ['Time', new Time(1, 1, 1, 1, 1)], ['Date', new Date(1, 1, 1)], ['LocalDateTime', new LocalDateTime(1, 1, 1, 1, 1, 1, 1)], + [ + 'DateTimeWithZoneOffset', + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CET', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Berlin 2:30 CEST', + new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 1 * 60 * 60, 'Europe/Berlin') + ], + ['Point2D', new Point(1, 1, 1)], + ['Point3D', new Point(1, 1, 1, 1)] + ])('should pack spatial types and temporal types (%s)', (_, object) => { + const buffer = alloc(256) + const protocol = new BoltProtocolV5x0( + new utils.MessageRecordingConnection(), + buffer, + { + disableLosslessIntegers: true + } + ) + + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ [ 'DateTimeWithZoneId / Australia', new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') @@ -478,14 +514,8 @@ describe('#unit BoltProtocolV5x0', () => { [ 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') - ], - [ - 'DateTimeWithZoneOffset', - new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) - ], - ['Point2D', new Point(1, 1, 1)], - ['Point3D', new Point(1, 1, 1, 1)] - ])('should pack spatial types and temporal types (%s)', (_, object) => { + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { const buffer = alloc(256) const protocol = new BoltProtocolV5x0( new utils.MessageRecordingConnection(), @@ -502,7 +532,22 @@ describe('#unit BoltProtocolV5x0', () => { buffer.reset() const unpacked = protocol.unpack(buffer) - expect(unpacked).toEqual(object) + + 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) }) }) @@ -774,14 +819,14 @@ describe('#unit BoltProtocolV5x0', () => { new structure.Structure(0x69, [ 1655212878, 183_000_000, 'Europe/Berlin' ]), - new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') ], [ 'DateTimeWithZoneId / Australia', new structure.Structure(0x69, [ 1655212878, 183_000_000, 'Australia/Eucla' ]), - new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, undefined, 'Australia/Eucla') + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') ] ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { const buffer = alloc(256) From b0a2e9bd6a490390e623e08e173deeca1d324871 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 16 Jun 2022 14:00:26 +0200 Subject: [PATCH 06/16] Improve testkit logging --- packages/testkit-backend/src/channel/testkit-protocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/channel/testkit-protocol.js b/packages/testkit-backend/src/channel/testkit-protocol.js index f6282c4c7..48fbb3808 100644 --- a/packages/testkit-backend/src/channel/testkit-protocol.js +++ b/packages/testkit-backend/src/channel/testkit-protocol.js @@ -69,7 +69,7 @@ export default class Protocol extends EventEmitter { _emitRequest () { const request = JSON.parse(this._request) const { name, data } = request - console.log('> Got request ' + name, data) + console.log('> Got request ' + name, JSON.stringify(data, undefined, 2)) this.emit('request', { name, data }) } } From 479b02b48a2bde1556ab1e3ff6248ce5baff7113 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 16 Jun 2022 14:19:34 +0200 Subject: [PATCH 07/16] Warning the user about the usage of timezone without offset --- .../bolt-connection/src/bolt/bolt-protocol-v1.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v2.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v3.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v4x0.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v4x1.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v4x2.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v4x3.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v4x4.js | 2 +- .../bolt-connection/src/bolt/bolt-protocol-v5x0.js | 2 +- .../src/bolt/bolt-protocol-v5x0.utc.transformer.js | 8 +++++++- .../test/bolt/bolt-protocol-v5x0.test.js | 14 ++++++++++++-- 11 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index ccb9704e9..f3a7b32a0 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -87,7 +87,7 @@ export default class BoltProtocol { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js index 4abec0941..399457cf2 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v2.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v2.js @@ -37,7 +37,7 @@ export default class BoltProtocol extends BoltProtocolV1 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v3.js index 32f0d664b..25dd7ad5a 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v3.js @@ -48,7 +48,7 @@ export default class BoltProtocol extends BoltProtocolV2 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js index 102034d51..2dfc1f541 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x0.js @@ -46,7 +46,7 @@ export default class BoltProtocol extends BoltProtocolV3 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js index 7ae475765..f8a1de208 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x1.js @@ -67,7 +67,7 @@ export default class BoltProtocol extends BoltProtocolV4 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x2.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x2.js index 07378b53a..96e607164 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x2.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x2.js @@ -34,7 +34,7 @@ export default class BoltProtocol extends BoltProtocolV41 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js index 0897c6826..e6ea25cb6 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js @@ -37,7 +37,7 @@ export default class BoltProtocol extends BoltProtocolV42 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js index 6c6e6af6d..3b4b55b8c 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js @@ -37,7 +37,7 @@ export default class BoltProtocol extends BoltProtocolV43 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js index f60c43d89..df93b786c 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js @@ -34,7 +34,7 @@ export default class BoltProtocol extends BoltProtocolV44 { get transformer () { if (this._transformer === undefined) { - this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config))) + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) } return this._transformer } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js index c1a760529..2ccbb50ff 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -43,7 +43,7 @@ const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3 const DATE_TIME_WITH_ZONE_ID = 0x69 const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3 -function createDateTimeWithZoneIdTransformer (config) { +function createDateTimeWithZoneIdTransformer (config, logger) { const { disableLosslessIntegers, useBigInt } = config const dateTimeWithZoneIdTransformer = v4x4.createDateTimeWithZoneIdTransformer(config) return dateTimeWithZoneIdTransformer.extendsWith({ @@ -86,6 +86,12 @@ function createDateTimeWithZoneIdTransformer (config) { const offset = value.timeZoneOffsetSeconds != null ? value.timeZoneOffsetSeconds : getOffsetFromZoneId(value.timeZoneId, epochSecond, value.nanosecond) + + if (value.timeZoneOffsetSeconds == null) { + logger.warn('DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + } const utc = epochSecond.subtract(offset) const nano = int(value.nanosecond) diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index fba6eba5f..b133e2457 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -44,7 +44,8 @@ const WRITE = 'WRITE' const { txConfig: { TxConfig }, - bookmarks: { Bookmarks } + bookmarks: { Bookmarks }, + logger: { Logger } } = internal describe('#unit BoltProtocolV5x0', () => { @@ -517,12 +518,15 @@ describe('#unit BoltProtocolV5x0', () => { ] ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { const buffer = alloc(256) + const loggerFunction = jest.fn() const protocol = new BoltProtocolV5x0( new utils.MessageRecordingConnection(), buffer, { disableLosslessIntegers: true - } + }, + undefined, + new Logger('debug', loggerFunction) ) const packable = protocol.packable(object) @@ -547,6 +551,12 @@ describe('#unit BoltProtocolV5x0', () => { unpacked.timeZoneId ) + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + expect(unpackedDateTimeWithoutOffset).toEqual(object) }) }) From ddb98cb09713a91be57c7d152ca7a880a5a8722a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 16 Jun 2022 16:03:24 +0200 Subject: [PATCH 08/16] Patching 4.3 and 4.4 with the UTC fix --- .../src/bolt/bolt-protocol-v4x3.js | 50 ++- .../src/bolt/bolt-protocol-v4x4.js | 8 + .../src/bolt/bolt-protocol-v5x0.js | 27 ++ .../src/bolt/request-message.js | 5 +- .../bolt-protocol-v4x3.test.js.snap | 8 + .../bolt-protocol-v4x4.test.js.snap | 8 + .../test/bolt/bolt-protocol-v4x3.test.js | 329 +++++++++++++++++- .../test/bolt/bolt-protocol-v4x4.test.js | 329 +++++++++++++++++- packages/bolt-connection/test/test-utils.js | 4 + .../testkit-backend/src/feature/common.js | 2 + 10 files changed, 764 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js index e6ea25cb6..3ca55e4f4 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x3.js @@ -18,9 +18,10 @@ */ import BoltProtocolV42 from './bolt-protocol-v4x2' import RequestMessage from './request-message' -import { RouteObserver } from './stream-observers' +import { LoginObserver, RouteObserver } from './stream-observers' import transformersFactories from './bolt-protocol-v4x3.transformer' +import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' import Transformer from './transformer' import { internal } from 'neo4j-driver-core' @@ -75,4 +76,51 @@ 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._transformer = new Transformer(Object.values({ + ...transformersFactories, + ...utcTransformersFactories + }).map(create => create(this._config, this._log))) + } } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js index 3b4b55b8c..bd2c94228 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v4x4.js @@ -23,6 +23,7 @@ import RequestMessage from './request-message' import { RouteObserver, ResultStreamObserver } from './stream-observers' import transformersFactories from './bolt-protocol-v4x4.transformer' +import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer' import Transformer from './transformer' const { @@ -163,4 +164,11 @@ export default class BoltProtocol extends BoltProtocolV43 { return observer } + + _applyUtcPatch () { + this._transformer = new Transformer(Object.values({ + ...transformersFactories, + ...utcTransformersFactories + }).map(create => create(this._config, this._log))) + } } diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js index df93b786c..8cfbefa2b 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js @@ -20,6 +20,8 @@ import BoltProtocolV44 from './bolt-protocol-v4x4' import transformersFactories from './bolt-protocol-v5x0.transformer' import Transformer from './transformer' +import RequestMessage from './request-message' +import { LoginObserver } from './stream-observers' import { internal } from 'neo4j-driver-core' @@ -38,4 +40,29 @@ export default class BoltProtocol extends BoltProtocolV44 { } return this._transformer } + + /** + * 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 => this._onLoginCompleted(metadata, onComplete) + }) + + this.write( + RequestMessage.hello(userAgent, authToken, this._serversideRouting), + observer, + true + ) + + return observer + } } diff --git a/packages/bolt-connection/src/bolt/request-message.js b/packages/bolt-connection/src/bolt/request-message.js index 5183b3cae..4a525daa7 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/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap index f1c9a01ed..405f0bd6b 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x3.test.js.snap @@ -59,3 +59,11 @@ exports[`#unit BoltProtocolV4x3 .unpack() should not unpack with wrong size (Tim exports[`#unit BoltProtocolV4x3 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 3 but was 2"`; exports[`#unit BoltProtocolV4x3 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV4x3 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV4x3 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV4x3 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV4x3 utc patch the server accepted the patch 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/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap index 4068caf7d..74085de44 100644 --- a/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap +++ b/packages/bolt-connection/test/bolt/__snapshots__/bolt-protocol-v4x4.test.js.snap @@ -59,3 +59,11 @@ exports[`#unit BoltProtocolV4x4 .unpack() should not unpack with wrong size (Tim exports[`#unit BoltProtocolV4x4 .unpack() should not unpack with wrong size (UnboundRelationship with less fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 3 but was 2"`; exports[`#unit BoltProtocolV4x4 .unpack() should not unpack with wrong size (UnboundRelationship with more fields) 1`] = `"Wrong struct size for UnboundRelationship, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV4x4 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneId with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV4x4 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneId with more fields) 1`] = `"Wrong struct size for DateTimeWithZoneId, expected 3 but was 4"`; + +exports[`#unit BoltProtocolV4x4 utc patch the server accepted the patch should not unpack with wrong size (DateTimeWithZoneOffset with less fields) 1`] = `"Wrong struct size for DateTimeWithZoneOffset, expected 3 but was 2"`; + +exports[`#unit BoltProtocolV4x4 utc patch the server accepted the patch 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/bolt/bolt-protocol-v4x3.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js index 84982df25..34bc28f27 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x3.test.js @@ -44,7 +44,8 @@ const WRITE = 'WRITE' const { txConfig: { TxConfig }, - bookmarks: { Bookmarks } + bookmarks: { Bookmarks }, + logger: { Logger } } = internal describe('#unit BoltProtocolV4x3', () => { @@ -196,7 +197,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]) @@ -673,4 +674,328 @@ describe('#unit BoltProtocolV4x3', () => { expect(unpacked).toEqual(object) }) }) + + 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.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 packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + expect(() => protocol.unpack(buffer)).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ] + ])('should unpack temporal types (%s)', (_, struct, object) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + describe('the server did not accept th 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.each([ + [ + 'DateTimeWithZoneOffset/0x49', + new structure.Structure(0x49, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x69', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack utc temporal types as unknown structs (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x46, [1, 2, 3]), + new DateTime(1970, 1, 1, 0, 0, 1, 2, 3) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x66, [1, 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 packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + 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 packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + }) + }) }) 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 80019513f..a0a046ace 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v4x4.test.js @@ -44,7 +44,8 @@ const WRITE = 'WRITE' const { txConfig: { TxConfig }, - bookmarks: { Bookmarks } + bookmarks: { Bookmarks }, + logger: { Logger } } = internal describe('#unit BoltProtocolV4x4', () => { @@ -270,7 +271,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]) @@ -706,4 +707,328 @@ describe('#unit BoltProtocolV4x4', () => { expect(unpacked).toEqual(object) }) }) + + 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.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 packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneId / Australia', + new DateTime(2022, 6, 15, 15, 21, 18, 183_000_000, undefined, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId', + new DateTime(2022, 6, 22, 15, 21, 18, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CEST', + new DateTime(2022, 3, 27, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CEST', + new DateTime(2022, 3, 27, 0, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CEST', + new DateTime(2022, 3, 27, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CEST', + new DateTime(2022, 3, 27, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just before turn CET', + new DateTime(2022, 10, 30, 2, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 before turn CET', + new DateTime(2022, 10, 30, 1, 59, 59, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just after turn CET', + new DateTime(2022, 10, 30, 3, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Europe just 1 after turn CET', + new DateTime(2022, 10, 30, 4, 0, 0, 183_000_000, undefined, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn summer time', + new DateTime(2018, 11, 4, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn summer time', + new DateTime(2018, 11, 4, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn summer time', + new DateTime(2018, 11, 5, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn summer time', + new DateTime(2018, 11, 5, 2, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just before turn winter time', + new DateTime(2019, 2, 17, 11, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 before turn winter time', + new DateTime(2019, 2, 17, 10, 59, 59, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just after turn winter time', + new DateTime(2019, 2, 18, 0, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', + new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ] + ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { + const packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + + expect(unpacked.timeZoneOffsetSeconds).toBeDefined() + + const unpackedDateTimeWithoutOffset = new DateTime( + unpacked.year, + unpacked.month, + unpacked.day, + unpacked.hour, + unpacked.minute, + unpacked.second, + unpacked.nanosecond, + undefined, + unpacked.timeZoneId + ) + + expect(loggerFunction) + .toBeCalledWith('warn', + 'DateTime objects without "timeZoneOffsetSeconds" property ' + + 'are prune to bugs related to ambiguous times. For instance, ' + + '2022-10-30T2:30:00[Europe/Berlin] could be GMT+1 or GMT+2.') + + expect(unpackedDateTimeWithoutOffset).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset with less fields', + new structure.Structure(0x49, [1, 2]) + ], + [ + 'DateTimeWithZoneOffset with more fields', + new structure.Structure(0x49, [1, 2, 3, 4]) + ], + [ + 'DateTimeWithZoneId with less fields', + new structure.Structure(0x69, [1, 2]) + ], + [ + 'DateTimeWithZoneId with more fields', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil']) + ] + ])('should not unpack with wrong size (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + expect(() => protocol.unpack(buffer)).toThrowErrorMatchingSnapshot() + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x49, [ + 1655212878, 183_000_000, 120 * 60 + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Europe/Berlin' + ]), + new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') + ], + [ + 'DateTimeWithZoneId / Australia', + new structure.Structure(0x69, [ + 1655212878, 183_000_000, 'Australia/Eucla' + ]), + new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ] + ])('should unpack temporal types (%s)', (_, struct, object) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset/0x46', + new structure.Structure(0x46, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x66', + new structure.Structure(0x66, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + }) + + describe('the server did not accept th 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.each([ + [ + 'DateTimeWithZoneOffset/0x49', + new structure.Structure(0x49, [1, 2, 3]) + ], + [ + 'DateTimeWithZoneId/0x69', + new structure.Structure(0x69, [1, 2, 'America/Sao Paulo']) + ] + ])('should unpack utc temporal types as unknown structs (%s)', (_, struct) => { + const packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(struct) + }) + + it.each([ + [ + 'DateTimeWithZoneOffset', + new structure.Structure(0x46, [1, 2, 3]), + new DateTime(1970, 1, 1, 0, 0, 1, 2, 3) + ], + [ + 'DateTimeWithZoneId', + new structure.Structure(0x66, [1, 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 packable = protocol.packable(struct) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + 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 packable = protocol.packable(object) + + expect(packable).not.toThrow() + + buffer.reset() + + const unpacked = protocol.unpack(buffer) + expect(unpacked).toEqual(object) + }) + }) + }) }) 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/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 2dc79ad92..26d7e4e12 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -19,6 +19,7 @@ const features = [ 'Feature:Auth:Bearer', 'Feature:API:SSLConfig', 'Feature:API:SSLSchemes', + 'Feature:API:Type.Temporal', 'AuthorizationExpiredTreatment', 'ConfHint:connection.recv_timeout_seconds', 'Feature:Impersonation', @@ -28,6 +29,7 @@ const features = [ 'Feature:Bolt:4.3', 'Feature:Bolt:4.4', 'Feature:Bolt:5.0', + 'Feature:Bolt:Patch:UTC', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', From c6065affc1a95d33257b1c12c565f2be32b69e8e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 17 Jun 2022 11:12:04 +0200 Subject: [PATCH 09/16] Fix DateTimeFormat configuration. hourCycle: 'h23' doesn't work in older Node versions --- .../src/bolt/bolt-protocol-v5x0.utc.transformer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js index 2ccbb50ff..fbdaf8d29 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -167,7 +167,7 @@ function getTimeInZoneId (timeZoneId, epochSecond, nano) { hour: 'numeric', minute: 'numeric', second: 'numeric', - hourCycle: 'h23', + hour12: false, timeZoneName: 'short' }) @@ -207,6 +207,8 @@ function getTimeInZoneId (timeZoneId, epochSecond, nano) { return obj }, {}) + localDateTime.hour = localDateTime.hour.modulo(24) + return localDateTime } From 1fe1d43ab3cc6e78ce2930c308d50f86356187d1 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Jun 2022 15:32:01 +0200 Subject: [PATCH 10/16] Add more temporal types to the testkit-backend --- .../src/cypher-native-binders.js | 86 +++++++++++++++++++ .../src/skipped-tests/common.js | 4 + 2 files changed, 90 insertions(+) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index eb8ed4395..84105c91a 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -115,6 +115,42 @@ function valueResponseOfObject (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, + 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)) { @@ -123,6 +159,16 @@ function valueResponseOfObject (x) { return valueResponse('CypherMap', map) } +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, @@ -142,6 +188,17 @@ export function cypherToNative (c) { case 'CypherList': 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, @@ -153,6 +210,35 @@ export function cypherToNative (c) { 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(data.value).reduce((acc, [key, val]) => { acc[key] = cypherToNative(val) diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index 385218ba5..f1bb8ab78 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -1,6 +1,10 @@ import skip, { ifEquals, ifEndsWith } from './skip' const skippedTests = [ + skip( + 'Using numbers out of bound', + ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_should_echo_temporal_type') + ), skip( 'Testkit implemenation is deprecated', ifEquals('stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_node_only_element_id'), From 0db6add251bbd2c3aba147d421ade96df716b536 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Jun 2022 12:39:26 +0200 Subject: [PATCH 11/16] Fix CypherDateTime implementation --- packages/testkit-backend/src/cypher-native-binders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index 84105c91a..7650deee7 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -131,7 +131,7 @@ function valueResponseOfObject (x) { minute: x.minute, second: x.second, nanosecond: x.nanosecond, - utc_offset_s: x.timeZoneOffsetSeconds, + utc_offset_s: x.timeZoneOffsetSeconds || 0, timezone_id: x.timeZoneId }) } else if (neo4j.isTime(x) || neo4j.isLocalTime(x)) { From 04aba6ecc1fc8baf80154ad1fdb317e3c8e1a671 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Jun 2022 14:28:06 +0200 Subject: [PATCH 12/16] Fix timezone offset calculation --- .../bolt-protocol-v5x0.utc.transformer.js | 33 ++++++++----------- .../test/bolt/bolt-protocol-v5x0.test.js | 26 +++++++++++++++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js index fbdaf8d29..94c0a8b1d 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -167,8 +167,7 @@ function getTimeInZoneId (timeZoneId, epochSecond, nano) { hour: 'numeric', minute: 'numeric', second: 'numeric', - hour12: false, - timeZoneName: 'short' + hour12: false }) const l = epochSecondAndNanoToLocalDateTime(epochSecond, nano) @@ -184,29 +183,23 @@ function getTimeInZoneId (timeZoneId, epochSecond, nano) { const formattedUtcParts = formatter.formatToParts(utc) const localDateTime = formattedUtcParts.reduce((obj, currentValue) => { - if (currentValue.type === 'timeZoneName') { - const parts = currentValue.value.replace('GMT', '').split(':') - const divisor = 60 - const { offset } = parts.reduce((state, value) => { - const part = int(value) - const signal = part.isPositive() ? int(1) : int(-1) - - const offset = part.multiply(state.factor).add(state.offset) - const factor = state.factor.div(divisor).multiply(signal) - - return { - offset, - factor - } - }, { factor: int(60 * 60), offset: int(0) }) - - obj.timeZoneOffsetSeconds = offset - } else if (currentValue.type !== 'literal') { + 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 diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js index b133e2457..223cac107 100644 --- a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -412,6 +412,10 @@ describe('#unit BoltProtocolV5x0', () => { 'DateTimeWithZoneOffset', new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) ], + [ + 'DateTimeWithZoneOffset / 1978', + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], [ 'DateTimeWithZoneId / Berlin 2:30 CET', new DateTime(2022, 10, 30, 2, 30, 0, 183_000_000, 2 * 60 * 60, 'Europe/Berlin') @@ -515,6 +519,14 @@ describe('#unit BoltProtocolV5x0', () => { [ 'DateTimeWithZoneId / Sao Paulo just 1 after turn winter time', new DateTime(2019, 2, 18, 1, 0, 0, 183_000_000, undefined, 'America/Sao_Paulo') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(1978, 12, 16, 12, 35, 59, 128000987, undefined, 'Europe/Istanbul') + ], + [ + 'DateTimeWithZoneId / Istanbul', + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, undefined, 'Pacific/Honolulu') ] ])('should pack and unpack DateTimeWithZoneId and without offset (%s)', (_, object) => { const buffer = alloc(256) @@ -824,6 +836,13 @@ describe('#unit BoltProtocolV5x0', () => { ]), new DateTime(2022, 6, 14, 15, 21, 18, 183_000_000, 120 * 60) ], + [ + 'DateTimeWithZoneOffset / 1978', + new structure.Structure(0x49, [ + 282659759, 128000987, -150 * 60 + ]), + new DateTime(1978, 12, 16, 10, 5, 59, 128000987, -150 * 60) + ], [ 'DateTimeWithZoneId', new structure.Structure(0x69, [ @@ -837,6 +856,13 @@ describe('#unit BoltProtocolV5x0', () => { 1655212878, 183_000_000, 'Australia/Eucla' ]), new DateTime(2022, 6, 14, 22, 6, 18, 183_000_000, 8 * 60 * 60 + 45 * 60, 'Australia/Eucla') + ], + [ + 'DateTimeWithZoneId / Honolulu', + new structure.Structure(0x69, [ + 1592231400, 183_000_000, 'Pacific/Honolulu' + ]), + new DateTime(2020, 6, 15, 4, 30, 0, 183_000_000, -10 * 60 * 60, 'Pacific/Honolulu') ] ])('should unpack spatial types and temporal types (%s)', (_, struct, object) => { const buffer = alloc(256) From 4e854aba405e3f30d1215c50a58f644dbb8a922b Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Jun 2022 16:02:27 +0200 Subject: [PATCH 13/16] Fix type mapping with backend --- packages/testkit-backend/src/cypher-native-binders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index 7650deee7..f7de792d9 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -131,7 +131,7 @@ function valueResponseOfObject (x) { minute: x.minute, second: x.second, nanosecond: x.nanosecond, - utc_offset_s: x.timeZoneOffsetSeconds || 0, + utc_offset_s: x.timeZoneOffsetSeconds || (x.timeZoneId == null ? undefined : 0), timezone_id: x.timeZoneId }) } else if (neo4j.isTime(x) || neo4j.isLocalTime(x)) { From 399da38ec6185446d0cc100829c9a230b73259dc Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Jun 2022 17:57:11 +0200 Subject: [PATCH 14/16] Skiping tests --- packages/testkit-backend/src/skipped-tests/common.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index f1bb8ab78..e0602f137 100644 --- a/packages/testkit-backend/src/skipped-tests/common.js +++ b/packages/testkit-backend/src/skipped-tests/common.js @@ -1,9 +1,17 @@ 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_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( 'Testkit implemenation is deprecated', From 0f26e61c2abddaea3a527eb19a5f531f9d79a0ca Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Jun 2022 18:39:20 +0200 Subject: [PATCH 15/16] DateTime doesn't throw error anymore when created with offset and timezoneid --- packages/neo4j-driver/test/temporal-types.test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/neo4j-driver/test/temporal-types.test.js b/packages/neo4j-driver/test/temporal-types.test.js index 9815a90f4..395ce09a6 100644 --- a/packages/neo4j-driver/test/temporal-types.test.js +++ b/packages/neo4j-driver/test/temporal-types.test.js @@ -1007,9 +1007,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', () => { From fb22358fbc1c4c181da6be0071dcfbe099b1dc7a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 23 Jun 2022 12:04:51 +0200 Subject: [PATCH 16/16] Revert changes in the testkit-backend log --- packages/testkit-backend/src/channel/testkit-protocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/channel/testkit-protocol.js b/packages/testkit-backend/src/channel/testkit-protocol.js index 48fbb3808..f6282c4c7 100644 --- a/packages/testkit-backend/src/channel/testkit-protocol.js +++ b/packages/testkit-backend/src/channel/testkit-protocol.js @@ -69,7 +69,7 @@ export default class Protocol extends EventEmitter { _emitRequest () { const request = JSON.parse(this._request) const { name, data } = request - console.log('> Got request ' + name, JSON.stringify(data, undefined, 2)) + console.log('> Got request ' + name, data) this.emit('request', { name, data }) } }