diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js new file mode 100644 index 000000000..1e21960bc --- /dev/null +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v5x0.js @@ -0,0 +1,40 @@ +/** + * 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 BoltProtocolV44 from './bolt-protocol-v4x4' +import { v5 } from '../packstream' + +import { internal } from 'neo4j-driver-core' + +const { + constants: { BOLT_PROTOCOL_V5_0 }, +} = internal + +export default class BoltProtocol extends BoltProtocolV44 { + get version() { + return BOLT_PROTOCOL_V5_0 + } + + _createPacker (chunker) { + return new v5.Packer(chunker) + } + + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v5.Unpacker(disableLosslessIntegers, useBigInt) + } +} diff --git a/packages/bolt-connection/src/bolt/create.js b/packages/bolt-connection/src/bolt/create.js index a8ab0d0d6..3a641fede 100644 --- a/packages/bolt-connection/src/bolt/create.js +++ b/packages/bolt-connection/src/bolt/create.js @@ -26,6 +26,7 @@ import BoltProtocolV4x1 from './bolt-protocol-v4x1' import BoltProtocolV4x2 from './bolt-protocol-v4x2' import BoltProtocolV4x3 from './bolt-protocol-v4x3' import BoltProtocolV4x4 from './bolt-protocol-v4x4' +import BoltProtocolV5x0 from './bolt-protocol-v5x0' import { Chunker, Dechunker } from '../channel' import ResponseHandler from './response-handler' @@ -175,6 +176,16 @@ function createProtocol ( onProtocolError, serversideRouting ) + case 5.0: + return new BoltProtocolV5x0( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) default: throw newError('Unknown Bolt protocol version: ' + version) } diff --git a/packages/bolt-connection/src/bolt/handshake.js b/packages/bolt-connection/src/bolt/handshake.js index bbed15022..c41d4bd82 100644 --- a/packages/bolt-connection/src/bolt/handshake.js +++ b/packages/bolt-connection/src/bolt/handshake.js @@ -76,9 +76,9 @@ function parseNegotiatedResponse (buffer) { */ function newHandshakeBuffer () { return createHandshakeMessage([ + version(5, 0), [version(4, 4), version(4, 2)], version(4, 1), - version(4, 0), version(3, 0) ]) } diff --git a/packages/bolt-connection/src/packstream/index.js b/packages/bolt-connection/src/packstream/index.js index b06b71b99..564c32001 100644 --- a/packages/bolt-connection/src/packstream/index.js +++ b/packages/bolt-connection/src/packstream/index.js @@ -19,7 +19,8 @@ import * as v1 from './packstream-v1' import * as v2 from './packstream-v2' +import * as v5 from './packstream-v5' -export { v1, v2 } +export { v1, v2, v5 } export default v2 diff --git a/packages/bolt-connection/src/packstream/packstream-v1.js b/packages/bolt-connection/src/packstream/packstream-v1.js index b1361cac5..5f5893e9d 100644 --- a/packages/bolt-connection/src/packstream/packstream-v1.js +++ b/packages/bolt-connection/src/packstream/packstream-v1.js @@ -646,18 +646,18 @@ class Unpacker { // information about their start and end nodes, that's instead // inferred from the path sequence. This is us inferring (and, // for performance reasons remembering) the start/end of a rel. - rels[relIndex - 1] = rel = rel.bind( - prevNode.identity, - nextNode.identity + rels[relIndex - 1] = rel = rel.bindTo( + prevNode, + nextNode ) } } else { rel = rels[-relIndex - 1] if (rel instanceof UnboundRelationship) { // See above - rels[-relIndex - 1] = rel = rel.bind( - nextNode.identity, - prevNode.identity + rels[-relIndex - 1] = rel = rel.bindTo( + nextNode, + prevNode ) } } diff --git a/packages/bolt-connection/src/packstream/packstream-v5.js b/packages/bolt-connection/src/packstream/packstream-v5.js new file mode 100644 index 000000000..0d8886d20 --- /dev/null +++ b/packages/bolt-connection/src/packstream/packstream-v5.js @@ -0,0 +1,102 @@ +/** + * 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 * as v2 from './packstream-v2' +import { + Node, + Relationship, + UnboundRelationship, + int +} from 'neo4j-driver-core' + +const NODE_STRUCT_SIZE = 4 +const RELATIONSHIP_STRUCT_SIZE = 8 +const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 4 + +export class Packer extends v2.Packer { + // This implementation is the same +} + +export class Unpacker extends v2.Unpacker { + /** + * @constructor + * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. + * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint + */ + constructor (disableLosslessIntegers = false, useBigInt = false) { + this._disableLosslessIntegers = disableLosslessIntegers + this._useBigInt = useBigInt + this._defaultIdentity = this._getDefaultIdentity() + } + + _getDefaultIdentity() { + if (this._useBigInt) { + return BigInt(-1) + } else if (this._disableLosslessIntegers) { + return -1 + } else { + return int(-1) + } + } + + _unpackNode (structSize, buffer) { + this._verifyStructSize('Node', NODE_STRUCT_SIZE, structSize) + + return new Node( + _valueOrDefault(this.unpack(buffer), this._defaultIdentity), // Identity + this.unpack(buffer), // Labels + this.unpack(buffer), // Properties, + this.unpack(buffer) // ElementId + ) + } + + _unpackRelationship (structSize, buffer) { + this._verifyStructSize('Relationship', RELATIONSHIP_STRUCT_SIZE, structSize) + + return new Relationship( + _valueOrDefault(this.unpack(buffer), this._defaultIdentity), // Identity + _valueOrDefault(this.unpack(buffer), this._defaultIdentity), // Start Node Identity + _valueOrDefault(this.unpack(buffer), this._defaultIdentity), // End Node Identity + this.unpack(buffer), // Type + this.unpack(buffer), // Properties, + this.unpack(buffer), // ElementId + this.unpack(buffer), // Start Node Element Id + this.unpack(buffer) // End Node Element Id + ) + } + + _unpackUnboundRelationship (structSize, buffer) { + this._verifyStructSize( + 'UnboundRelationship', + UNBOUND_RELATIONSHIP_STRUCT_SIZE, + structSize + ) + + return new UnboundRelationship( + _valueOrDefault(this.unpack(buffer), this._defaultIdentity), // Identity + this.unpack(buffer), // Type + this.unpack(buffer), // Properties + this.unpack(buffer) // ElementId + ) + } +} + +function _valueOrDefault(value, defaultValue) { + return value === null ? defaultValue : value +} diff --git a/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js new file mode 100644 index 000000000..f8b4ed95a --- /dev/null +++ b/packages/bolt-connection/test/bolt/bolt-protocol-v5x0.test.js @@ -0,0 +1,369 @@ +/** + * 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 BoltProtocolV5x0 from '../../src/bolt/bolt-protocol-v5x0' +import RequestMessage from '../../src/bolt/request-message' +import { v5 } from '../../src/packstream' +import utils from '../test-utils' +import { RouteObserver } from '../../src/bolt/stream-observers' +import { internal } from 'neo4j-driver-core' + +const WRITE = 'WRITE' + +const { + txConfig: { TxConfig }, + bookmarks: { Bookmarks } +} = internal + +describe('#unit BoltProtocolV5x0', () => { + beforeEach(() => { + expect.extend(utils.matchers) + }) + + it('should request routing information', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, [], { databaseName, impersonatedUser: null }) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should request routing information sending bookmarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + const routingContext = { someContextParam: 'value' } + const listOfBookmarks = ['a', 'b', 'c'] + const bookmarks = new Bookmarks(listOfBookmarks) + const databaseName = 'name' + + const observer = protocol.requestRoutingInformation({ + routingContext, + databaseName, + sessionContext: { bookmarks } + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.routeV4x4(routingContext, listOfBookmarks, { databaseName, impersonatedUser: null}) + ) + expect(protocol.observers).toEqual([observer]) + expect(observer).toEqual(expect.any(RouteObserver)) + expect(protocol.flushes).toEqual([true]) + }) + + it('should run a query', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should run a with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + + const observer = protocol.run(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(2) + + expect(protocol.messages[0]).toBeMessage( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + ) + expect(protocol.messages[1]).toBeMessage(RequestMessage.pull()) + expect(protocol.observers).toEqual([observer, observer]) + expect(protocol.flushes).toEqual([false, true]) + }) + + it('should begin a transaction', () => { + const database = 'testdb' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction with impersonated user', () => { + const database = 'testdb' + const impersonatedUser = 'the impostor' + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + database, + mode: WRITE, + impersonatedUser + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, database, mode: WRITE, impersonatedUser }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should return correct bolt version number', () => { + const protocol = new BoltProtocolV5x0(null, null, false) + + expect(protocol.version).toBe(5.0) + }) + + it('should update metadata', () => { + const metadata = { t_first: 1, t_last: 2, db_hits: 3, some_other_key: 4 } + const protocol = new BoltProtocolV5x0(null, null, false) + + const transformedMetadata = protocol.transformMetadata(metadata) + + expect(transformedMetadata).toEqual({ + result_available_after: 1, + result_consumed_after: 2, + db_hits: 3, + some_other_key: 4 + }) + }) + + it('should initialize connection', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const clientName = 'js-driver/1.2.3' + const authToken = { username: 'neo4j', password: 'secret' } + + const observer = protocol.initialize({ userAgent: clientName, authToken }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.hello(clientName, authToken) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should begin a transaction', () => { + const bookmarks = new Bookmarks([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2' + ]) + const txConfig = new TxConfig({ + timeout: 5000, + metadata: { x: 1, y: 'something' } + }) + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.beginTransaction({ + bookmarks, + txConfig, + mode: WRITE + }) + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage( + RequestMessage.begin({ bookmarks, txConfig, mode: WRITE }) + ) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should commit', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.commitTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.commit()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + it('should rollback', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = new BoltProtocolV5x0(recorder, null, false) + utils.spyProtocolWrite(protocol) + + const observer = protocol.rollbackTransaction() + + protocol.verifyMessageCount(1) + expect(protocol.messages[0]).toBeMessage(RequestMessage.rollback()) + expect(protocol.observers).toEqual([observer]) + expect(protocol.flushes).toEqual([true]) + }) + + describe('unpacker configuration', () => { + test.each([ + [false, false], + [false, true], + [true, false], + [true, true] + ])( + 'should create unpacker with disableLosslessIntegers=%p and useBigInt=%p', + (disableLosslessIntegers, useBigInt) => { + const protocol = new BoltProtocolV5x0(null, null, { + disableLosslessIntegers, + useBigInt + }) + expect(protocol._unpacker._disableLosslessIntegers).toBe( + disableLosslessIntegers + ) + expect(protocol._unpacker._useBigInt).toBe(useBigInt) + } + ) + }) + + describe('watermarks', () => { + it('.run() should configure watermarks', () => { + const recorder = new utils.MessageRecordingConnection() + const protocol = utils.spyProtocolWrite( + new BoltProtocolV5x0(recorder, null, false) + ) + + const query = 'RETURN $x, $y' + const parameters = { x: 'x', y: 'y' } + const observer = protocol.run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + lowRecordWatermark: 100, + highRecordWatermark: 200, + }) + + expect(observer._lowRecordWatermark).toEqual(100) + expect(observer._highRecordWatermark).toEqual(200) + }) + }) + + describe('packstream', () => { + it('should configure v5 packer', () => { + const protocol = new BoltProtocolV5x0(null, null, false) + expect(protocol.packer()).toBeInstanceOf(v5.Packer) + }) + + it('should configure v5 unpacker', () => { + const protocol = new BoltProtocolV5x0(null, null, false) + expect(protocol.unpacker()).toBeInstanceOf(v5.Unpacker) + }) + }) +}) diff --git a/packages/bolt-connection/test/bolt/index.test.js b/packages/bolt-connection/test/bolt/index.test.js index 838b46be6..e1bc0fd94 100644 --- a/packages/bolt-connection/test/bolt/index.test.js +++ b/packages/bolt-connection/test/bolt/index.test.js @@ -30,6 +30,7 @@ import BoltProtocolV4x1 from '../../src/bolt/bolt-protocol-v4x1' import BoltProtocolV4x2 from '../../src/bolt/bolt-protocol-v4x2' import BoltProtocolV4x3 from '../../src/bolt/bolt-protocol-v4x3' import BoltProtocolV4x4 from '../../src/bolt/bolt-protocol-v4x4' +import BoltProtocolV5x0 from '../../src/bolt/bolt-protocol-v5x0' const { logger: { Logger } @@ -43,13 +44,13 @@ describe('#unit Bolt', () => { const writtenBuffer = channel.written[0] const boltMagicPreamble = '60 60 b0 17' + const protocolVersion5x0 = '00 00 00 05' const protocolVersion4x4to4x2 = '00 02 04 04' const protocolVersion4x1 = '00 00 01 04' - const protocolVersion4x0 = '00 00 00 04' const protocolVersion3 = '00 00 00 03' expect(writtenBuffer.toHex()).toEqual( - `${boltMagicPreamble} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion4x0} ${protocolVersion3}` + `${boltMagicPreamble} ${protocolVersion5x0} ${protocolVersion4x4to4x2} ${protocolVersion4x1} ${protocolVersion3}` ) }) @@ -303,7 +304,8 @@ describe('#unit Bolt', () => { v(4.1, BoltProtocolV4x1), v(4.2, BoltProtocolV4x2), v(4.3, BoltProtocolV4x3), - v(4.4, BoltProtocolV4x4) + v(4.4, BoltProtocolV4x4), + v(5.0, BoltProtocolV5x0) ] availableProtocols.forEach(lambda) diff --git a/packages/bolt-connection/test/packstream/packstream-v5.test.js b/packages/bolt-connection/test/packstream/packstream-v5.test.js new file mode 100644 index 000000000..cd38f214e --- /dev/null +++ b/packages/bolt-connection/test/packstream/packstream-v5.test.js @@ -0,0 +1,550 @@ +/** + * 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-v5' +import { Structure } from '../../src/packstream/packstream-v1' +import { Node, int, Relationship, UnboundRelationship } from 'neo4j-driver-core' + +describe('#unit PackStreamV5', () => { + 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]) + }) + + it.each( + validNodesAndConfig() + )('should unpack Nodes', (struct, expectedNode, config) => { + const node = packAndUnpack(struct, config) + + expect(node).toEqual(expectedNode) + }) + + it.each( + invalidNodesConfig() + )('should thrown error for unpacking invalid Nodes', (struct) => { + expect(() => packAndUnpack(struct)).toThrow() + }) + + it.each( + validRelationshipsAndConfig() + )('should unpack Relationships', (struct, expectedRelationship, config) => { + const releationship = packAndUnpack(struct, config) + + expect(releationship).toEqual(expectedRelationship) + }) + + it.each( + invalidRelationshipsConfig() + )('should thrown error for unpacking invalid Relationships', (struct) => { + expect(() => packAndUnpack(struct)).toThrow() + }) + + it.each( + validUnboundRelationshipsAndConfig() + )('should unpack UnboundRelationships', (struct, expectedRelationship, config) => { + const releationship = packAndUnpack(struct, config) + + expect(releationship).toEqual(expectedRelationship) + }) + + it.each( + invalidUnboundRelationshipsConfig() + )('should thrown error for unpacking invalid UnboundRelationships', (struct) => { + expect(() => packAndUnpack(struct)).toThrow() + }) + + function validNodesAndConfig() { + function validWithNumber() { + const identity = 1 + const labels = ['a', 'b'] + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedNode = new Node(identity, labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLossy() { + const identity = null + const labels = ['a', 'b'] + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedNode = new Node(-1, labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLosslessInteger() { + const identity = null + const labels = ['a', 'b'] + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedNode = new Node(int(-1), labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithoutOldIdentifiersBigInt() { + const identity = null + const labels = ['a', 'b'] + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedNode = new Node(BigInt(-1), labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: false, useBigInt: true }] + } + + function validWithInt() { + const identity = int(1) + const labels = ['a', 'b'] + const properties = { 'a': int(1), 'b': int(2) } + const elementId = 'element_id_1' + const expectedNode = new Node(identity, labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithBigInt() { + const identity = BigInt(1) + const labels = ['a', 'b'] + const properties = { 'a': BigInt(1), 'b': BigInt(2) } + const elementId = 'element_id_1' + const expectedNode = new Node(identity, labels, properties, elementId) + const nodeStruct = new Structure(0x4e, [ + identity, labels, properties, elementId + ]) + return [nodeStruct, expectedNode, { disableLosslessIntegers: false, useBigInt: true }] + } + + return [ + validWithNumber(), + validWithInt(), + validWithBigInt(), + validWithoutOldIdentifiersLossy(), + validWithoutOldIdentifiersLosslessInteger(), + validWithoutOldIdentifiersBigInt() + ] + } + + function invalidNodesConfig() { + return [ + [new Structure(0x4e, [1, ['a', 'b'], { 'a': 1, 'b': 2 }])], + [new Structure(0x4e, [1, ['a', 'b'], { 'a': 1, 'b': 2 }, 'elementId', 'myId'])], + ] + } + + function validRelationshipsAndConfig() { + function validWithNumber() { + const identity = 1 + const start = 2 + const end = 3 + const type = 'KNOWS' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + identity, start, end, type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLossy() { + const identity = null + const start = null + const end = null + const type = 'KNOWS' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + -1, -1, -1, type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLossLess() { + const identity = null + const start = null + const end = null + const type = 'KNOWS' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + int(-1), int(-1), int(-1), type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithoutOldIdentifiersBigInt() { + const identity = null + const start = null + const end = null + const type = 'KNOWS' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + BigInt(-1), BigInt(-1), BigInt(-1), type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: true, useBigInt: true }] + } + + function validWithInt() { + const identity = int(1) + const start = int(2) + const end = int(3) + const type = 'KNOWS' + const properties = { 'a': int(1), 'b': int(2) } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + identity, start, end, type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithBigInt() { + const identity = BigInt(1) + const start = BigInt(2) + const end = BigInt(3) + const type = 'KNOWS' + const properties = { 'a': BigInt(1), 'b': BigInt(2) } + const elementId = 'element_id_1' + const startNodeElementId = 'element_id_2' + const endNodeElementId = 'element_id_3' + const expectedRel = new Relationship( + identity, start, end, type, properties, + elementId, startNodeElementId, endNodeElementId) + const relStruct = new Structure(0x52, [ + identity, start, end, type, properties, elementId, + startNodeElementId, endNodeElementId + ]) + return [relStruct, expectedRel, { disableLosslessIntegers: false, useBigInt: true }] + } + + return [ + validWithNumber(), + validWithInt(), + validWithBigInt(), + validWithoutOldIdentifiersLossy(), + validWithoutOldIdentifiersLossLess(), + validWithoutOldIdentifiersBigInt() + ] + } + + function invalidRelationshipsConfig() { + return [ + [new Structure(0x52, [1, 2, 3, 'rel', { 'a': 1, 'b': 2 }, 'elementId', 'startNodeId'])], + [new Structure(0x52, [1, 2, 3, 'rel', { 'a': 1, 'b': 2 }, 'elementId', 'startNodeId', 'endNodeId', 'myId'])], + ] + } + + function validUnboundRelationshipsAndConfig() { + function validWithNumber() { + const identity = 1 + const type = 'DOESNT_KNOW' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(identity, type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLossy() { + const identity = null + const type = 'DOESNT_KNOW' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(-1, type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: true, useBigInt: false }] + } + + function validWithoutOldIdentifiersLossless() { + const identity = null + const type = 'DOESNT_KNOW' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(int(-1), type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithoutOldIdentifiersBigInt() { + const identity = null + const type = 'DOESNT_KNOW' + const properties = { 'a': 1, 'b': 2 } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(BigInt(-1), type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: false, useBigInt: true }] + } + + function validWithInt() { + const identity = int(1) + const type = 'DOESNT_KNOW' + const properties = { 'a': int(1), 'b': int(2) } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(identity, type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: false, useBigInt: false }] + } + + function validWithBigInt() { + const identity = BigInt(1) + const type = 'DOESNT_KNOW' + const properties = { 'a': BigInt(1), 'b': BigInt(2) } + const elementId = 'element_id_1' + const expectedUnboundRel = new UnboundRelationship(identity, type, properties, elementId) + const struct = new Structure(0x72, [ + identity, type, properties, elementId + ]) + return [struct, expectedUnboundRel, { disableLosslessIntegers: false, useBigInt: true }] + } + + return [ + validWithNumber(), + validWithInt(), + validWithBigInt(), + validWithoutOldIdentifiersLossy(), + validWithoutOldIdentifiersLossless(), + validWithoutOldIdentifiersBigInt() + ] + } + + function invalidUnboundRelationshipsConfig() { + return [ + [new Structure(0x72, [1, 'DOESNT_KNOW', { 'a': 1, 'b': 2 }])], + [new Structure(0x72, [1, 'DOESNT_KNOW', { 'a': 1, 'b': 2 }, 'elementId', 'myId'])], + ] + } +}) + +function packAndUnpack( + val, + { bufferSize = 128, disableLosslessIntegers = false, useBigInt = false } = {} +) { + const buffer = alloc(bufferSize) + new Packer(buffer).packable(val)() + buffer.reset() + return new Unpacker(disableLosslessIntegers, useBigInt).unpack(buffer) +} diff --git a/packages/core/src/graph-types.ts b/packages/core/src/graph-types.ts index dbd442533..06f648979 100644 --- a/packages/core/src/graph-types.ts +++ b/packages/core/src/graph-types.ts @@ -20,6 +20,9 @@ import Integer from './integer' import { stringify } from './json' type StandardDate = Date +/** + * @typedef {number | Integer | bigint} NumberOrInteger + */ type NumberOrInteger = number | Integer | bigint type Properties = { [key: string]: any } @@ -48,17 +51,20 @@ class Node} labels - Array for all labels * @param {Properties} properties - Map with node properties + * @param {string} elementId - Node element identifier */ - constructor(identity: T, labels: string[], properties: P) { + constructor(identity: T, labels: string[], properties: P, elementId?: string) { /** * Identity of the node. - * @type {Integer|number} + * @type {NumberOrInteger} + * @deprecated use {@link Node#elementId} instead */ this.identity = identity /** @@ -71,13 +77,18 @@ class Node identity.toString()) } /** * @ignore */ toString() { - let s = '(' + this.identity + let s = '(' + this.elementId for (let i = 0; i < this.labels.length; i++) { s += ':' + this.labels[i] } @@ -119,30 +130,42 @@ class Relationship identity.toString()) + + /** + * The Start Node element identifier. + * @type {string} + */ + this.startNodeElementId = _valueOrGetDefault(startNodeElementId, () => start.toString()) + + /** + * The End Node element identifier. + * @type {string} + */ + this.endNodeElementId = _valueOrGetDefault(endNodeElementId, () => end.toString()) } /** * @ignore */ toString(): string { - let s = '(' + this.start + ')-[:' + this.type + let s = '(' + this.startNodeElementId + ')-[:' + this.type const keys = Object.keys(this.properties) if (keys.length > 0) { s += ' {' @@ -171,7 +212,7 @@ class Relationship identity.toString()) } /** * Bind relationship * * @protected + * @deprecated use {@link UnboundRelationship#bindTo} instead * @param {Integer} start - Identity of start node * @param {Integer} end - Identity of end node * @return {Relationship} - Created relationship @@ -239,7 +290,29 @@ class UnboundRelationship, end: Node): Relationship { + return new Relationship( + this.identity, + start.identity, + end.identity, + this.type, + this.properties, + this.elementId, + start.elementId, + end.elementId, ) } @@ -379,6 +452,10 @@ function isPath(obj: object): obj is Path { return hasIdentifierProperty(obj, PATH_IDENTIFIER_PROPERTY) } +function _valueOrGetDefault (value: T|undefined|null, getDefault: () => T): T { + return value === undefined || value === null ? getDefault() : value +} + export { Node, isNode, diff --git a/packages/core/src/internal/constants.ts b/packages/core/src/internal/constants.ts index 922ab884f..39e790f2f 100644 --- a/packages/core/src/internal/constants.ts +++ b/packages/core/src/internal/constants.ts @@ -33,6 +33,7 @@ const BOLT_PROTOCOL_V4_1: number = 4.1 const BOLT_PROTOCOL_V4_2: number = 4.2 const BOLT_PROTOCOL_V4_3: number = 4.3 const BOLT_PROTOCOL_V4_4: number = 4.4 +const BOLT_PROTOCOL_V5_0: number = 5.0 export { FETCH_ALL, @@ -48,5 +49,6 @@ export { BOLT_PROTOCOL_V4_1, BOLT_PROTOCOL_V4_2, BOLT_PROTOCOL_V4_3, - BOLT_PROTOCOL_V4_4 + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_0 } diff --git a/packages/core/test/__snapshots__/graph-types.test.ts.snap b/packages/core/test/__snapshots__/graph-types.test.ts.snap new file mode 100644 index 000000000..70d980fc3 --- /dev/null +++ b/packages/core/test/__snapshots__/graph-types.test.ts.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Node should be serialized as string 1`] = `"(elementId:label)"`; + +exports[`Node should be serialized as string 2`] = `"(1:label)"`; + +exports[`Node should be serialized as string 3`] = `"(1)"`; + +exports[`Node should be serialized as string 4`] = `"(2:label)"`; + +exports[`Node should be serialized as string 5`] = `"(3:label)"`; + +exports[`Node should be serialized as string 6`] = `"(1 {property:\\"value\\"})"`; + +exports[`Node should be serialized as string 7`] = `"(1:label {property:\\"value\\"})"`; + +exports[`Relationship should be serialized as string 1`] = `"(startNodeElementId)-[:Rel]->(endNodeElementId)"`; + +exports[`Relationship should be serialized as string 2`] = `"(startNodeElementId)-[:Rel]->(3)"`; + +exports[`Relationship should be serialized as string 3`] = `"(2)-[:Rel]->(3)"`; + +exports[`Relationship should be serialized as string 4`] = `"(2)-[:Rel]->(3)"`; + +exports[`Relationship should be serialized as string 5`] = `"(2)-[:Rel {property:\\"value\\"}]->(3)"`; + +exports[`Relationship should be serialized as string 6`] = `"(5)-[:Rel]->(6)"`; + +exports[`Relationship should be serialized as string 7`] = `"(7)-[:Rel]->(8)"`; + +exports[`UnboundRelationship should be serialized as string 1`] = `"-[:Rel]->"`; + +exports[`UnboundRelationship should be serialized as string 2`] = `"-[:Rel]->"`; + +exports[`UnboundRelationship should be serialized as string 3`] = `"-[:Rel {property:\\"value\\"}]->"`; + +exports[`UnboundRelationship should be serialized as string 4`] = `"-[:Rel {property:\\"value\\"}]->"`; + +exports[`UnboundRelationship should be serialized as string 5`] = `"-[:Rel {property:\\"value\\"}]->"`; diff --git a/packages/core/test/graph-types.test.ts b/packages/core/test/graph-types.test.ts new file mode 100644 index 000000000..d991837d0 --- /dev/null +++ b/packages/core/test/graph-types.test.ts @@ -0,0 +1,342 @@ +/** + * 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 { + Node, + isNode, + Relationship, + isRelationship, + UnboundRelationship, + isUnboundRelationship, +} from '../src/graph-types' + +import { + int +} from '../src/integer' + +describe('Node', () => { + test('should have identity', () => { + const node = new Node(1, [], {}) + + expect(node.identity).toEqual(1) + }) + + test('should have labels', () => { + const node = new Node(1, ['label'], {}) + + expect(node.labels).toEqual(['label']) + }) + + test('should have properties', () => { + const node = new Node(1, [], { + property: 'value' + }) + + expect(node.properties).toEqual({ + property: 'value' + }) + }) + + test('should have elementId', () => { + const node = new Node(1, [], {}, 'elementId') + + expect(node.elementId).toEqual('elementId') + }) + + test.each( + validIdentityAndExpectedElementIds() + )('should have elementId default to identity when it is not set', (identity, expected) => { + const node = new Node(identity, [], {}) + + expect(node.elementId).toEqual(expected) + }) + + test.each(validNodes())('should be serialized as string', node => { + expect(node.toString()).toMatchSnapshot() + }) + + test.each(validNodes())('should be consider a node', (node: any) => { + expect(isNode(node)).toBe(true) + }) + + test.each(nonNodes())('should not consider a non-node object as node', nonNode => { + expect(isNode(nonNode)).toBe(false) + }) + + function validNodes(): any[] { + return [ + [new Node(1, ['label'], {}, 'elementId')], + [new Node(1, ['label'], {})], + [new Node(1, [], {})], + [new Node(BigInt(2), ['label'], {})], + [new Node(int(3), ['label'], {})], + [new Node(1, [], { 'property': 'value' })], + [new Node(1, ['label'], { 'property': 'value' })], + ] + } + + function nonNodes(): any[] { + return [ + [undefined], + [null], + [{ identity: 1, labels: ['label'], properties: { 'property': 'value' } }], + [{ identity: 1, labels: ['label'], properties: { 'property': 'value' }, elementId: 'elementId' }], + [{}], + [{ 'property': 'value' }], + [{ 'property': 'value', 'labels': ['label'] }], + [{ 'property': 'value', 'labels': ['label'], 'identity': 1 }], + [{ identity: BigInt(2), labels: ['label'], properties: { 'property': 'value' } }], + [{ identity: int(3), labels: ['label'], properties: { 'property': 'value' } }], + ] + } +}) + +describe('Relationship', () => { + test('should have identity', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}) + + expect(relationship.identity).toEqual(1) + }) + + test('should have start', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}) + + expect(relationship.start).toEqual(2) + }) + + test('should have end', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}) + + expect(relationship.end).toEqual(3) + }) + + test('should have type', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}) + + expect(relationship.type).toEqual('Rel') + }) + + test('should have properties', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', { 'property': 'value' }) + + expect(relationship.properties).toEqual({ 'property': 'value' }) + }) + + test('should have elementId', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}, 'elementId') + + expect(relationship.elementId).toEqual('elementId') + }) + + test.each( + validIdentityAndExpectedElementIds() + )('should default elementId to indentity when it is not set', (identity, expected) => { + const relationship = new Relationship(identity, 2, 3, 'Rel', {}) + + expect(relationship.elementId).toEqual(expected) + }) + + test('should have startNodeElementId', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}, 'elementId', 'startNodeElementId') + + expect(relationship.startNodeElementId).toEqual('startNodeElementId') + }) + + test.each( + validIdentityAndExpectedElementIds() + )('should default startNodeElementId to start when it is not set', (identity, expected) => { + const relationship = new Relationship(1, identity, 3, 'Rel', {}) + + expect(relationship.startNodeElementId).toEqual(expected) + }) + + test('should have endNodeElementId', () => { + const relationship = new Relationship(1, 2, 3, 'Rel', {}, 'elementId', 'startNodeElementId', 'endNodeElementId') + + expect(relationship.endNodeElementId).toEqual('endNodeElementId') + }) + + test.each( + validIdentityAndExpectedElementIds() + )('should default endNodeElementId to start when it is not set', (identity, expected) => { + const relationship = new Relationship(1, 2, identity, 'Rel', {}) + + expect(relationship.endNodeElementId).toEqual(expected) + }) + + test.each(validRelationships())('should be serialized as string', relationship => { + expect(relationship.toString()).toMatchSnapshot() + }) + + test.each(validRelationships())('should be consider a relationship', relationship => { + expect(isRelationship(relationship)).toBe(true) + }) + + test.each(nonRelationships())('should not consider a non-relationship object as relationship', nonRelationship => { + expect(isRelationship(nonRelationship)).toBe(false) + }) + + function validRelationships (): any[] { + return [ + [new Relationship(1, 2, 3, 'Rel', {}, 'elementId', 'startNodeElementId', 'endNodeElementId')], + [new Relationship(1, 2, 3, 'Rel', {}, 'elementId', 'startNodeElementId')], + [new Relationship(1, 2, 3, 'Rel', {}, 'elementId')], + [new Relationship(1, 2, 3, 'Rel', {})], + [new Relationship(1, 2, 3, 'Rel', { 'property': 'value' })], + [new Relationship(BigInt(4), BigInt(5), BigInt(6), 'Rel', {})], + [new Relationship(int(6), int(7), int(8), 'Rel', {})], + ] + } + + function nonRelationships (): any[] { + return [ + [undefined], + [null], + ['Relationship'], + [{}], + [{ 'property': 'value' }], + [{ + identity: 1, start: 2, end: 3, type: 'Rel', + properties: { 'property': 'value' } + }], + [{ + identity: 1, start: 2, end: 3, type: 'Rel', + properties: { 'property': 'value' }, elementId: 'elementId' + }], + [{ + identity: 1, start: 2, end: 3, type: 'Rel', + properties: { 'property': 'value' }, elementId: 'elementId', + startNodeElementId: 'startNodeElementId', endNodeElementId: 'endNodeElementId' + }], + ] + } +}) + +describe('UnboundRelationship', () => { + test('should have identity', () => { + const relationship = new UnboundRelationship(1, 'Rel', {}) + + expect(relationship.identity).toEqual(1) + }) + + test('should have type', () => { + const relationship = new UnboundRelationship(1, 'Rel', {}) + + expect(relationship.type).toEqual('Rel') + }) + + test('should have properties', () => { + const relationship = new UnboundRelationship(1, 'Rel', { 'property': 'value' }) + + expect(relationship.properties).toEqual({ 'property': 'value' }) + }) + + test.each(validUnboundRelationships())('should be serialized as string', relationship => { + expect(relationship.toString()).toMatchSnapshot() + }) + + test.each(validUnboundRelationships())('should be consider a unbound relationship', relationship => { + expect(isUnboundRelationship(relationship)).toBe(true) + }) + + test.each( + nonUnboundRelationships() + )('should not consider a non-unbound relationship object as unbound relationship', nonUnboundRelationship => { + expect(isUnboundRelationship(nonUnboundRelationship)).toBe(false) + }) + + test.each( + bindUnboundRelationshipFixture() + )('should bind with node identity', (rel, startNode, endNode) => { + expect(rel.bind(startNode.identity, endNode.identity)) + .toEqual( + new Relationship( + rel.identity, + startNode.identity, + endNode.identity, + rel.type, + rel.properties, + rel.elementId + ) + ) + }) + + test.each( + bindUnboundRelationshipFixture() + )('should bind to nodes', (rel, startNode, endNode) => { + expect(rel.bindTo(startNode, endNode)) + .toEqual( + new Relationship( + rel.identity, + startNode.identity, + endNode.identity, + rel.type, + rel.properties, + rel.elementId, + startNode.elementId, + endNode.elementId + ) + ) + }) + + function validUnboundRelationships (): any[] { + return [ + [new UnboundRelationship(1, 'Rel', {}, 'elementId')], + [new UnboundRelationship(1, 'Rel', {})], + [new UnboundRelationship(1, 'Rel', { 'property': 'value' })], + [new UnboundRelationship(BigInt(2), 'Rel', { 'property': 'value' })], + [new UnboundRelationship(int(3), 'Rel', { 'property': 'value' })], + ] + } + + function nonUnboundRelationships (): any[] { + return [ + [undefined], + [null], + ['Relationship'], + [{}], + [{ 'property': 'value' }], + [{ + identity: 1, type: 'Rel', + properties: { 'property': 'value' } + }], + [{ + identity: 1, type: 'Rel', + properties: { 'property': 'value' }, elementId: 'elementId' + }] + ] + } + + function bindUnboundRelationshipFixture (): any[] { + return [ + [new UnboundRelationship(0, 'Rel', {}), new Node(1, ['Node'], {}), new Node(2, ['Node'], {})], + [new UnboundRelationship(0, 'Rel', {}, 'elementId'), new Node(1, ['Node'], {}), new Node(2, ['Node'], {})], + [new UnboundRelationship(0, 'Rel', {}), new Node(1, ['Node'], {}, 'nodeElementId'), new Node(2, ['Node'], {})], + [new UnboundRelationship(0, 'Rel', {}), new Node(1, ['Node'], {}, 'nodeElementId'), new Node(2, ['Node'], {}), 'nodeElementId2'], + [new UnboundRelationship(0, 'Rel', {}, 'elementId'), new Node(1, ['Node'], {}, 'nodeElementId'), new Node(2, ['Node'], {}), 'nodeElementId2'], + ] + } +}) + +function validIdentityAndExpectedElementIds (): any[] { + return [ + [10, '10'], + [int(12), '12'], + [BigInt(32), '32'], + ] +} diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index f7852e174..c625ab403 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -60,7 +60,8 @@ export function nativeToCypher (x) { const node = { id: nativeToCypher(x.identity), labels: nativeToCypher(x.labels), - props: nativeToCypher(x.properties) + props: nativeToCypher(x.properties), + elementId: nativeToCypher(x.elementId) } return { name: 'CypherNode', data: node } } @@ -70,7 +71,10 @@ export function nativeToCypher (x) { startNodeId: nativeToCypher(x.start), endNodeId: nativeToCypher(x.end), type: nativeToCypher(x.type), - props: nativeToCypher(x.properties) + props: nativeToCypher(x.properties), + elementId: nativeToCypher(x.elementId), + startNodeElementId: nativeToCypher(x.startNodeElementId), + endNodeElementId: nativeToCypher(x.endNodeElementId) } return { name: 'CypherRelationship', data: relationship } } diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index ea419ef36..e78d029b3 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -27,6 +27,7 @@ const features = [ 'Feature:Bolt:4.2', 'Feature:Bolt:4.3', 'Feature:Bolt:4.4', + 'Feature:Bolt:5.0', 'Feature:API:ConnectionAcquisitionTimeout', 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', diff --git a/packages/testkit-backend/src/skipped-tests/common.js b/packages/testkit-backend/src/skipped-tests/common.js index f550b7e03..2887fa17d 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, ifStartsWith } from './skip' const skippedTests = [ + skip( + 'Skipped because server doesn\'t support protocol 5.0 yet', + ifEndsWith('neo4j.test_summary.TestSummary.test_protocol_version_information') + ), skip( 'Handle qid omission optmization can cause issues in nested queries', ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments'),