From 6abeb1bd49f71354f270041ed5d7b46975628deb Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Oct 2022 15:44:53 +0200 Subject: [PATCH 1/2] Commit `neo4j-driver-deno` to the repository The `deno` driver is generated using a custom script made for this repository. This script doesn't have any tests linked to it. So the only way this changes are tested is by running the `deno driver` test suite. For guarantee the behaviour of this driver, we should version commit the generated driver to the repository. This changes introduce the first commited version of the driver. Adding the verification if the commited deno driver is in sync with the lite driver is also part of the this scope. --- package.json | 2 +- packages/neo4j-driver-deno/.gitignore | 2 +- packages/neo4j-driver-deno/generate.ts | 4 ++-- testkit/build.py | 13 +++++++++---- testkit/common.py | 4 ++++ 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a0cb4ac09..f34bc042f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "scripts": { "clean": "lerna clean -y && lerna run clean", "build": "lerna bootstrap --ci", - "build::deno": "(cd ./packages/neo4j-driver-deno && deno run --allow-read --allow-write --allow-net ./generate.ts --version=5.0.0-dev)", + "build::deno": "cd ./packages/neo4j-driver-deno && deno run --allow-read --allow-write --allow-net ./generate.ts --version=5.0.0-dev", "build::notci": "lerna bootstrap", "docs": "lerna run docs --stream --concurrency 1", "test::unit": "lerna run test::unit --stream", diff --git a/packages/neo4j-driver-deno/.gitignore b/packages/neo4j-driver-deno/.gitignore index 5f354661c..68e2c5173 100644 --- a/packages/neo4j-driver-deno/.gitignore +++ b/packages/neo4j-driver-deno/.gitignore @@ -1,2 +1,2 @@ -lib/ .vscode/ +lib2/ diff --git a/packages/neo4j-driver-deno/generate.ts b/packages/neo4j-driver-deno/generate.ts index ed5741c62..582f6c463 100644 --- a/packages/neo4j-driver-deno/generate.ts +++ b/packages/neo4j-driver-deno/generate.ts @@ -27,7 +27,7 @@ const isDir = (path: string) => { //////////////////////////////////////////////////////////////////////////////// // Parse arguments const parsedArgs = parse(Deno.args, { - string: ["version"], + string: ["version", "output"], boolean: ["transform"], // Pass --no-transform to disable default: { transform: true }, unknown: (arg) => { @@ -42,7 +42,7 @@ const version = parsedArgs.version ?? "0.0.0dev"; //////////////////////////////////////////////////////////////////////////////// // Clear out the destination folder -const rootOutDir = "lib/"; +const rootOutDir = parsedArgs.output ?? "lib/"; await ensureDir(rootOutDir); // Make sure it exists for await (const existingFile of Deno.readDir(rootOutDir)) { await Deno.remove(`${rootOutDir}${existingFile.name}`, { recursive: true }); diff --git a/testkit/build.py b/testkit/build.py index ad428a0b5..7fdbd7000 100644 --- a/testkit/build.py +++ b/testkit/build.py @@ -2,13 +2,12 @@ Executed in Javascript driver container. Responsible for building driver and test backend. """ -from common import is_deno, run, run_in_driver_repo, DRIVER_REPO +from common import is_deno, is_team_city, run, run_in_driver_repo, DRIVER_REPO import os def copy_files_to_workdir(): - run(["mkdir", DRIVER_REPO]) - run(["cp", "-fr", ".", DRIVER_REPO]) + run(["cp", "-fr", "./", DRIVER_REPO]) run(["chown", "-Rh", "driver:driver", DRIVER_REPO]) @@ -20,7 +19,13 @@ def init_monorepo(): def clean_and_build(): run_in_driver_repo(["npm", "run", "clean"], env=os.environ) run_in_driver_repo(["npm", "run", "build"], env=os.environ) - run_in_driver_repo(["npm", "run", "build::deno"], env=os.environ) + run_in_driver_repo(["npm", "run", "build::deno", "--", + "--output=lib2/"], env=os.environ) + + if is_deno() and is_team_city(): + run_in_driver_repo(["diff", "-r", "-u", + "packages/neo4j-driver-deno/lib/", + "packages/neo4j-driver-deno/lib2/"]) if __name__ == "__main__": diff --git a/testkit/common.py b/testkit/common.py index 93717b9d9..cd01753aa 100644 --- a/testkit/common.py +++ b/testkit/common.py @@ -46,3 +46,7 @@ def is_browser(): def is_deno(): return is_enabled(os.environ.get("TEST_DRIVER_DENO", "false")) + + +def is_team_city(): + return is_enabled(os.environ.get("TEST_IN_TEAMCITY", "false")) From 286fc1a9338c5f5d4abbca1a8a54090e283e4432 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Oct 2022 16:31:19 +0200 Subject: [PATCH 2/2] Committing the source --- .../bolt/bolt-protocol-util.js | 82 ++ .../bolt-connection/bolt/bolt-protocol-v1.js | 493 ++++++++ .../bolt/bolt-protocol-v1.transformer.js | 187 +++ .../bolt-connection/bolt/bolt-protocol-v2.js | 48 + .../bolt/bolt-protocol-v2.transformer.js | 432 +++++++ .../bolt-connection/bolt/bolt-protocol-v3.js | 246 ++++ .../bolt/bolt-protocol-v3.transformer.js | 24 + .../bolt/bolt-protocol-v4x0.js | 194 +++ .../bolt/bolt-protocol-v4x0.transformer.js | 24 + .../bolt/bolt-protocol-v4x1.js | 89 ++ .../bolt/bolt-protocol-v4x1.transformer.js | 24 + .../bolt/bolt-protocol-v4x2.js | 41 + .../bolt/bolt-protocol-v4x2.transformer.js | 24 + .../bolt/bolt-protocol-v4x3.js | 126 ++ .../bolt/bolt-protocol-v4x3.transformer.js | 24 + .../bolt/bolt-protocol-v4x4.js | 174 +++ .../bolt/bolt-protocol-v4x4.transformer.js | 24 + .../bolt/bolt-protocol-v5x0.js | 68 + .../bolt/bolt-protocol-v5x0.transformer.js | 131 ++ .../bolt-protocol-v5x0.utc.transformer.js | 281 +++++ .../lib/bolt-connection/bolt/create.js | 197 +++ .../lib/bolt-connection/bolt/handshake.js | 133 ++ .../lib/bolt-connection/bolt/index.js | 32 + .../bolt-connection/bolt/request-message.js | 329 +++++ .../bolt-connection/bolt/response-handler.js | 215 ++++ .../bolt-connection/bolt/routing-table-raw.js | 158 +++ .../bolt-connection/bolt/stream-observers.js | 679 ++++++++++ .../bolt-connection/bolt/temporal-factory.js | 144 +++ .../lib/bolt-connection/bolt/transformer.js | 130 ++ .../lib/bolt-connection/buf/base-buf.js | 417 +++++++ .../lib/bolt-connection/buf/index.js | 23 + .../channel/browser/browser-channel.js | 419 +++++++ .../browser/browser-host-name-resolver.js | 30 + .../bolt-connection/channel/browser/index.js | 34 + .../bolt-connection/channel/channel-buf.js | 101 ++ .../bolt-connection/channel/channel-config.js | 89 ++ .../lib/bolt-connection/channel/chunking.js | 209 ++++ .../bolt-connection/channel/combined-buf.js | 71 ++ .../channel/deno/deno-channel.js | 336 +++++ .../channel/deno/deno-host-name-resolver.js | 29 + .../lib/bolt-connection/channel/deno/index.js | 34 + .../lib/bolt-connection/channel/index.js | 24 + .../lib/bolt-connection/channel/node/index.js | 35 + .../channel/node/node-channel.js | 423 +++++++ .../channel/node/node-host-name-resolver.js | 42 + .../lib/bolt-connection/channel/utf8.js | 104 ++ .../connection-provider-direct.js | 117 ++ .../connection-provider-pooled.js | 149 +++ .../connection-provider-routing.js | 715 +++++++++++ .../connection-provider-single.js | 37 + .../connection-provider/index.js | 22 + .../connection/connection-channel.js | 448 +++++++ .../connection/connection-delegate.js | 101 ++ .../connection/connection-error-handler.js | 109 ++ .../bolt-connection/connection/connection.js | 132 ++ .../lib/bolt-connection/connection/index.js | 34 + .../lib/bolt-connection/index.js | 27 + .../lib/bolt-connection/lang/functional.js | 30 + .../lib/bolt-connection/lang/index.js | 20 + .../bolt-connection/load-balancing/index.js | 23 + ...least-connected-load-balancing-strategy.js | 83 ++ .../load-balancing/load-balancing-strategy.js | 41 + .../load-balancing/round-robin-array-index.js | 47 + .../lib/bolt-connection/packstream/index.js | 26 + .../packstream/packstream-v1.js | 547 +++++++++ .../packstream/packstream-v2.js | 37 + .../bolt-connection/packstream/structure.js | 63 + .../lib/bolt-connection/pool/index.js | 27 + .../lib/bolt-connection/pool/pool-config.js | 60 + .../lib/bolt-connection/pool/pool.js | 450 +++++++ .../lib/bolt-connection/rediscovery/index.js | 24 + .../rediscovery/rediscovery.js | 78 ++ .../rediscovery/routing-table.js | 266 ++++ .../lib/bolt-connection/types/index.d.ts | 34 + packages/neo4j-driver-deno/lib/core/auth.ts | 89 ++ .../lib/core/bookmark-manager.ts | 187 +++ .../lib/core/connection-provider.ts | 123 ++ .../neo4j-driver-deno/lib/core/connection.ts | 122 ++ packages/neo4j-driver-deno/lib/core/driver.ts | 594 +++++++++ packages/neo4j-driver-deno/lib/core/error.ts | 160 +++ .../neo4j-driver-deno/lib/core/graph-types.ts | 474 +++++++ packages/neo4j-driver-deno/lib/core/index.ts | 227 ++++ .../neo4j-driver-deno/lib/core/integer.ts | 1093 +++++++++++++++++ .../lib/core/internal/bookmarks.ts | 135 ++ .../lib/core/internal/connection-holder.ts | 331 +++++ .../lib/core/internal/constants.ts | 54 + .../lib/core/internal/index.ts | 48 + .../lib/core/internal/logger.ts | 224 ++++ .../lib/core/internal/object-util.ts | 87 ++ .../lib/core/internal/observers.ts | 210 ++++ .../resolver/base-host-name-resolver.ts | 34 + .../resolver/configured-custom-resolver.ts | 47 + .../lib/core/internal/resolver/index.ts | 22 + .../lib/core/internal/server-address.ts | 79 ++ .../lib/core/internal/temporal-util.ts | 645 ++++++++++ .../lib/core/internal/transaction-executor.ts | 268 ++++ .../lib/core/internal/tx-config.ts | 97 ++ .../lib/core/internal/url-util.ts | 343 ++++++ .../lib/core/internal/util.ts | 238 ++++ packages/neo4j-driver-deno/lib/core/json.ts | 41 + packages/neo4j-driver-deno/lib/core/record.ts | 245 ++++ .../lib/core/result-summary.ts | 549 +++++++++ packages/neo4j-driver-deno/lib/core/result.ts | 656 ++++++++++ .../neo4j-driver-deno/lib/core/session.ts | 594 +++++++++ .../lib/core/spatial-types.ts | 98 ++ .../lib/core/temporal-types.ts | 809 ++++++++++++ .../lib/core/transaction-managed.ts | 68 + .../lib/core/transaction-promise.ts | 195 +++ .../neo4j-driver-deno/lib/core/transaction.ts | 699 +++++++++++ packages/neo4j-driver-deno/lib/core/types.ts | 81 ++ packages/neo4j-driver-deno/lib/logging.ts | 20 + packages/neo4j-driver-deno/lib/mod.ts | 537 ++++++++ packages/neo4j-driver-deno/lib/version.ts | 20 + 113 files changed, 21463 insertions(+) create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.utc.transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/routing-table-raw.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/temporal-factory.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/bolt/transformer.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/buf/base-buf.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/buf/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-host-name-resolver.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-buf.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/chunking.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/combined-buf.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-host-name-resolver.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-host-name-resolver.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/channel/utf8.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-single.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/lang/functional.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/least-connected-load-balancing-strategy.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/load-balancing-strategy.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/round-robin-array-index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/packstream/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/packstream/structure.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/pool/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/pool/pool-config.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/index.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/rediscovery.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/routing-table.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/types/index.d.ts create mode 100644 packages/neo4j-driver-deno/lib/core/auth.ts create mode 100644 packages/neo4j-driver-deno/lib/core/bookmark-manager.ts create mode 100644 packages/neo4j-driver-deno/lib/core/connection-provider.ts create mode 100644 packages/neo4j-driver-deno/lib/core/connection.ts create mode 100644 packages/neo4j-driver-deno/lib/core/driver.ts create mode 100644 packages/neo4j-driver-deno/lib/core/error.ts create mode 100644 packages/neo4j-driver-deno/lib/core/graph-types.ts create mode 100644 packages/neo4j-driver-deno/lib/core/index.ts create mode 100644 packages/neo4j-driver-deno/lib/core/integer.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/bookmarks.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/constants.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/index.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/logger.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/object-util.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/observers.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/resolver/base-host-name-resolver.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/resolver/configured-custom-resolver.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/resolver/index.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/server-address.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/temporal-util.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/tx-config.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/url-util.ts create mode 100644 packages/neo4j-driver-deno/lib/core/internal/util.ts create mode 100644 packages/neo4j-driver-deno/lib/core/json.ts create mode 100644 packages/neo4j-driver-deno/lib/core/record.ts create mode 100644 packages/neo4j-driver-deno/lib/core/result-summary.ts create mode 100644 packages/neo4j-driver-deno/lib/core/result.ts create mode 100644 packages/neo4j-driver-deno/lib/core/session.ts create mode 100644 packages/neo4j-driver-deno/lib/core/spatial-types.ts create mode 100644 packages/neo4j-driver-deno/lib/core/temporal-types.ts create mode 100644 packages/neo4j-driver-deno/lib/core/transaction-managed.ts create mode 100644 packages/neo4j-driver-deno/lib/core/transaction-promise.ts create mode 100644 packages/neo4j-driver-deno/lib/core/transaction.ts create mode 100644 packages/neo4j-driver-deno/lib/core/types.ts create mode 100644 packages/neo4j-driver-deno/lib/logging.ts create mode 100644 packages/neo4j-driver-deno/lib/mod.ts create mode 100644 packages/neo4j-driver-deno/lib/version.ts diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js new file mode 100644 index 000000000..21293dd7a --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-util.js @@ -0,0 +1,82 @@ +/** + * 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 { newError } from '../../core/index.ts' +// eslint-disable-next-line no-unused-vars +import { ResultStreamObserver } from './stream-observers.js' + +/** + * @param {TxConfig} txConfig the auto-commit transaction configuration. + * @param {function(error: string)} onProtocolError called when the txConfig is not empty. + * @param {ResultStreamObserver} observer the response observer. + */ +function assertTxConfigIsEmpty (txConfig, onProtocolError = () => {}, observer) { + if (txConfig && !txConfig.isEmpty()) { + const error = newError( + 'Driver is connected to the database that does not support transaction configuration. ' + + 'Please upgrade to neo4j 3.5.0 or later in order to use this functionality' + ) + + // unsupported API was used, consider this a fatal error for the current connection + onProtocolError(error.message) + observer.onError(error) + throw error + } +} + +/** + * Asserts that the passed-in database name is empty. + * @param {string} database + * @param {fuction(err: String)} onProtocolError Called when it doesn't have database set + */ +function assertDatabaseIsEmpty (database, onProtocolError = () => {}, observer) { + if (database) { + const error = newError( + 'Driver is connected to the database that does not support multiple databases. ' + + 'Please upgrade to neo4j 4.0.0 or later in order to use this functionality' + ) + + // unsupported API was used, consider this a fatal error for the current connection + onProtocolError(error.message) + observer.onError(error) + throw error + } +} + +/** + * Asserts that the passed-in impersonated user is empty + * @param {string} impersonatedUser + * @param {function (err:Error)} onProtocolError Called when it does have impersonated user set + * @param {any} observer + */ +function assertImpersonatedUserIsEmpty (impersonatedUser, onProtocolError = () => {}, observer) { + if (impersonatedUser) { + const error = newError( + 'Driver is connected to the database that does not support user impersonation. ' + + 'Please upgrade to neo4j 4.4.0 or later in order to use this functionality. ' + + `Trying to impersonate ${impersonatedUser}.` + ) + + // unsupported API was used, consider this a fatal error for the current connection + onProtocolError(error.message) + observer.onError(error) + throw error + } +} + +export { assertDatabaseIsEmpty, assertTxConfigIsEmpty, assertImpersonatedUserIsEmpty } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js new file mode 100644 index 000000000..20bf7a31b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -0,0 +1,493 @@ +/** + * 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 { + assertDatabaseIsEmpty, + assertTxConfigIsEmpty, + assertImpersonatedUserIsEmpty +} from './bolt-protocol-util.js' +// eslint-disable-next-line no-unused-vars +import { Chunker } from '../channel/index.js' +import { structure, v1 } from '../packstream/index.js' +import RequestMessage from './request-message.js' +import { + LoginObserver, + ResetObserver, + ResultStreamObserver, + // eslint-disable-next-line no-unused-vars + StreamObserver +} from './stream-observers.js' +import { internal } from '../../core/index.ts' +import transformersFactories from './bolt-protocol-v1.transformer.js' +import Transformer from './transformer.js' + +const { + bookmarks: { Bookmarks }, + constants: { ACCESS_MODE_WRITE, BOLT_PROTOCOL_V1 }, + // eslint-disable-next-line no-unused-vars + logger: { Logger }, + txConfig: { TxConfig } +} = internal + +export default class BoltProtocol { + /** + * @callback CreateResponseHandler Creates the response handler + * @param {BoltProtocol} protocol The bolt protocol + * @returns {ResponseHandler} The response handler + */ + /** + * @callback OnProtocolError Handles protocol error + * @param {string} error The description + */ + /** + * @constructor + * @param {Object} server the server informatio. + * @param {Chunker} chunker the chunker. + * @param {Object} packstreamConfig Packstream configuration + * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler + * @param {Logger} log the logger + * @param {OnProtocolError} onProtocolError handles protocol errors + */ + constructor ( + server, + chunker, + { disableLosslessIntegers, useBigInt } = {}, + createResponseHandler = () => null, + log, + onProtocolError + ) { + this._server = server || {} + this._chunker = chunker + this._packer = this._createPacker(chunker) + this._unpacker = this._createUnpacker(disableLosslessIntegers, useBigInt) + this._responseHandler = createResponseHandler(this) + this._log = log + this._onProtocolError = onProtocolError + this._fatalError = null + this._lastMessageSignature = null + this._config = { disableLosslessIntegers, useBigInt } + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Returns the numerical version identifier for this protocol + */ + get version () { + return BOLT_PROTOCOL_V1 + } + + /** + * Get the packer. + * @return {Packer} the protocol's packer. + */ + packer () { + return this._packer + } + + /** + * Creates a packable function out of the provided value + * @param x the value to pack + * @returns Function + */ + packable (x) { + return this._packer.packable(x, this.transformer.toStructure) + } + + /** + * Get the unpacker. + * @return {Unpacker} the protocol's unpacker. + */ + unpacker () { + return this._unpacker + } + + /** + * Unpack a buffer + * @param {Buffer} buf + * @returns {any|null} The unpacked value + */ + unpack (buf) { + return this._unpacker.unpack(buf, this.transformer.fromStructure) + } + + /** + * Transform metadata received in SUCCESS message before it is passed to the handler. + * @param {Object} metadata the received metadata. + * @return {Object} transformed metadata. + */ + transformMetadata (metadata) { + return metadata + } + + /** + * Perform initialization and authentication of the underlying connection. + * @param {Object} param + * @param {string} param.userAgent the user agent. + * @param {Object} param.authToken the authentication token. + * @param {function(err: Error)} param.onError the callback to invoke on error. + * @param {function()} param.onComplete the callback to invoke on completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + onError: error => this._onLoginError(error, onError), + onCompleted: metadata => this._onLoginCompleted(metadata, onComplete) + }) + + this.write(RequestMessage.init(userAgent, authToken), observer, true) + + return observer + } + + /** + * Perform protocol related operations for closing this connection + */ + prepareToClose () { + // no need to notify the database in this protocol version + } + + /** + * Begin an explicit transaction. + * @param {Object} param + * @param {Bookmarks} param.bookmarks the bookmarks. + * @param {TxConfig} param.txConfig the configuration. + * @param {string} param.database the target database name. + * @param {string} param.mode the access mode. + * @param {string} param.impersonatedUser the impersonated user + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + return this.run( + 'BEGIN', + bookmarks ? bookmarks.asBeginTransactionParameters() : {}, + { + bookmarks: bookmarks, + txConfig: txConfig, + database, + mode, + impersonatedUser, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush: false + } + ) + } + + /** + * Commit the explicit transaction. + * @param {Object} param + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + commitTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + // WRITE access mode is used as a place holder here, it has + // no effect on behaviour for Bolt V1 & V2 + return this.run( + 'COMMIT', + {}, + { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) + } + + /** + * Rollback the explicit transaction. + * @param {Object} param + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + rollbackTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + // WRITE access mode is used as a place holder here, it has + // no effect on behaviour for Bolt V1 & V2 + return this.run( + 'ROLLBACK', + {}, + { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) + } + + /** + * Send a Cypher query through the underlying connection. + * @param {string} query the cypher query. + * @param {Object} parameters the query parameters. + * @param {Object} param + * @param {Bookmarks} param.bookmarks the bookmarks. + * @param {TxConfig} param.txConfig the transaction configuration. + * @param {string} param.database the target database name. + * @param {string} param.impersonatedUser the impersonated user + * @param {string} param.mode the access mode. + * @param {function(keys: string[])} param.beforeKeys the callback to invoke before handling the keys. + * @param {function(keys: string[])} param.afterKeys the callback to invoke after handling the keys. + * @param {function(err: Error)} param.beforeError the callback to invoke before handling the error. + * @param {function(err: Error)} param.afterError the callback to invoke after handling the error. + * @param {function()} param.beforeComplete the callback to invoke before handling the completion. + * @param {function()} param.afterComplete the callback to invoke after handling the completion. + * @param {boolean} param.flush whether to flush the buffered messages. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + // bookmarks and mode are ignored in this version of the protocol + assertTxConfigIsEmpty(txConfig, this._onProtocolError, observer) + // passing in a database name on this protocol version throws an error + assertDatabaseIsEmpty(database, this._onProtocolError, observer) + // passing impersonated user on this protocol version throws an error + assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + + this.write(RequestMessage.run(query, parameters), observer, false) + this.write(RequestMessage.pullAll(), observer, flush) + + return observer + } + + get currentFailure () { + return this._responseHandler.currentFailure + } + + /** + * Send a RESET through the underlying connection. + * @param {Object} param + * @param {function(err: Error)} param.onError the callback to invoke on error. + * @param {function()} param.onComplete the callback to invoke on completion. + * @returns {StreamObserver} the stream observer that monitors the corresponding server response. + */ + reset ({ onError, onComplete } = {}) { + const observer = new ResetObserver({ + onProtocolError: this._onProtocolError, + onError, + onComplete + }) + + this.write(RequestMessage.reset(), observer, true) + + return observer + } + + _createPacker (chunker) { + return new v1.Packer(chunker) + } + + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v1.Unpacker(disableLosslessIntegers, useBigInt) + } + + /** + * Write a message to the network channel. + * @param {RequestMessage} message the message to write. + * @param {StreamObserver} observer the response observer. + * @param {boolean} flush `true` if flush should happen after the message is written to the buffer. + */ + write (message, observer, flush) { + const queued = this.queueObserverIfProtocolIsNotBroken(observer) + + if (queued) { + if (this._log.isDebugEnabled()) { + this._log.debug(`C: ${message}`) + } + + this._lastMessageSignature = message.signature + const messageStruct = new structure.Structure(message.signature, message.fields) + + this.packable(messageStruct)() + + this._chunker.messageBoundary() + + if (flush) { + this._chunker.flush() + } + } + } + + isLastMessageLogin () { + return this._lastMessageSignature === 0x01 + } + + isLastMessageReset () { + return this._lastMessageSignature === 0x0f + } + + /** + * Notifies faltal erros to the observers and mark the protocol in the fatal error state. + * @param {Error} error The error + */ + notifyFatalError (error) { + this._fatalError = error + return this._responseHandler._notifyErrorToObservers(error) + } + + /** + * Updates the the current observer with the next one on the queue. + */ + updateCurrentObserver () { + return this._responseHandler._updateCurrentObserver() + } + + /** + * Checks if exist an ongoing observable requests + * @return {boolean} + */ + hasOngoingObservableRequests () { + return this._responseHandler.hasOngoingObservableRequests() + } + + /** + * Enqueue the observer if the protocol is not broken. + * In case it's broken, the observer will be notified about the error. + * + * @param {StreamObserver} observer The observer + * @returns {boolean} if it was queued + */ + queueObserverIfProtocolIsNotBroken (observer) { + if (this.isBroken()) { + this.notifyFatalErrorToObserver(observer) + return false + } + + return this._responseHandler._queueObserver(observer) + } + + /** + * Veritfy the protocol is not broken. + * @returns {boolean} + */ + isBroken () { + return !!this._fatalError + } + + /** + * Notifies the current fatal error to the observer + * + * @param {StreamObserver} observer The observer + */ + notifyFatalErrorToObserver (observer) { + if (observer && observer.onError) { + observer.onError(this._fatalError) + } + } + + /** + * Reset current failure on the observable response handler to null. + */ + resetFailure () { + this._responseHandler._resetFailure() + } + + _onLoginCompleted (metadata, onCompleted) { + if (metadata) { + const serverVersion = metadata.server + if (!this._server.version) { + this._server.version = serverVersion + } + } + if (onCompleted) { + onCompleted(metadata) + } + } + + _onLoginError (error, onError) { + this._onProtocolError(error.message) + if (onError) { + onError(error) + } + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js new file mode 100644 index 000000000..4756576c0 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.transformer.js @@ -0,0 +1,187 @@ +/** + * 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, + newError, + error, + Relationship, + UnboundRelationship, + Path, + toNumber, + PathSegment +} from '../../core/index.ts' + +import { structure } from '../packstream/index.js' +import { TypeTransformer } from './transformer.js' + +const { PROTOCOL_ERROR } = error + +const NODE = 0x4e +const NODE_STRUCT_SIZE = 3 + +const RELATIONSHIP = 0x52 +const RELATIONSHIP_STRUCT_SIZE = 5 + +const UNBOUND_RELATIONSHIP = 0x72 +const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 3 + +const PATH = 0x50 +const PATH_STRUCT_SIZE = 3 + +/** + * Creates the Node Transformer + * @returns {TypeTransformer} + */ +function createNodeTransformer () { + return new TypeTransformer({ + signature: NODE, + isTypeInstance: object => object instanceof Node, + toStructure: object => { + throw newError( + `It is not allowed to pass nodes in query parameters, given: ${object}`, + PROTOCOL_ERROR + ) + }, + fromStructure: struct => { + structure.verifyStructSize('Node', NODE_STRUCT_SIZE, struct.size) + + const [identity, labels, properties] = struct.fields + + return new Node(identity, labels, properties) + } + }) +} + +/** + * Creates the Relationship Transformer + * @returns {TypeTransformer} + */ +function createRelationshipTransformer () { + return new TypeTransformer({ + signature: RELATIONSHIP, + isTypeInstance: object => object instanceof Relationship, + toStructure: object => { + throw newError( + `It is not allowed to pass relationships in query parameters, given: ${object}`, + PROTOCOL_ERROR + ) + }, + fromStructure: struct => { + structure.verifyStructSize('Relationship', RELATIONSHIP_STRUCT_SIZE, struct.size) + + const [identity, startNodeIdentity, endNodeIdentity, type, properties] = struct.fields + + return new Relationship(identity, startNodeIdentity, endNodeIdentity, type, properties) + } + }) +} + +/** + * Creates the Unbound Relationship Transformer + * @returns {TypeTransformer} + */ +function createUnboundRelationshipTransformer () { + return new TypeTransformer({ + signature: UNBOUND_RELATIONSHIP, + isTypeInstance: object => object instanceof UnboundRelationship, + toStructure: object => { + throw newError( + `It is not allowed to pass unbound relationships in query parameters, given: ${object}`, + PROTOCOL_ERROR + ) + }, + fromStructure: struct => { + structure.verifyStructSize( + 'UnboundRelationship', + UNBOUND_RELATIONSHIP_STRUCT_SIZE, + struct.size + ) + + const [identity, type, properties] = struct.fields + + return new UnboundRelationship(identity, type, properties) + } + }) +} + +/** + * Creates the Path Transformer + * @returns {TypeTransformer} + */ +function createPathTransformer () { + return new TypeTransformer({ + signature: PATH, + isTypeInstance: object => object instanceof Path, + toStructure: object => { + throw newError( + `It is not allowed to pass paths in query parameters, given: ${object}`, + PROTOCOL_ERROR + ) + }, + fromStructure: struct => { + structure.verifyStructSize('Path', PATH_STRUCT_SIZE, struct.size) + + const [nodes, rels, sequence] = struct.fields + + const segments = [] + let prevNode = nodes[0] + + for (let i = 0; i < sequence.length; i += 2) { + const nextNode = nodes[sequence[i + 1]] + const relIndex = toNumber(sequence[i]) + let rel + + if (relIndex > 0) { + rel = rels[relIndex - 1] + if (rel instanceof UnboundRelationship) { + // To avoid duplication, relationships in a path do not contain + // 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.bindTo( + prevNode, + nextNode + ) + } + } else { + rel = rels[-relIndex - 1] + if (rel instanceof UnboundRelationship) { + // See above + rels[-relIndex - 1] = rel = rel.bindTo( + nextNode, + prevNode + ) + } + } + // Done hydrating one path segment. + segments.push(new PathSegment(prevNode, rel, nextNode)) + prevNode = nextNode + } + return new Path(nodes[0], nodes[nodes.length - 1], segments) + } + }) +} + +export default { + createNodeTransformer, + createRelationshipTransformer, + createUnboundRelationshipTransformer, + createPathTransformer +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js new file mode 100644 index 000000000..fb627f399 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.js @@ -0,0 +1,48 @@ +/** + * 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 BoltProtocolV1 from './bolt-protocol-v1.js' +import v2 from '../packstream/index.js' +import { internal } from '../../core/index.ts' +import transformersFactories from './bolt-protocol-v2.transformer.js' +import Transformer from './transformer.js' + +const { + constants: { BOLT_PROTOCOL_V2 } +} = internal + +export default class BoltProtocol extends BoltProtocolV1 { + _createPacker (chunker) { + return new v2.Packer(chunker) + } + + _createUnpacker (disableLosslessIntegers, useBigInt) { + return new v2.Unpacker(disableLosslessIntegers, useBigInt) + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + get version () { + return BOLT_PROTOCOL_V2 + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.transformer.js new file mode 100644 index 000000000..a8108d409 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v2.transformer.js @@ -0,0 +1,432 @@ +/** + * 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 { + isPoint, + int, + isDuration, + Duration, + isLocalDateTime, + isLocalTime, + internal, + isTime, + Time, + isDate, + isDateTime, + DateTime, + Point, + isInt +} from '../../core/index.ts' + +import { structure } from '../packstream/index.js' +import { TypeTransformer } from './transformer.js' + +import { + epochDayToDate, + nanoOfDayToLocalTime, + epochSecondAndNanoToLocalDateTime +} from './temporal-factory.js' + +import v1 from './bolt-protocol-v1.transformer.js' + +const { + temporalUtil: { + dateToEpochDay, + localDateTimeToEpochSecond, + localTimeToNanoOfDay + } +} = internal + +const POINT_2D = 0x58 +const POINT_2D_STRUCT_SIZE = 3 + +const POINT_3D = 0x59 +const POINT_3D_STRUCT_SIZE = 4 + +const DURATION = 0x45 +const DURATION_STRUCT_SIZE = 4 + +const LOCAL_TIME = 0x74 +const LOCAL_TIME_STRUCT_SIZE = 1 + +const TIME = 0x54 +const TIME_STRUCT_SIZE = 2 + +const DATE = 0x44 +const DATE_STRUCT_SIZE = 1 + +const LOCAL_DATE_TIME = 0x64 +const LOCAL_DATE_TIME_STRUCT_SIZE = 2 + +const DATE_TIME_WITH_ZONE_OFFSET = 0x46 +const DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE = 3 + +const DATE_TIME_WITH_ZONE_ID = 0x66 +const DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE = 3 + +/** + * Creates the Point2D Transformer + * @returns {TypeTransformer} + */ +function createPoint2DTransformer () { + return new TypeTransformer({ + signature: POINT_2D, + isTypeInstance: point => isPoint(point) && (point.z === null || point.z === undefined), + toStructure: point => new structure.Structure(POINT_2D, [ + int(point.srid), + point.x, + point.y + ]), + fromStructure: struct => { + structure.verifyStructSize('Point2D', POINT_2D_STRUCT_SIZE, struct.size) + + const [srid, x, y] = struct.fields + return new Point( + srid, + x, + y, + undefined // z + ) + } + }) +} + +/** + * Creates the Point3D Transformer + * @returns {TypeTransformer} + */ +function createPoint3DTransformer () { + return new TypeTransformer({ + signature: POINT_3D, + isTypeInstance: point => isPoint(point) && point.z !== null && point.z !== undefined, + toStructure: point => new structure.Structure(POINT_3D, [ + int(point.srid), + point.x, + point.y, + point.z + ]), + fromStructure: struct => { + structure.verifyStructSize('Point3D', POINT_3D_STRUCT_SIZE, struct.size) + + const [srid, x, y, z] = struct.fields + return new Point( + srid, + x, + y, + z + ) + } + }) +} + +/** + * Creates the Duration Transformer + * @returns {TypeTransformer} + */ +function createDurationTransformer () { + return new TypeTransformer({ + signature: DURATION, + isTypeInstance: isDuration, + toStructure: value => { + const months = int(value.months) + const days = int(value.days) + const seconds = int(value.seconds) + const nanoseconds = int(value.nanoseconds) + + return new structure.Structure(DURATION, [months, days, seconds, nanoseconds]) + }, + fromStructure: struct => { + structure.verifyStructSize('Duration', DURATION_STRUCT_SIZE, struct.size) + + const [months, days, seconds, nanoseconds] = struct.fields + + return new Duration(months, days, seconds, nanoseconds) + } + }) +} + +/** + * Creates the LocalTime Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createLocalTimeTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: LOCAL_TIME, + isTypeInstance: isLocalTime, + toStructure: value => { + const nanoOfDay = localTimeToNanoOfDay( + value.hour, + value.minute, + value.second, + value.nanosecond + ) + + return new structure.Structure(LOCAL_TIME, [nanoOfDay]) + }, + fromStructure: struct => { + structure.verifyStructSize('LocalTime', LOCAL_TIME_STRUCT_SIZE, struct.size) + + const [nanoOfDay] = struct.fields + const result = nanoOfDayToLocalTime(nanoOfDay) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + } + }) +} + +/** + * Creates the Time Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createTimeTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: TIME, + isTypeInstance: isTime, + toStructure: value => { + const nanoOfDay = localTimeToNanoOfDay( + value.hour, + value.minute, + value.second, + value.nanosecond + ) + const offsetSeconds = int(value.timeZoneOffsetSeconds) + + return new structure.Structure(TIME, [nanoOfDay, offsetSeconds]) + }, + fromStructure: struct => { + structure.verifyStructSize('Time', TIME_STRUCT_SIZE, struct.size) + + const [nanoOfDay, offsetSeconds] = struct.fields + const localTime = nanoOfDayToLocalTime(nanoOfDay) + const result = new Time( + localTime.hour, + localTime.minute, + localTime.second, + localTime.nanosecond, + offsetSeconds + ) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + } + }) +} + +/** + * Creates the Date Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createDateTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: DATE, + isTypeInstance: isDate, + toStructure: value => { + const epochDay = dateToEpochDay(value.year, value.month, value.day) + + return new structure.Structure(DATE, [epochDay]) + }, + fromStructure: struct => { + structure.verifyStructSize('Date', DATE_STRUCT_SIZE, struct.size) + + const [epochDay] = struct.fields + const result = epochDayToDate(epochDay) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + } + }) +} + +/** + * Creates the LocalDateTime Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createLocalDateTimeTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: LOCAL_DATE_TIME, + isTypeInstance: isLocalDateTime, + toStructure: value => { + const epochSecond = localDateTimeToEpochSecond( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.nanosecond + ) + const nano = int(value.nanosecond) + + return new structure.Structure(LOCAL_DATE_TIME, [epochSecond, nano]) + }, + fromStructure: struct => { + structure.verifyStructSize( + 'LocalDateTime', + LOCAL_DATE_TIME_STRUCT_SIZE, + struct.size + ) + + const [epochSecond, nano] = struct.fields + const result = epochSecondAndNanoToLocalDateTime(epochSecond, nano) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + } + }) +} + +/** + * Creates the DateTime with ZoneId Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createDateTimeWithZoneIdTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: DATE_TIME_WITH_ZONE_ID, + isTypeInstance: object => isDateTime(object) && object.timeZoneId != null, + 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 timeZoneId = value.timeZoneId + + return new structure.Structure(DATE_TIME_WITH_ZONE_ID, [epochSecond, nano, timeZoneId]) + }, + fromStructure: struct => { + structure.verifyStructSize( + 'DateTimeWithZoneId', + DATE_TIME_WITH_ZONE_ID_STRUCT_SIZE, + struct.size + ) + + const [epochSecond, nano, timeZoneId] = struct.fields + + const localDateTime = epochSecondAndNanoToLocalDateTime(epochSecond, nano) + const result = new DateTime( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + localDateTime.nanosecond, + null, + timeZoneId + ) + return convertIntegerPropsIfNeeded(result, disableLosslessIntegers, useBigInt) + } + }) +} + +/** + * Creates the DateTime with Offset Transformer + * @param {Object} param + * @param {boolean} param.disableLosslessIntegers Disables lossless integers + * @param {boolean} param.useBigInt Uses BigInt instead of number or Integer + * @returns {TypeTransformer} + */ +function createDateTimeWithOffsetTransformer ({ disableLosslessIntegers, useBigInt }) { + return new TypeTransformer({ + signature: DATE_TIME_WITH_ZONE_OFFSET, + isTypeInstance: object => isDateTime(object) && object.timeZoneId == null, + 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) + return new structure.Structure(DATE_TIME_WITH_ZONE_OFFSET, [epochSecond, nano, timeZoneOffsetSeconds]) + }, + fromStructure: struct => { + structure.verifyStructSize( + 'DateTimeWithZoneOffset', + DATE_TIME_WITH_ZONE_OFFSET_STRUCT_SIZE, + struct.size + ) + + const [epochSecond, nano, timeZoneOffsetSeconds] = struct.fields + + 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 { + ...v1, + createPoint2DTransformer, + createPoint3DTransformer, + createDurationTransformer, + createLocalTimeTransformer, + createTimeTransformer, + createDateTransformer, + createLocalDateTimeTransformer, + createDateTimeWithZoneIdTransformer, + createDateTimeWithOffsetTransformer +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js new file mode 100644 index 000000000..57b5632ae --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.js @@ -0,0 +1,246 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV2 from './bolt-protocol-v2.js' +import RequestMessage from './request-message.js' +import { assertDatabaseIsEmpty, assertImpersonatedUserIsEmpty } from './bolt-protocol-util.js' +import { + StreamObserver, + LoginObserver, + ResultStreamObserver, + ProcedureRouteObserver +} from './stream-observers.js' +import transformersFactories from './bolt-protocol-v3.transformer.js' +import Transformer from './transformer.js' +import { internal } from '../../core/index.ts' + +const { + // eslint-disable-next-line no-unused-vars + bookmarks: { Bookmarks }, + constants: { BOLT_PROTOCOL_V3 }, + txConfig: { TxConfig } +} = internal + +const CONTEXT = 'context' +const CALL_GET_ROUTING_TABLE = `CALL dbms.cluster.routing.getRoutingTable($${CONTEXT})` + +const noOpObserver = new StreamObserver() + +export default class BoltProtocol extends BoltProtocolV2 { + get version () { + return BOLT_PROTOCOL_V3 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + transformMetadata (metadata) { + if ('t_first' in metadata) { + // Bolt V3 uses shorter key 't_first' to represent 'result_available_after' + // adjust the key to be the same as in Bolt V1 so that ResultSummary can retrieve the value + metadata.result_available_after = metadata.t_first + delete metadata.t_first + } + if ('t_last' in metadata) { + // Bolt V3 uses shorter key 't_last' to represent 'result_consumed_after' + // adjust the key to be the same as in Bolt V1 so that ResultSummary can retrieve the value + metadata.result_consumed_after = metadata.t_last + delete metadata.t_last + } + return metadata + } + + 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), observer, true) + + return observer + } + + prepareToClose () { + this.write(RequestMessage.goodbye(), noOpObserver, true) + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + impersonatedUser, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + // passing in a database name on this protocol version throws an error + assertDatabaseIsEmpty(database, this._onProtocolError, observer) + // passing impersonated user on this protocol version throws an error + assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + + this.write( + RequestMessage.begin({ bookmarks, txConfig, mode }), + observer, + true + ) + + return observer + } + + commitTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this.write(RequestMessage.commit(), observer, true) + + return observer + } + + rollbackTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this.write(RequestMessage.rollback(), observer, true) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + impersonatedUser, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + // passing in a database name on this protocol version throws an error + assertDatabaseIsEmpty(database, this._onProtocolError, observer) + // passing impersonated user on this protocol version throws an error + assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + + this.write( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + mode + }), + observer, + false + ) + this.write(RequestMessage.pullAll(), observer, flush) + + return observer + } + + /** + * Request routing information + * + * @param {Object} param - + * @param {object} param.routingContext The routing context used to define the routing table. + * Multi-datacenter deployments is one of its use cases + * @param {string} param.databaseName The database name + * @param {Bookmarks} params.sessionContext.bookmarks The bookmarks used for requesting the routing table + * @param {string} params.sessionContext.mode The session mode + * @param {string} params.sessionContext.database The database name used on the session + * @param {function()} params.sessionContext.afterComplete The session param used after the session closed + * @param {function(err: Error)} param.onError + * @param {function(RawRoutingTable)} param.onCompleted + * @returns {RouteObserver} the route observer + */ + requestRoutingInformation ({ + routingContext = {}, + sessionContext = {}, + onError, + onCompleted + }) { + const resultObserver = this.run( + CALL_GET_ROUTING_TABLE, + { [CONTEXT]: routingContext }, + { ...sessionContext, txConfig: TxConfig.empty() } + ) + + return new ProcedureRouteObserver({ + resultObserver, + onProtocolError: this._onProtocolError, + onError, + onCompleted + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.transformer.js new file mode 100644 index 000000000..c8d9a407f --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v3.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v2 from './bolt-protocol-v2.transformer.js' + +export default { + ...v2 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js new file mode 100644 index 000000000..a9eb5c856 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.js @@ -0,0 +1,194 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import BoltProtocolV3 from './bolt-protocol-v3.js' +import RequestMessage from './request-message.js' +import { assertImpersonatedUserIsEmpty } from './bolt-protocol-util.js' +import { + ResultStreamObserver, + ProcedureRouteObserver +} from './stream-observers.js' +import transformersFactories from './bolt-protocol-v4x0.transformer.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + // eslint-disable-next-line no-unused-vars + bookmarks: { Bookmarks }, + constants: { BOLT_PROTOCOL_V4_0, FETCH_ALL }, + txConfig: { TxConfig } +} = internal + +const CONTEXT = 'context' +const DATABASE = 'database' +const CALL_GET_ROUTING_TABLE_MULTI_DB = `CALL dbms.routing.getRoutingTable($${CONTEXT}, $${DATABASE})` + +export default class BoltProtocol extends BoltProtocolV3 { + get version () { + return BOLT_PROTOCOL_V4_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + impersonatedUser, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + // passing impersonated user on this protocol version throws an error + assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + + this.write( + RequestMessage.begin({ bookmarks, txConfig, database, mode }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + impersonatedUser, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + reactive = false, + fetchSize = FETCH_ALL, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + reactive: reactive, + fetchSize: fetchSize, + moreFunction: this._requestMore.bind(this), + discardFunction: this._requestDiscard.bind(this), + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + // passing impersonated user on this protocol version throws an error + assertImpersonatedUserIsEmpty(impersonatedUser, this._onProtocolError, observer) + + const flushRun = reactive + this.write( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } + + _requestMore (stmtId, n, observer) { + this.write(RequestMessage.pull({ stmtId, n }), observer, true) + } + + _requestDiscard (stmtId, observer) { + this.write(RequestMessage.discard({ stmtId }), observer, true) + } + + _noOp () {} + + /** + * Request routing information + * + * @param {Object} param - + * @param {object} param.routingContext The routing context used to define the routing table. + * Multi-datacenter deployments is one of its use cases + * @param {string} param.databaseName The database name + * @param {Bookmarks} params.sessionContext.bookmarks The bookmarks used for requesting the routing table + * @param {string} params.sessionContext.mode The session mode + * @param {string} params.sessionContext.database The database name used on the session + * @param {function()} params.sessionContext.afterComplete The session param used after the session closed + * @param {function(err: Error)} param.onError + * @param {function(RawRoutingTable)} param.onCompleted + * @returns {RouteObserver} the route observer + */ + requestRoutingInformation ({ + routingContext = {}, + databaseName = null, + sessionContext = {}, + onError, + onCompleted + }) { + const resultObserver = this.run( + CALL_GET_ROUTING_TABLE_MULTI_DB, + { + [CONTEXT]: routingContext, + [DATABASE]: databaseName + }, + { ...sessionContext, txConfig: TxConfig.empty() } + ) + + return new ProcedureRouteObserver({ + resultObserver, + onProtocolError: this._onProtocolError, + onError, + onCompleted + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.transformer.js new file mode 100644 index 000000000..967308414 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x0.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v3 from './bolt-protocol-v3.transformer.js' + +export default { + ...v3 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js new file mode 100644 index 000000000..001f41e68 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.js @@ -0,0 +1,89 @@ +/** + * 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 BoltProtocolV4 from './bolt-protocol-v4x0.js' +import RequestMessage from './request-message.js' +import { LoginObserver } from './stream-observers.js' +import { internal } from '../../core/index.ts' + +import transformersFactories from './bolt-protocol-v4x1.transformer.js' +import Transformer from './transformer.js' + +const { + constants: { BOLT_PROTOCOL_V4_1 } +} = internal + +export default class BoltProtocol extends BoltProtocolV4 { + /** + * @constructor + * @param {Object} server the server informatio. + * @param {Chunker} chunker the chunker. + * @param {Object} packstreamConfig Packstream configuration + * @param {boolean} packstreamConfig.disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {CreateResponseHandler} createResponseHandler Function which creates the response handler + * @param {Logger} log the logger + * @param {Object} serversideRouting + * + */ + constructor ( + server, + chunker, + packstreamConfig, + createResponseHandler = () => null, + log, + onProtocolError, + serversideRouting + ) { + super( + server, + chunker, + packstreamConfig, + createResponseHandler, + log, + onProtocolError + ) + this._serversideRouting = serversideRouting + } + + get version () { + return BOLT_PROTOCOL_V4_1 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + 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/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.transformer.js new file mode 100644 index 000000000..7b3f46e6c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x1.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v4x0 from './bolt-protocol-v4x0.transformer.js' + +export default { + ...v4x0 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.js new file mode 100644 index 000000000..176de4db0 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.js @@ -0,0 +1,41 @@ +/** + * 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 BoltProtocolV41 from './bolt-protocol-v4x1.js' + +import { internal } from '../../core/index.ts' + +import transformersFactories from './bolt-protocol-v4x2.transformer.js' +import Transformer from './transformer.js' + +const { + constants: { BOLT_PROTOCOL_V4_2 } +} = internal + +export default class BoltProtocol extends BoltProtocolV41 { + get version () { + return BOLT_PROTOCOL_V4_2 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.transformer.js new file mode 100644 index 000000000..c4aa19bac --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x2.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v4x1 from './bolt-protocol-v4x1.transformer.js' + +export default { + ...v4x1 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js new file mode 100644 index 000000000..86d62a1f1 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.js @@ -0,0 +1,126 @@ +/** + * 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 BoltProtocolV42 from './bolt-protocol-v4x2.js' +import RequestMessage from './request-message.js' +import { LoginObserver, RouteObserver } from './stream-observers.js' + +import transformersFactories from './bolt-protocol-v4x3.transformer.js' +import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' +import Transformer from './transformer.js' + +import { internal } from '../../core/index.ts' + +const { + bookmarks: { Bookmarks }, + constants: { BOLT_PROTOCOL_V4_3 } +} = internal + +export default class BoltProtocol extends BoltProtocolV42 { + get version () { + return BOLT_PROTOCOL_V4_3 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Request routing information + * + * @param {Object} param - + * @param {object} param.routingContext The routing context used to define the routing table. + * Multi-datacenter deployments is one of its use cases + * @param {string} param.databaseName The database name + * @param {Bookmarks} params.sessionContext.bookmarks The bookmarks used for requesting the routing table + * @param {function(err: Error)} param.onError + * @param {function(RawRoutingTable)} param.onCompleted + * @returns {RouteObserver} the route observer + */ + requestRoutingInformation ({ + routingContext = {}, + databaseName = null, + sessionContext = {}, + onError, + onCompleted + }) { + const observer = new RouteObserver({ + onProtocolError: this._onProtocolError, + onError, + onCompleted + }) + const bookmarks = sessionContext.bookmarks || Bookmarks.empty() + this.write( + RequestMessage.route(routingContext, bookmarks.values(), databaseName), + observer, + true + ) + + 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/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.transformer.js new file mode 100644 index 000000000..c98fef489 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x3.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v4x2 from './bolt-protocol-v4x2.transformer.js' + +export default { + ...v4x2 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js new file mode 100644 index 000000000..22d95f52b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.js @@ -0,0 +1,174 @@ +/** + * 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 BoltProtocolV43 from './bolt-protocol-v4x3.js' + +import { internal } from '../../core/index.ts' +import RequestMessage from './request-message.js' +import { RouteObserver, ResultStreamObserver } from './stream-observers.js' + +import transformersFactories from './bolt-protocol-v4x4.transformer.js' +import utcTransformersFactories from './bolt-protocol-v5x0.utc.transformer.js' +import Transformer from './transformer.js' + +const { + constants: { BOLT_PROTOCOL_V4_4, FETCH_ALL }, + bookmarks: { Bookmarks } +} = internal + +export default class BoltProtocol extends BoltProtocolV43 { + get version () { + return BOLT_PROTOCOL_V4_4 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + return this._transformer + } + + /** + * Request routing information + * + * @param {Object} param - + * @param {object} param.routingContext The routing context used to define the routing table. + * Multi-datacenter deployments is one of its use cases + * @param {string} param.databaseName The database name + * @param {Bookmarks} params.sessionContext.bookmarks The bookmarks used for requesting the routing table + * @param {function(err: Error)} param.onError + * @param {function(RawRoutingTable)} param.onCompleted + * @returns {RouteObserver} the route observer + */ + requestRoutingInformation ({ + routingContext = {}, + databaseName = null, + impersonatedUser = null, + sessionContext = {}, + onError, + onCompleted + }) { + const observer = new RouteObserver({ + onProtocolError: this._onProtocolError, + onError, + onCompleted + }) + const bookmarks = sessionContext.bookmarks || Bookmarks.empty() + this.write( + RequestMessage.routeV4x4(routingContext, bookmarks.values(), { databaseName, impersonatedUser }), + observer, + true + ) + + return observer + } + + run ( + query, + parameters, + { + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true, + reactive = false, + fetchSize = FETCH_ALL, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {} + ) { + const observer = new ResultStreamObserver({ + server: this._server, + reactive: reactive, + fetchSize: fetchSize, + moreFunction: this._requestMore.bind(this), + discardFunction: this._requestDiscard.bind(this), + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + highRecordWatermark, + lowRecordWatermark + }) + + const flushRun = reactive + this.write( + RequestMessage.runWithMetadata(query, parameters, { + bookmarks, + txConfig, + database, + mode, + impersonatedUser + }), + observer, + flushRun && flush + ) + + if (!reactive) { + this.write(RequestMessage.pull({ n: fetchSize }), observer, flush) + } + + return observer + } + + beginTransaction ({ + bookmarks, + txConfig, + database, + mode, + impersonatedUser, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + server: this._server, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this.write( + RequestMessage.begin({ bookmarks, txConfig, database, mode, impersonatedUser }), + observer, + true + ) + + return observer + } + + _applyUtcPatch () { + this._transformer = new Transformer(Object.values({ + ...transformersFactories, + ...utcTransformersFactories + }).map(create => create(this._config, this._log))) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.transformer.js new file mode 100644 index 000000000..190439f85 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v4x4.transformer.js @@ -0,0 +1,24 @@ +/** + * 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 v4x3 from './bolt-protocol-v4x3.transformer.js' + +export default { + ...v4x3 +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js new file mode 100644 index 000000000..4e30ccb0b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.js @@ -0,0 +1,68 @@ +/** + * 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.js' + +import transformersFactories from './bolt-protocol-v5x0.transformer.js' +import Transformer from './transformer.js' +import RequestMessage from './request-message.js' +import { LoginObserver } from './stream-observers.js' + +import { internal } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V5_0 } +} = internal + +export default class BoltProtocol extends BoltProtocolV44 { + get version () { + return BOLT_PROTOCOL_V5_0 + } + + get transformer () { + if (this._transformer === undefined) { + this._transformer = new Transformer(Object.values(transformersFactories).map(create => create(this._config, this._log))) + } + 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/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.transformer.js new file mode 100644 index 000000000..9527cc96a --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.transformer.js @@ -0,0 +1,131 @@ +/** + * 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/index.js' +import { + Node, + Relationship, + UnboundRelationship +} from '../../core/index.ts' + +import v4x4 from './bolt-protocol-v4x4.transformer.js' +import v5x0Utc from './bolt-protocol-v5x0.utc.transformer.js' + +const NODE_STRUCT_SIZE = 4 +const RELATIONSHIP_STRUCT_SIZE = 8 +const UNBOUND_RELATIONSHIP_STRUCT_SIZE = 4 + +/** + * Create an extend Node transformer with support to elementId + * @param {any} config + * @returns {TypeTransformer} + */ +function createNodeTransformer (config) { + const node4x4Transformer = v4x4.createNodeTransformer(config) + return node4x4Transformer.extendsWith({ + fromStructure: struct => { + structure.verifyStructSize('Node', NODE_STRUCT_SIZE, struct.size) + + const [identity, lables, properties, elementId] = struct.fields + + return new Node( + identity, + lables, + properties, + elementId + ) + } + }) +} + +/** + * Create an extend Relationship transformer with support to elementId + * @param {any} config + * @returns {TypeTransformer} + */ +function createRelationshipTransformer (config) { + const relationship4x4Transformer = v4x4.createRelationshipTransformer(config) + return relationship4x4Transformer.extendsWith({ + fromStructure: struct => { + structure.verifyStructSize('Relationship', RELATIONSHIP_STRUCT_SIZE, struct.size) + + const [ + identity, + startNodeIdentity, + endNodeIdentity, + type, + properties, + elementId, + startNodeElementId, + endNodeElementId + ] = struct.fields + + return new Relationship( + identity, + startNodeIdentity, + endNodeIdentity, + type, + properties, + elementId, + startNodeElementId, + endNodeElementId + ) + } + }) +} + +/** + * Create an extend Unbound Relationship transformer with support to elementId + * @param {any} config + * @returns {TypeTransformer} + */ +function createUnboundRelationshipTransformer (config) { + const unboundRelationshipTransformer = v4x4.createUnboundRelationshipTransformer(config) + return unboundRelationshipTransformer.extendsWith({ + fromStructure: struct => { + structure.verifyStructSize( + 'UnboundRelationship', + UNBOUND_RELATIONSHIP_STRUCT_SIZE, + struct.size + ) + + const [ + identity, + type, + properties, + elementId + ] = struct.fields + + return new UnboundRelationship( + identity, + type, + properties, + elementId + ) + } + }) +} + +export default { + ...v4x4, + ...v5x0Utc, + createNodeTransformer, + createRelationshipTransformer, + createUnboundRelationshipTransformer +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.utc.transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.utc.transformer.js new file mode 100644 index 000000000..821ba47bf --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v5x0.utc.transformer.js @@ -0,0 +1,281 @@ +/** + * 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/index.js' +import { + DateTime, + isInt, + int, + internal +} from '../../core/index.ts' + +import v4x4 from './bolt-protocol-v4x4.transformer.js' + +import { + epochSecondAndNanoToLocalDateTime +} from './temporal-factory.js' +import { identity } from '../lang/functional.js' + +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, logger) { + 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), + localDateTime.timeZoneOffsetSeconds, + 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 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) + const timeZoneId = value.timeZoneId + + return new structure.Structure(DATE_TIME_WITH_ZONE_ID, [utc, nano, timeZoneId]) + } + }) +} + +/** + * Returns the offset for a given timezone id + * + * Javascript doesn't have support for direct getting the timezone offset from a given + * TimeZoneId and DateTime in the given TimeZoneId. For solving this issue, + * + * 1. The ZoneId is applied to the timestamp, so we could make the difference between the + * given timestamp and the new calculated one. This is the offset for the timezone + * in the utc is equal to epoch (some time in the future or past) + * 2. The offset is subtracted from the timestamp, so we have an estimated utc timestamp. + * 3. The ZoneId is applied to the new timestamp, se we could could make the difference + * between the new timestamp and the calculated one. This is the offset for the given timezone. + * + * Example: + * Input: 2022-3-27 1:59:59 'Europe/Berlin' + * Apply 1, 2022-3-27 1:59:59 => 2022-3-27 3:59:59 'Europe/Berlin' +2:00 + * Apply 2, 2022-3-27 1:59:59 - 2:00 => 2022-3-26 23:59:59 + * Apply 3, 2022-3-26 23:59:59 => 2022-3-27 00:59:59 'Europe/Berlin' +1:00 + * The offset is +1 hour. + * + * @param {string} timeZoneId The timezone id + * @param {Integer} epochSecond The epoch second in the timezone id + * @param {Integerable} nanosecond The nanoseconds in the timezone id + * @returns The timezone offset + */ +function getOffsetFromZoneId (timeZoneId, epochSecond, nanosecond) { + const dateTimeWithZoneAppliedTwice = getTimeInZoneId(timeZoneId, epochSecond, nanosecond) + + // The wallclock form the current date time + const epochWithZoneAppliedTwice = localDateTimeToEpochSecond( + dateTimeWithZoneAppliedTwice.year, + dateTimeWithZoneAppliedTwice.month, + dateTimeWithZoneAppliedTwice.day, + dateTimeWithZoneAppliedTwice.hour, + dateTimeWithZoneAppliedTwice.minute, + dateTimeWithZoneAppliedTwice.second, + nanosecond) + + const offsetOfZoneInTheFutureUtc = epochWithZoneAppliedTwice.subtract(epochSecond) + const guessedUtc = epochSecond.subtract(offsetOfZoneInTheFutureUtc) + + const zonedDateTimeFromGuessedUtc = getTimeInZoneId(timeZoneId, guessedUtc, nanosecond) + + const zonedEpochFromGuessedUtc = localDateTimeToEpochSecond( + zonedDateTimeFromGuessedUtc.year, + zonedDateTimeFromGuessedUtc.month, + zonedDateTimeFromGuessedUtc.day, + zonedDateTimeFromGuessedUtc.hour, + zonedDateTimeFromGuessedUtc.minute, + zonedDateTimeFromGuessedUtc.second, + nanosecond) + + const offset = zonedEpochFromGuessedUtc.subtract(guessedUtc) + return offset +} + +function getTimeInZoneId (timeZoneId, epochSecond, nano) { + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: timeZoneId, + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + era: 'narrow' + }) + + const utc = int(epochSecond) + .multiply(1000) + .add(int(nano).div(1_000_000)) + .toNumber() + + const formattedUtcParts = formatter.formatToParts(utc) + + const localDateTime = formattedUtcParts.reduce((obj, currentValue) => { + if (currentValue.type === 'era') { + obj.adjustEra = + currentValue.value.toUpperCase() === 'B' + ? year => year.subtract(1).negate() // 1BC equals to year 0 in astronomical year numbering + : identity + } else if (currentValue.type !== 'literal') { + obj[currentValue.type] = int(currentValue.value) + } + return obj + }, {}) + + localDateTime.year = localDateTime.adjustEra(localDateTime.year) + + const epochInTimeZone = localDateTimeToEpochSecond( + localDateTime.year, + localDateTime.month, + localDateTime.day, + localDateTime.hour, + localDateTime.minute, + localDateTime.second, + localDateTime.nanosecond + ) + + localDateTime.timeZoneOffsetSeconds = epochInTimeZone.subtract(epochSecond) + localDateTime.hour = localDateTime.hour.modulo(24) + + return localDateTime +} + +function 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/neo4j-driver-deno/lib/bolt-connection/bolt/create.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js new file mode 100644 index 000000000..9a4549cd6 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/create.js @@ -0,0 +1,197 @@ +/** + * 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 { newError } from '../../core/index.ts' +import BoltProtocolV1 from './bolt-protocol-v1.js' +import BoltProtocolV2 from './bolt-protocol-v2.js' +import BoltProtocolV3 from './bolt-protocol-v3.js' +import BoltProtocolV4x0 from './bolt-protocol-v4x0.js' +import BoltProtocolV4x1 from './bolt-protocol-v4x1.js' +import BoltProtocolV4x2 from './bolt-protocol-v4x2.js' +import BoltProtocolV4x3 from './bolt-protocol-v4x3.js' +import BoltProtocolV4x4 from './bolt-protocol-v4x4.js' +import BoltProtocolV5x0 from './bolt-protocol-v5x0.js' +// eslint-disable-next-line no-unused-vars +import { Chunker, Dechunker } from '../channel/index.js' +import ResponseHandler from './response-handler.js' + +/** + * Creates a protocol with a given version + * + * @param {object} config + * @param {number} config.version The version of the protocol + * @param {channel} config.channel The channel + * @param {Chunker} config.chunker The chunker + * @param {Dechunker} config.dechunker The dechunker + * @param {Logger} config.log The logger + * @param {ResponseHandler~Observer} config.observer Observer + * @param {boolean} config.disableLosslessIntegers Disable the lossless integers + * @param {boolean} packstreamConfig.useBigInt if this connection should convert all received integers to native BigInt numbers. + * @param {boolean} config.serversideRouting It's using server side routing + */ +export default function create ({ + version, + chunker, + dechunker, + channel, + disableLosslessIntegers, + useBigInt, + serversideRouting, + server, // server info + log, + observer +} = {}) { + const createResponseHandler = protocol => { + const responseHandler = new ResponseHandler({ + transformMetadata: protocol.transformMetadata.bind(protocol), + log, + observer + }) + + // reset the error handler to just handle errors and forget about the handshake promise + channel.onerror = observer.onError.bind(observer) + + // Ok, protocol running. Simply forward all messages to the dechunker + channel.onmessage = buf => dechunker.write(buf) + + // setup dechunker to dechunk messages and forward them to the message handler + dechunker.onmessage = buf => { + try { + responseHandler.handleResponse(protocol.unpack(buf)) + } catch (e) { + return observer.onError(e) + } + } + + return responseHandler + } + + return createProtocol( + version, + server, + chunker, + { disableLosslessIntegers, useBigInt }, + serversideRouting, + createResponseHandler, + observer.onProtocolError.bind(observer), + log + ) +} + +function createProtocol ( + version, + server, + chunker, + packingConfig, + serversideRouting, + createResponseHandler, + onProtocolError, + log +) { + switch (version) { + case 1: + return new BoltProtocolV1( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError + ) + case 2: + return new BoltProtocolV2( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError + ) + case 3: + return new BoltProtocolV3( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError + ) + case 4.0: + return new BoltProtocolV4x0( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError + ) + case 4.1: + return new BoltProtocolV4x1( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) + case 4.2: + return new BoltProtocolV4x2( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) + case 4.3: + return new BoltProtocolV4x3( + server, + chunker, + packingConfig, + createResponseHandler, + log, + onProtocolError, + serversideRouting + ) + case 4.4: + return new BoltProtocolV4x4( + server, + chunker, + packingConfig, + createResponseHandler, + log, + 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/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js new file mode 100644 index 000000000..515458ee5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/handshake.js @@ -0,0 +1,133 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { alloc } from '../channel/index.js' +import { newError } from '../../core/index.ts' + +const BOLT_MAGIC_PREAMBLE = 0x6060b017 + +function version (major, minor) { + return { + major, + minor + } +} + +function createHandshakeMessage (versions) { + if (versions.length > 4) { + throw newError('It should not have more than 4 versions of the protocol') + } + const handshakeBuffer = alloc(5 * 4) + + handshakeBuffer.writeInt32(BOLT_MAGIC_PREAMBLE) + + versions.forEach(version => { + if (version instanceof Array) { + const { major, minor } = version[0] + const { minor: minMinor } = version[1] + const range = minor - minMinor + handshakeBuffer.writeInt32((range << 16) | (minor << 8) | major) + } else { + const { major, minor } = version + handshakeBuffer.writeInt32((minor << 8) | major) + } + }) + + handshakeBuffer.reset() + + return handshakeBuffer +} + +function parseNegotiatedResponse (buffer) { + const h = [ + buffer.readUInt8(), + buffer.readUInt8(), + buffer.readUInt8(), + buffer.readUInt8() + ] + if (h[0] === 0x48 && h[1] === 0x54 && h[2] === 0x54 && h[3] === 0x50) { + throw newError( + 'Server responded HTTP. Make sure you are not trying to connect to the http endpoint ' + + '(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)' + ) + } + return Number(h[3] + '.' + h[2]) +} + +/** + * @return {BaseBuffer} + * @private + */ +function newHandshakeBuffer () { + return createHandshakeMessage([ + version(5, 0), + [version(4, 4), version(4, 2)], + version(4, 1), + version(3, 0) + ]) +} + +/** + * This callback is displayed as a global member. + * @callback BufferConsumerCallback + * @param {buffer} buffer the remaining buffer + */ +/** + * @typedef HandshakeResult + * @property {number} protocolVersion The protocol version negotiated in the handshake + * @property {function(BufferConsumerCallback)} consumeRemainingBuffer A function to consume the remaining buffer if it exists + */ +/** + * Shake hands using the channel and return the protocol version + * + * @param {Channel} channel the channel use to shake hands + * @returns {Promise} Promise of protocol version and consumeRemainingBuffer + */ +export default function handshake (channel) { + return new Promise((resolve, reject) => { + const handshakeErrorHandler = error => { + reject(error) + } + + channel.onerror = handshakeErrorHandler.bind(this) + if (channel._error) { + handshakeErrorHandler(channel._error) + } + + channel.onmessage = buffer => { + try { + // read the response buffer and initialize the protocol + const protocolVersion = parseNegotiatedResponse(buffer) + + resolve({ + protocolVersion, + consumeRemainingBuffer: consumer => { + if (buffer.hasRemaining()) { + consumer(buffer.readSlice(buffer.remaining())) + } + } + }) + } catch (e) { + reject(e) + } + } + + channel.write(newHandshakeBuffer()) + }) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/index.js new file mode 100644 index 000000000..ff230b5af --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/index.js @@ -0,0 +1,32 @@ +/** + * 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 handshake from './handshake.js' +import create from './create.js' +import _BoltProtocol from './bolt-protocol-v4x3.js' +import _RawRoutingTable from './routing-table-raw.js' + +export * from './stream-observers.js' + +export const BoltProtocol = _BoltProtocol +export const RawRoutingTable = _RawRoutingTable + +export default { + handshake, + create +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js new file mode 100644 index 000000000..0ceb430c0 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/request-message.js @@ -0,0 +1,329 @@ +/** + * 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, internal, json } from '../../core/index.ts' + +const { + constants: { ACCESS_MODE_READ, FETCH_ALL }, + util: { assertString } +} = internal + +/* eslint-disable no-unused-vars */ +// Signature bytes for each request message type +const INIT = 0x01 // 0000 0001 // INIT +const ACK_FAILURE = 0x0e // 0000 1110 // ACK_FAILURE - unused +const RESET = 0x0f // 0000 1111 // RESET +const RUN = 0x10 // 0001 0000 // RUN +const DISCARD_ALL = 0x2f // 0010 1111 // DISCARD_ALL - unused +const PULL_ALL = 0x3f // 0011 1111 // PULL_ALL + +const HELLO = 0x01 // 0000 0001 // HELLO +const GOODBYE = 0x02 // 0000 0010 // GOODBYE +const BEGIN = 0x11 // 0001 0001 // BEGIN +const COMMIT = 0x12 // 0001 0010 // COMMIT +const ROLLBACK = 0x13 // 0001 0011 // ROLLBACK +const ROUTE = 0x66 // 0110 0110 // ROUTE + +const DISCARD = 0x2f // 0010 1111 // DISCARD +const PULL = 0x3f // 0011 1111 // PULL + +const READ_MODE = 'r' +/* eslint-enable no-unused-vars */ + +const NO_STATEMENT_ID = -1 + +export default class RequestMessage { + constructor (signature, fields, toString) { + this.signature = signature + this.fields = fields + this.toString = toString + } + + /** + * Create a new INIT message. + * @param {string} clientName the client name. + * @param {Object} authToken the authentication token. + * @return {RequestMessage} new INIT message. + */ + static init (clientName, authToken) { + return new RequestMessage( + INIT, + [clientName, authToken], + () => `INIT ${clientName} {...}` + ) + } + + /** + * Create a new RUN message. + * @param {string} query the cypher query. + * @param {Object} parameters the query parameters. + * @return {RequestMessage} new RUN message. + */ + static run (query, parameters) { + return new RequestMessage( + RUN, + [query, parameters], + () => `RUN ${query} ${json.stringify(parameters)}` + ) + } + + /** + * Get a PULL_ALL message. + * @return {RequestMessage} the PULL_ALL message. + */ + static pullAll () { + return PULL_ALL_MESSAGE + } + + /** + * Get a RESET message. + * @return {RequestMessage} the RESET message. + */ + static reset () { + return RESET_MESSAGE + } + + /** + * Create a new HELLO message. + * @param {string} userAgent the user agent. + * @param {Object} authToken the authentication token. + * @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, 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], + () => `HELLO {user_agent: '${userAgent}', ...}` + ) + } + + /** + * Create a new BEGIN message. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @param {string} database the database name. + * @param {string} mode the access mode. + * @param {string} impersonatedUser the impersonated user. + * @return {RequestMessage} new BEGIN message. + */ + static begin ({ bookmarks, txConfig, database, mode, impersonatedUser } = {}) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + return new RequestMessage( + BEGIN, + [metadata], + () => `BEGIN ${json.stringify(metadata)}` + ) + } + + /** + * Get a COMMIT message. + * @return {RequestMessage} the COMMIT message. + */ + static commit () { + return COMMIT_MESSAGE + } + + /** + * Get a ROLLBACK message. + * @return {RequestMessage} the ROLLBACK message. + */ + static rollback () { + return ROLLBACK_MESSAGE + } + + /** + * Create a new RUN message with additional metadata. + * @param {string} query the cypher query. + * @param {Object} parameters the query parameters. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @param {string} database the database name. + * @param {string} mode the access mode. + * @param {string} impersonatedUser the impersonated user. + * @return {RequestMessage} new RUN message with additional metadata. + */ + static runWithMetadata ( + query, + parameters, + { bookmarks, txConfig, database, mode, impersonatedUser } = {} + ) { + const metadata = buildTxMetadata(bookmarks, txConfig, database, mode, impersonatedUser) + return new RequestMessage( + RUN, + [query, parameters, metadata], + () => + `RUN ${query} ${json.stringify(parameters)} ${json.stringify(metadata)}` + ) + } + + /** + * Get a GOODBYE message. + * @return {RequestMessage} the GOODBYE message. + */ + static goodbye () { + return GOODBYE_MESSAGE + } + + /** + * Generates a new PULL message with additional metadata. + * @param {Integer|number} stmtId + * @param {Integer|number} n + * @return {RequestMessage} the PULL message. + */ + static pull ({ stmtId = NO_STATEMENT_ID, n = FETCH_ALL } = {}) { + const metadata = buildStreamMetadata( + stmtId === null || stmtId === undefined ? NO_STATEMENT_ID : stmtId, + n || FETCH_ALL + ) + return new RequestMessage( + PULL, + [metadata], + () => `PULL ${json.stringify(metadata)}` + ) + } + + /** + * Generates a new DISCARD message with additional metadata. + * @param {Integer|number} stmtId + * @param {Integer|number} n + * @return {RequestMessage} the PULL message. + */ + static discard ({ stmtId = NO_STATEMENT_ID, n = FETCH_ALL } = {}) { + const metadata = buildStreamMetadata( + stmtId === null || stmtId === undefined ? NO_STATEMENT_ID : stmtId, + n || FETCH_ALL + ) + return new RequestMessage( + DISCARD, + [metadata], + () => `DISCARD ${json.stringify(metadata)}` + ) + } + + /** + * Generate the ROUTE message, this message is used to fetch the routing table from the server + * + * @param {object} routingContext The routing context used to define the routing table. Multi-datacenter deployments is one of its use cases + * @param {string[]} bookmarks The list of the bookmarks should be used + * @param {string} databaseName The name of the database to get the routing table for. + * @return {RequestMessage} the ROUTE message. + */ + static route (routingContext = {}, bookmarks = [], databaseName = null) { + return new RequestMessage( + ROUTE, + [routingContext, bookmarks, databaseName], + () => + `ROUTE ${json.stringify(routingContext)} ${json.stringify( + bookmarks + )} ${databaseName}` + ) + } + + /** + * Generate the ROUTE message, this message is used to fetch the routing table from the server + * + * @param {object} routingContext The routing context used to define the routing table. Multi-datacenter deployments is one of its use cases + * @param {string[]} bookmarks The list of the bookmarks should be used + * @param {object} databaseContext The context inforamtion of the database to get the routing table for. + * @param {string} databaseContext.databaseName The name of the database to get the routing table. + * @param {string} databaseContext.impersonatedUser The name of the user to impersonation when getting the routing table. + * @return {RequestMessage} the ROUTE message. + */ + static routeV4x4 (routingContext = {}, bookmarks = [], databaseContext = {}) { + const dbContext = {} + + if (databaseContext.databaseName) { + dbContext.db = databaseContext.databaseName + } + + if (databaseContext.impersonatedUser) { + dbContext.imp_user = databaseContext.impersonatedUser + } + + return new RequestMessage( + ROUTE, + [routingContext, bookmarks, dbContext], + () => + `ROUTE ${json.stringify(routingContext)} ${json.stringify( + bookmarks + )} ${json.stringify(dbContext)}` + ) + } +} + +/** + * Create an object that represent transaction metadata. + * @param {Bookmarks} bookmarks the bookmarks. + * @param {TxConfig} txConfig the configuration. + * @param {string} database the database name. + * @param {string} mode the access mode. + * @param {string} impersonatedUser the impersonated user mode. + * @return {Object} a metadata object. + */ +function buildTxMetadata (bookmarks, txConfig, database, mode, impersonatedUser) { + const metadata = {} + if (!bookmarks.isEmpty()) { + metadata.bookmarks = bookmarks.values() + } + if (txConfig.timeout !== null) { + metadata.tx_timeout = txConfig.timeout + } + if (txConfig.metadata) { + metadata.tx_metadata = txConfig.metadata + } + if (database) { + metadata.db = assertString(database, 'database') + } + if (impersonatedUser) { + metadata.imp_user = assertString(impersonatedUser, 'impersonatedUser') + } + if (mode === ACCESS_MODE_READ) { + metadata.mode = READ_MODE + } + return metadata +} + +/** + * Create an object that represents streaming metadata. + * @param {Integer|number} stmtId The query id to stream its results. + * @param {Integer|number} n The number of records to stream. + * @returns {Object} a metadata object. + */ +function buildStreamMetadata (stmtId, n) { + const metadata = { n: int(n) } + if (stmtId !== NO_STATEMENT_ID) { + metadata.qid = int(stmtId) + } + return metadata +} + +// constants for messages that never change +const PULL_ALL_MESSAGE = new RequestMessage(PULL_ALL, [], () => 'PULL_ALL') +const RESET_MESSAGE = new RequestMessage(RESET, [], () => 'RESET') +const COMMIT_MESSAGE = new RequestMessage(COMMIT, [], () => 'COMMIT') +const ROLLBACK_MESSAGE = new RequestMessage(ROLLBACK, [], () => 'ROLLBACK') +const GOODBYE_MESSAGE = new RequestMessage(GOODBYE, [], () => 'GOODBYE') diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js new file mode 100644 index 000000000..d4aa8f4a5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/response-handler.js @@ -0,0 +1,215 @@ +/** + * 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 { newError, json } from '../../core/index.ts' + +// Signature bytes for each response message type +const SUCCESS = 0x70 // 0111 0000 // SUCCESS +const RECORD = 0x71 // 0111 0001 // RECORD +const IGNORED = 0x7e // 0111 1110 // IGNORED +const FAILURE = 0x7f // 0111 1111 // FAILURE + +function NO_OP () {} + +function NO_OP_IDENTITY (subject) { + return subject +} + +const NO_OP_OBSERVER = { + onNext: NO_OP, + onCompleted: NO_OP, + onError: NO_OP +} + +/** + * Treat the protocol responses and notify the observers + */ +export default class ResponseHandler { + /** + * Called when something went wrong with the connectio + * @callback ResponseHandler~Observer~OnErrorApplyTransformation + * @param {any} error The error + * @returns {any} The new error + */ + /** + * Called when something went wrong with the connectio + * @callback ResponseHandler~Observer~OnError + * @param {any} error The error + */ + /** + * Called when something went wrong with the connectio + * @callback ResponseHandler~MetadataTransformer + * @param {any} metadata The metadata got onSuccess + * @returns {any} The transformed metadata + */ + /** + * @typedef {Object} ResponseHandler~Observer + * @property {ResponseHandler~Observer~OnError} onError Invoke when a connection error occurs + * @property {ResponseHandler~Observer~OnError} onFailure Invoke when a protocol failure occurs + * @property {ResponseHandler~Observer~OnErrorApplyTransformation} onErrorApplyTransformation Invoke just after the failure occurs, + * before notify to respective observer. This method should transform the failure reason to the approprited one. + */ + /** + * Constructor + * @param {Object} param The params + * @param {ResponseHandler~MetadataTransformer} transformMetadata Transform metadata when the SUCCESS is received. + * @param {Channel} channel The channel used to exchange messages + * @param {Logger} log The logger + * @param {ResponseHandler~Observer} observer Object which will be notified about errors + */ + constructor ({ transformMetadata, log, observer } = {}) { + this._pendingObservers = [] + this._log = log + this._transformMetadata = transformMetadata || NO_OP_IDENTITY + this._observer = Object.assign( + { + onPendingObserversChange: NO_OP, + onError: NO_OP, + onFailure: NO_OP, + onErrorApplyTransformation: NO_OP_IDENTITY + }, + observer + ) + } + + get currentFailure () { + return this._currentFailure + } + + handleResponse (msg) { + const payload = msg.fields[0] + + switch (msg.signature) { + case RECORD: + if (this._log.isDebugEnabled()) { + this._log.debug(`S: RECORD ${json.stringify(msg)}`) + } + this._currentObserver.onNext(payload) + break + case SUCCESS: + if (this._log.isDebugEnabled()) { + this._log.debug(`S: SUCCESS ${json.stringify(msg)}`) + } + try { + const metadata = this._transformMetadata(payload) + this._currentObserver.onCompleted(metadata) + } finally { + this._updateCurrentObserver() + } + break + case FAILURE: + if (this._log.isDebugEnabled()) { + this._log.debug(`S: FAILURE ${json.stringify(msg)}`) + } + try { + const standardizedCode = _standardizeCode(payload.code) + const error = newError(payload.message, standardizedCode) + this._currentFailure = this._observer.onErrorApplyTransformation( + error + ) + this._currentObserver.onError(this._currentFailure) + } finally { + this._updateCurrentObserver() + // Things are now broken. Pending observers will get FAILURE messages routed until we are done handling this failure. + this._observer.onFailure(this._currentFailure) + } + break + case IGNORED: + if (this._log.isDebugEnabled()) { + this._log.debug(`S: IGNORED ${json.stringify(msg)}`) + } + try { + if (this._currentFailure && this._currentObserver.onError) { + this._currentObserver.onError(this._currentFailure) + } else if (this._currentObserver.onError) { + this._currentObserver.onError( + newError('Ignored either because of an error or RESET') + ) + } + } finally { + this._updateCurrentObserver() + } + break + default: + this._observer.onError( + newError('Unknown Bolt protocol message: ' + msg) + ) + } + } + + /* + * Pop next pending observer form the list of observers and make it current observer. + * @protected + */ + _updateCurrentObserver () { + this._currentObserver = this._pendingObservers.shift() + this._observer.onPendingObserversChange(this._pendingObservers.length) + } + + _queueObserver (observer) { + observer = observer || NO_OP_OBSERVER + observer.onCompleted = observer.onCompleted || NO_OP + observer.onError = observer.onError || NO_OP + observer.onNext = observer.onNext || NO_OP + if (this._currentObserver === undefined) { + this._currentObserver = observer + } else { + this._pendingObservers.push(observer) + } + this._observer.onPendingObserversChange(this._pendingObservers.length) + return true + } + + _notifyErrorToObservers (error) { + if (this._currentObserver && this._currentObserver.onError) { + this._currentObserver.onError(error) + } + while (this._pendingObservers.length > 0) { + const observer = this._pendingObservers.shift() + if (observer && observer.onError) { + observer.onError(error) + } + } + } + + hasOngoingObservableRequests () { + return this._currentObserver != null || this._pendingObservers.length > 0 + } + + _resetFailure () { + this._currentFailure = null + } +} + +/** + * Standardize error classification that are different between 5.x and previous versions. + * + * The transient error were clean-up for being retrieable and because of this + * `Terminated` and `LockClientStopped` were reclassified as `ClientError`. + * + * @param {string} code + * @returns {string} the standardized error code + */ +function _standardizeCode (code) { + if (code === 'Neo.TransientError.Transaction.Terminated') { + return 'Neo.ClientError.Transaction.Terminated' + } else if (code === 'Neo.TransientError.Transaction.LockClientStopped') { + return 'Neo.ClientError.Transaction.LockClientStopped' + } + return code +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/routing-table-raw.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/routing-table-raw.js new file mode 100644 index 000000000..39e03d4f3 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/routing-table-raw.js @@ -0,0 +1,158 @@ +/** + * 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. + */ +// eslint-disable-next-line no-unused-vars +import Record from '../../core/index.ts' + +/** + * Represente the raw version of the routing table + */ +export default class RawRoutingTable { + /** + * Constructs the raw routing table for Record based result + * @param {Record} record The record which will be used get the raw routing table + * @returns {RawRoutingTable} The raw routing table + */ + static ofRecord (record) { + if (record === null) { + return RawRoutingTable.ofNull() + } + return new RecordRawRoutingTable(record) + } + + /** + * Constructs the raw routing table for Success result for a Routing Message + * @param {object} response The result + * @returns {RawRoutingTable} The raw routing table + */ + static ofMessageResponse (response) { + if (response === null) { + return RawRoutingTable.ofNull() + } + return new ResponseRawRoutingTable(response) + } + + /** + * Construct the raw routing table of a null response + * + * @returns {RawRoutingTable} the raw routing table + */ + static ofNull () { + return new NullRawRoutingTable() + } + + /** + * Get raw ttl + * + * @returns {number|string} ttl Time to live + */ + get ttl () { + throw new Error('Not implemented') + } + + /** + * Get raw db + * + * @returns {string?} The database name + */ + get db () { + throw new Error('Not implemented') + } + + /** + * + * @typedef {Object} ServerRole + * @property {string} role the role of the address on the cluster + * @property {string[]} addresses the address within the role + * + * @return {ServerRole[]} list of servers addresses + */ + get servers () { + throw new Error('Not implemented') + } + + /** + * Indicates the result is null + * + * @returns {boolean} Is null + */ + get isNull () { + throw new Error('Not implemented') + } +} + +/** + * Get the raw routing table information from route message response + */ +class ResponseRawRoutingTable extends RawRoutingTable { + constructor (response) { + super() + this._response = response + } + + get ttl () { + return this._response.rt.ttl + } + + get servers () { + return this._response.rt.servers + } + + get db () { + return this._response.rt.db + } + + get isNull () { + return this._response === null + } +} + +/** + * Null routing table + */ +class NullRawRoutingTable extends RawRoutingTable { + get isNull () { + return true + } +} + +/** + * Get the raw routing table information from the record + */ +class RecordRawRoutingTable extends RawRoutingTable { + constructor (record) { + super() + this._record = record + } + + get ttl () { + return this._record.get('ttl') + } + + get servers () { + return this._record.get('servers') + } + + get db () { + return this._record.has('db') ? this._record.get('db') : null + } + + get isNull () { + return this._record === null + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js new file mode 100644 index 000000000..d8ff0b88d --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/stream-observers.js @@ -0,0 +1,679 @@ +/** + * 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 { + newError, + error, + // eslint-disable-next-line no-unused-vars + Integer, + Record, + json, + internal +} from '../../core/index.ts' +import RawRoutingTable from './routing-table-raw.js' + +const { + constants: { FETCH_ALL } +} = internal +const { PROTOCOL_ERROR } = error +class StreamObserver { + onNext (rawRecord) {} + + onError (_error) {} + + onCompleted (meta) {} +} + +/** + * Handles a RUN/PULL_ALL, or RUN/DISCARD_ALL requests, maps the responses + * in a way that a user-provided observer can see these as a clean Stream + * of records. + * This class will queue up incoming messages until a user-provided observer + * for the incoming stream is registered. Thus, we keep fields around + * for tracking head/records/tail. These are only used if there is no + * observer registered. + * @access private + */ +class ResultStreamObserver extends StreamObserver { + /** + * + * @param {Object} param + * @param {Object} param.server + * @param {boolean} param.reactive + * @param {function(stmtId: number|Integer, n: number|Integer, observer: StreamObserver)} param.moreFunction - + * @param {function(stmtId: number|Integer, observer: StreamObserver)} param.discardFunction - + * @param {number|Integer} param.fetchSize - + * @param {function(err: Error): Promise|void} param.beforeError - + * @param {function(err: Error): Promise|void} param.afterError - + * @param {function(keys: string[]): Promise|void} param.beforeKeys - + * @param {function(keys: string[]): Promise|void} param.afterKeys - + * @param {function(metadata: Object): Promise|void} param.beforeComplete - + * @param {function(metadata: Object): Promise|void} param.afterComplete - + */ + constructor ({ + reactive = false, + moreFunction, + discardFunction, + fetchSize = FETCH_ALL, + beforeError, + afterError, + beforeKeys, + afterKeys, + beforeComplete, + afterComplete, + server, + highRecordWatermark = Number.MAX_VALUE, + lowRecordWatermark = Number.MAX_VALUE + } = {}) { + super() + + this._fieldKeys = null + this._fieldLookup = null + this._head = null + this._queuedRecords = [] + this._tail = null + this._error = null + this._observers = [] + this._meta = {} + this._server = server + + this._beforeError = beforeError + this._afterError = afterError + this._beforeKeys = beforeKeys + this._afterKeys = afterKeys + this._beforeComplete = beforeComplete + this._afterComplete = afterComplete + + this._queryId = null + this._moreFunction = moreFunction + this._discardFunction = discardFunction + this._discard = false + this._fetchSize = fetchSize + this._lowRecordWatermark = lowRecordWatermark + this._highRecordWatermark = highRecordWatermark + this._setState(reactive ? _states.READY : _states.READY_STREAMING) + this._setupAutoPull() + this._paused = false + } + + /** + * Pause the record consuming + * + * This function will supend the record consuming. It will not cancel the stream and the already + * requested records will be sent to the subscriber. + */ + pause () { + this._paused = true + } + + /** + * Resume the record consuming + * + * This function will resume the record consuming fetching more records from the server. + */ + resume () { + this._paused = false + this._setupAutoPull(true) + this._state.pull(this) + } + + /** + * Will be called on every record that comes in and transform a raw record + * to a Object. If user-provided observer is present, pass transformed record + * to it's onNext method, otherwise, push to record que. + * @param {Array} rawRecord - An array with the raw record + */ + onNext (rawRecord) { + const record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) + if (this._observers.some(o => o.onNext)) { + this._observers.forEach(o => { + if (o.onNext) { + o.onNext(record) + } + }) + } else { + this._queuedRecords.push(record) + if (this._queuedRecords.length > this._highRecordWatermark) { + this._autoPull = false + } + } + } + + onCompleted (meta) { + this._state.onSuccess(this, meta) + } + + /** + * Will be called on errors. + * If user-provided observer is present, pass the error + * to it's onError method, otherwise set instance variable _error. + * @param {Object} error - An error object + */ + onError (error) { + this._state.onError(this, error) + } + + /** + * Cancel pending record stream + */ + cancel () { + this._discard = true + } + + /** + * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. + * Response for RUN initializes query keys. Response for PULL_ALL / DISCARD_ALL exposes the result stream. + * + * However, some operations can be represented as a single message which receives full metadata in a single response. + * For example, operations to begin, commit and rollback an explicit transaction use two messages in Bolt V1 but a single message in Bolt V3. + * Messages are `RUN "BEGIN" {}` + `PULL_ALL` in Bolt V1 and `BEGIN` in Bolt V3. + * + * This function prepares the observer to only handle a single response message. + */ + prepareToHandleSingleResponse () { + this._head = [] + this._fieldKeys = [] + this._setState(_states.STREAMING) + } + + /** + * Mark this observer as if it has completed with no metadata. + */ + markCompleted () { + this._head = [] + this._fieldKeys = [] + this._tail = {} + this._setState(_states.SUCCEEDED) + } + + /** + * Subscribe to events with provided observer. + * @param {Object} observer - Observer object + * @param {function(keys: String[])} observer.onKeys - Handle stream header, field keys. + * @param {function(record: Object)} observer.onNext - Handle records, one by one. + * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the metadata. + * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. + */ + subscribe (observer) { + if (this._head && observer.onKeys) { + observer.onKeys(this._head) + } + if (this._queuedRecords.length > 0 && observer.onNext) { + for (let i = 0; i < this._queuedRecords.length; i++) { + observer.onNext(this._queuedRecords[i]) + if (this._queuedRecords.length - i - 1 <= this._lowRecordWatermark) { + this._autoPull = true + if (this._state === _states.READY) { + this._handleStreaming() + } + } + } + } + if (this._tail && observer.onCompleted) { + observer.onCompleted(this._tail) + } + if (this._error) { + observer.onError(this._error) + } + this._observers.push(observer) + + if (this._state === _states.READY) { + this._handleStreaming() + } + } + + _handleHasMore (meta) { + // We've consumed current batch and server notified us that there're more + // records to stream. Let's invoke more or discard function based on whether + // the user wants to discard streaming or not + this._setState(_states.READY) // we've done streaming + this._handleStreaming() + delete meta.has_more + } + + _handlePullSuccess (meta) { + const completionMetadata = Object.assign( + this._server ? { server: this._server } : {}, + this._meta, + meta + ) + + if (![undefined, null, 'r', 'w', 'rw', 's'].includes(completionMetadata.type)) { + this.onError( + newError( + `Server returned invalid query type. Expected one of [undefined, null, "r", "w", "rw", "s"] but got '${completionMetadata.type}'`, + PROTOCOL_ERROR)) + return + } + + this._setState(_states.SUCCEEDED) + + let beforeHandlerResult = null + if (this._beforeComplete) { + beforeHandlerResult = this._beforeComplete(completionMetadata) + } + + const continuation = () => { + // End of stream + this._tail = completionMetadata + + if (this._observers.some(o => o.onCompleted)) { + this._observers.forEach(o => { + if (o.onCompleted) { + o.onCompleted(completionMetadata) + } + }) + } + + if (this._afterComplete) { + this._afterComplete(completionMetadata) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + + _handleRunSuccess (meta, afterSuccess) { + if (this._fieldKeys === null) { + // Stream header, build a name->index field lookup table + // to be used by records. This is an optimization to make it + // faster to look up fields in a record by name, rather than by index. + // Since the records we get back via Bolt are just arrays of values. + this._fieldKeys = [] + this._fieldLookup = {} + if (meta.fields && meta.fields.length > 0) { + this._fieldKeys = meta.fields + for (let i = 0; i < meta.fields.length; i++) { + this._fieldLookup[meta.fields[i]] = i + } + + // remove fields key from metadata object + delete meta.fields + } + + // Extract server generated query id for use in requestMore and discard + // functions + if (meta.qid !== null && meta.qid !== undefined) { + this._queryId = meta.qid + + // remove qid from metadata object + delete meta.qid + } + + this._storeMetadataForCompletion(meta) + + let beforeHandlerResult = null + if (this._beforeKeys) { + beforeHandlerResult = this._beforeKeys(this._fieldKeys) + } + + const continuation = () => { + this._head = this._fieldKeys + + if (this._observers.some(o => o.onKeys)) { + this._observers.forEach(o => { + if (o.onKeys) { + o.onKeys(this._fieldKeys) + } + }) + } + + if (this._afterKeys) { + this._afterKeys(this._fieldKeys) + } + + afterSuccess() + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + } + + _handleError (error) { + this._setState(_states.FAILED) + this._error = error + + let beforeHandlerResult = null + if (this._beforeError) { + beforeHandlerResult = this._beforeError(error) + } + + const continuation = () => { + if (this._observers.some(o => o.onError)) { + this._observers.forEach(o => { + if (o.onError) { + o.onError(error) + } + }) + } + + if (this._afterError) { + this._afterError(error) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + + _handleStreaming () { + if (this._head && this._observers.some(o => o.onNext || o.onCompleted)) { + if (!this._paused && (this._discard || this._autoPull)) { + this._more() + } + } + } + + _more () { + if (this._discard) { + this._discardFunction(this._queryId, this) + } else { + this._moreFunction(this._queryId, this._fetchSize, this) + } + this._setState(_states.STREAMING) + } + + _storeMetadataForCompletion (meta) { + const keys = Object.keys(meta) + let index = keys.length + let key = '' + + while (index--) { + key = keys[index] + this._meta[key] = meta[key] + } + } + + _setState (state) { + this._state = state + } + + _setupAutoPull () { + this._autoPull = true + } +} + +class LoginObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onCompleted + */ + constructor ({ onError, onCompleted } = {}) { + super() + this._onError = onError + this._onCompleted = onCompleted + } + + onNext (record) { + this.onError( + newError('Received RECORD when initializing ' + json.stringify(record)) + ) + } + + onError (error) { + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onCompleted) { + this._onCompleted(metadata) + } + } +} + +class ResetObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {function(err: String)} param.onProtocolError + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onComplete + */ + constructor ({ onProtocolError, onError, onComplete } = {}) { + super() + + this._onProtocolError = onProtocolError + this._onError = onError + this._onComplete = onComplete + } + + onNext (record) { + this.onError( + newError( + 'Received RECORD when resetting: received record is: ' + + json.stringify(record), + PROTOCOL_ERROR + ) + ) + } + + onError (error) { + if (error.code === PROTOCOL_ERROR && this._onProtocolError) { + this._onProtocolError(error.message) + } + + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onComplete) { + this._onComplete(metadata) + } + } +} + +class FailedObserver extends ResultStreamObserver { + constructor ({ error, onError }) { + super({ beforeError: onError }) + + this.onError(error) + } +} + +class CompletedObserver extends ResultStreamObserver { + constructor () { + super() + super.markCompleted() + } +} + +class ProcedureRouteObserver extends StreamObserver { + constructor ({ resultObserver, onProtocolError, onError, onCompleted }) { + super() + + this._resultObserver = resultObserver + this._onError = onError + this._onCompleted = onCompleted + this._records = [] + this._onProtocolError = onProtocolError + resultObserver.subscribe(this) + } + + onNext (record) { + this._records.push(record) + } + + onError (error) { + if (error.code === PROTOCOL_ERROR && this._onProtocolError) { + this._onProtocolError(error.message) + } + + if (this._onError) { + this._onError(error) + } + } + + onCompleted () { + if (this._records !== null && this._records.length !== 1) { + this.onError( + newError( + 'Illegal response from router. Received ' + + this._records.length + + ' records but expected only one.\n' + + json.stringify(this._records), + PROTOCOL_ERROR + ) + ) + return + } + + if (this._onCompleted) { + this._onCompleted(RawRoutingTable.ofRecord(this._records[0])) + } + } +} + +class RouteObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {function(err: String)} param.onProtocolError + * @param {function(err: Error)} param.onError + * @param {function(RawRoutingTable)} param.onCompleted + */ + constructor ({ onProtocolError, onError, onCompleted } = {}) { + super() + + this._onProtocolError = onProtocolError + this._onError = onError + this._onCompleted = onCompleted + } + + onNext (record) { + this.onError( + newError( + 'Received RECORD when resetting: received record is: ' + + json.stringify(record), + PROTOCOL_ERROR + ) + ) + } + + onError (error) { + if (error.code === PROTOCOL_ERROR && this._onProtocolError) { + this._onProtocolError(error.message) + } + + if (this._onError) { + this._onError(error) + } + } + + onCompleted (metadata) { + if (this._onCompleted) { + this._onCompleted(RawRoutingTable.ofMessageResponse(metadata)) + } + } +} + +const _states = { + READY_STREAMING: { + // async start state + onSuccess: (streamObserver, meta) => { + streamObserver._handleRunSuccess( + meta, + () => { + streamObserver._setState(_states.STREAMING) + } // after run succeeded, async directly move to streaming + // state + ) + }, + onError: (streamObserver, error) => { + streamObserver._handleError(error) + }, + name: () => { + return 'READY_STREAMING' + }, + pull: () => {} + }, + READY: { + // reactive start state + onSuccess: (streamObserver, meta) => { + streamObserver._handleRunSuccess( + meta, + () => streamObserver._handleStreaming() // after run succeeded received, reactive shall start pulling + ) + }, + onError: (streamObserver, error) => { + streamObserver._handleError(error) + }, + name: () => { + return 'READY' + }, + pull: streamObserver => streamObserver._more() + }, + STREAMING: { + onSuccess: (streamObserver, meta) => { + if (meta.has_more) { + streamObserver._handleHasMore(meta) + } else { + streamObserver._handlePullSuccess(meta) + } + }, + onError: (streamObserver, error) => { + streamObserver._handleError(error) + }, + name: () => { + return 'STREAMING' + }, + pull: () => {} + }, + FAILED: { + onError: _error => { + // more errors are ignored + }, + name: () => { + return 'FAILED' + }, + pull: () => {} + }, + SUCCEEDED: { + name: () => { + return 'SUCCEEDED' + }, + pull: () => {} + } +} + +export { + StreamObserver, + ResultStreamObserver, + LoginObserver, + ResetObserver, + FailedObserver, + CompletedObserver, + RouteObserver, + ProcedureRouteObserver +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/temporal-factory.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/temporal-factory.js new file mode 100644 index 000000000..063e57627 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/temporal-factory.js @@ -0,0 +1,144 @@ +/** + * 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, + Date, + LocalDateTime, + LocalTime, + internal +} from '../../core/index.ts' +const { + temporalUtil: { + DAYS_0000_TO_1970, + DAYS_PER_400_YEAR_CYCLE, + NANOS_PER_HOUR, + NANOS_PER_MINUTE, + NANOS_PER_SECOND, + SECONDS_PER_DAY, + floorDiv, + floorMod + } +} = internal + +/** + * Converts given epoch day to a local date. + * @param {Integer|number|string} epochDay the epoch day to convert. + * @return {Date} the date representing the epoch day in years, months and days. + */ +export function epochDayToDate (epochDay) { + epochDay = int(epochDay) + + let zeroDay = epochDay.add(DAYS_0000_TO_1970).subtract(60) + let adjust = int(0) + if (zeroDay.lessThan(0)) { + const adjustCycles = zeroDay + .add(1) + .div(DAYS_PER_400_YEAR_CYCLE) + .subtract(1) + adjust = adjustCycles.multiply(400) + zeroDay = zeroDay.add(adjustCycles.multiply(-DAYS_PER_400_YEAR_CYCLE)) + } + let year = zeroDay + .multiply(400) + .add(591) + .div(DAYS_PER_400_YEAR_CYCLE) + let dayOfYearEst = zeroDay.subtract( + year + .multiply(365) + .add(year.div(4)) + .subtract(year.div(100)) + .add(year.div(400)) + ) + if (dayOfYearEst.lessThan(0)) { + year = year.subtract(1) + dayOfYearEst = zeroDay.subtract( + year + .multiply(365) + .add(year.div(4)) + .subtract(year.div(100)) + .add(year.div(400)) + ) + } + year = year.add(adjust) + const marchDayOfYear = dayOfYearEst + + const marchMonth = marchDayOfYear + .multiply(5) + .add(2) + .div(153) + const month = marchMonth + .add(2) + .modulo(12) + .add(1) + const day = marchDayOfYear + .subtract( + marchMonth + .multiply(306) + .add(5) + .div(10) + ) + .add(1) + year = year.add(marchMonth.div(10)) + + return new Date(year, month, day) +} + +/** + * Converts nanoseconds of the day into local time. + * @param {Integer|number|string} nanoOfDay the nanoseconds of the day to convert. + * @return {LocalTime} the local time representing given nanoseconds of the day. + */ +export function nanoOfDayToLocalTime (nanoOfDay) { + nanoOfDay = int(nanoOfDay) + + const hour = nanoOfDay.div(NANOS_PER_HOUR) + nanoOfDay = nanoOfDay.subtract(hour.multiply(NANOS_PER_HOUR)) + + const minute = nanoOfDay.div(NANOS_PER_MINUTE) + nanoOfDay = nanoOfDay.subtract(minute.multiply(NANOS_PER_MINUTE)) + + const second = nanoOfDay.div(NANOS_PER_SECOND) + const nanosecond = nanoOfDay.subtract(second.multiply(NANOS_PER_SECOND)) + + return new LocalTime(hour, minute, second, nanosecond) +} + +/** + * Converts given epoch second and nanosecond adjustment into a local date time object. + * @param {Integer|number|string} epochSecond the epoch second to use. + * @param {Integer|number|string} nano the nanosecond to use. + * @return {LocalDateTime} the local date time representing given epoch second and nano. + */ +export function epochSecondAndNanoToLocalDateTime (epochSecond, nano) { + const epochDay = floorDiv(epochSecond, SECONDS_PER_DAY) + const secondsOfDay = floorMod(epochSecond, SECONDS_PER_DAY) + const nanoOfDay = secondsOfDay.multiply(NANOS_PER_SECOND).add(nano) + + const localDate = epochDayToDate(epochDay) + const localTime = nanoOfDayToLocalTime(nanoOfDay) + return new LocalDateTime( + localDate.year, + localDate.month, + localDate.day, + localTime.hour, + localTime.minute, + localTime.second, + localTime.nanosecond + ) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/transformer.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/transformer.js new file mode 100644 index 000000000..4424e6d82 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/transformer.js @@ -0,0 +1,130 @@ +/** + * 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/index.js' +import { internal } from '../../core/index.ts' + +const { objectUtil } = internal + +/** + * Class responsible for applying the expected {@link TypeTransformer} to + * transform the driver types from and to {@link struct.Structure} + */ +export default class Transformer { + /** + * Constructor + * @param {TypeTransformer[]} transformers The type transformers + */ + constructor (transformers) { + this._transformers = transformers + this._transformersPerSignature = new Map(transformers.map(typeTransformer => [typeTransformer.signature, typeTransformer])) + this.fromStructure = this.fromStructure.bind(this) + this.toStructure = this.toStructure.bind(this) + Object.freeze(this) + } + + /** + * Transform from structure to specific object + * + * @param {struct.Structure} struct The structure + * @returns {|structure.Structure} The driver object or the structure if the transformer was not found. + */ + fromStructure (struct) { + try { + if (struct instanceof structure.Structure && this._transformersPerSignature.has(struct.signature)) { + const { fromStructure } = this._transformersPerSignature.get(struct.signature) + return fromStructure(struct) + } + return struct + } catch (error) { + return objectUtil.createBrokenObject(error) + } + } + + /** + * Transform from object to structure + * @param {} type The object to be transoformed in structure + * @returns {|structure.Structure} The structure or the object, if any transformer was found + */ + toStructure (type) { + const transformer = this._transformers.find(({ isTypeInstance }) => isTypeInstance(type)) + if (transformer !== undefined) { + return transformer.toStructure(type) + } + return type + } +} + +/** + * @callback isTypeInstanceFunction + * @param {any} object The object + * @return {boolean} is instance of + */ + +/** + * @callback toStructureFunction + * @param {any} object The object + * @return {structure.Structure} The structure + */ + +/** + * @callback fromStructureFunction + * @param {structure.Structure} struct The structure + * @return {any} The object + */ + +/** + * Class responsible for grouping the properties of a TypeTransformer + */ +export class TypeTransformer { + /** + * @param {Object} param + * @param {number} param.signature The signature of the structure + * @param {isTypeInstanceFunction} param.isTypeInstance The function which checks if object is + * instance of the type described by the TypeTransformer + * @param {toStructureFunction} param.toStructure The function which gets the object and converts to structure + * @param {fromStructureFunction} param.fromStructure The function which get the structure and covnverts to object + */ + constructor ({ signature, fromStructure, toStructure, isTypeInstance }) { + this.signature = signature + this.isTypeInstance = isTypeInstance + this.fromStructure = fromStructure + this.toStructure = toStructure + + Object.freeze(this) + } + + /** + * @param {Object} param + * @param {number} [param.signature] The signature of the structure + * @param {isTypeInstanceFunction} [param.isTypeInstance] The function which checks if object is + * instance of the type described by the TypeTransformer + * @param {toStructureFunction} [param.toStructure] The function which gets the object and converts to structure + * @param {fromStructureFunction} pparam.fromStructure] The function which get the structure and covnverts to object + * @returns {TypeTransformer} A new type transform extends with new methods + */ + extendsWith ({ signature, fromStructure, toStructure, isTypeInstance }) { + return new TypeTransformer({ + signature: signature || this.signature, + fromStructure: fromStructure || this.fromStructure, + toStructure: toStructure || this.toStructure, + isTypeInstance: isTypeInstance || this.isTypeInstance + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/buf/base-buf.js b/packages/neo4j-driver-deno/lib/bolt-connection/buf/base-buf.js new file mode 100644 index 000000000..2e19e75ff --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/buf/base-buf.js @@ -0,0 +1,417 @@ +/** + * 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. + */ + +/** + * Common base with default implementation for most buffer methods. + * Buffers are stateful - they track a current "position", this helps greatly + * when reading and writing from them incrementally. You can also ignore the + * stateful read/write methods. + * readXXX and writeXXX-methods move the inner position of the buffer. + * putXXX and getXXX-methods do not. + * @access private + */ +export default class BaseBuffer { + /** + * Create a instance with the injected size. + * @constructor + * @param {Integer} size + */ + constructor (size) { + this.position = 0 + this.length = size + } + + getUInt8 (position) { + throw new Error('Not implemented') + } + + getInt8 (position) { + throw new Error('Not implemented') + } + + getFloat64 (position) { + throw new Error('Not implemented') + } + + putUInt8 (position, val) { + throw new Error('Not implemented') + } + + putInt8 (position, val) { + throw new Error('Not implemented') + } + + putFloat64 (position, val) { + throw new Error('Not implemented') + } + + /** + * @param p + */ + getInt16 (p) { + return (this.getInt8(p) << 8) | this.getUInt8(p + 1) + } + + /** + * @param p + */ + getUInt16 (p) { + return (this.getUInt8(p) << 8) | this.getUInt8(p + 1) + } + + /** + * @param p + */ + getInt32 (p) { + return ( + (this.getInt8(p) << 24) | + (this.getUInt8(p + 1) << 16) | + (this.getUInt8(p + 2) << 8) | + this.getUInt8(p + 3) + ) + } + + /** + * @param p + */ + getUInt32 (p) { + return ( + (this.getUInt8(p) << 24) | + (this.getUInt8(p + 1) << 16) | + (this.getUInt8(p + 2) << 8) | + this.getUInt8(p + 3) + ) + } + + /** + * @param p + */ + getInt64 (p) { + return ( + (this.getInt8(p) << 56) | + (this.getUInt8(p + 1) << 48) | + (this.getUInt8(p + 2) << 40) | + (this.getUInt8(p + 3) << 32) | + (this.getUInt8(p + 4) << 24) | + (this.getUInt8(p + 5) << 16) | + (this.getUInt8(p + 6) << 8) | + this.getUInt8(p + 7) + ) + } + + /** + * Get a slice of this buffer. This method does not copy any data, + * but simply provides a slice view of this buffer + * @param start + * @param length + */ + getSlice (start, length) { + return new SliceBuffer(start, length, this) + } + + /** + * @param p + * @param val + */ + putInt16 (p, val) { + this.putInt8(p, val >> 8) + this.putUInt8(p + 1, val & 0xff) + } + + /** + * @param p + * @param val + */ + putUInt16 (p, val) { + this.putUInt8(p, (val >> 8) & 0xff) + this.putUInt8(p + 1, val & 0xff) + } + + /** + * @param p + * @param val + */ + putInt32 (p, val) { + this.putInt8(p, val >> 24) + this.putUInt8(p + 1, (val >> 16) & 0xff) + this.putUInt8(p + 2, (val >> 8) & 0xff) + this.putUInt8(p + 3, val & 0xff) + } + + /** + * @param p + * @param val + */ + putUInt32 (p, val) { + this.putUInt8(p, (val >> 24) & 0xff) + this.putUInt8(p + 1, (val >> 16) & 0xff) + this.putUInt8(p + 2, (val >> 8) & 0xff) + this.putUInt8(p + 3, val & 0xff) + } + + /** + * @param p + * @param val + */ + putInt64 (p, val) { + this.putInt8(p, val >> 48) + this.putUInt8(p + 1, (val >> 42) & 0xff) + this.putUInt8(p + 2, (val >> 36) & 0xff) + this.putUInt8(p + 3, (val >> 30) & 0xff) + this.putUInt8(p + 4, (val >> 24) & 0xff) + this.putUInt8(p + 5, (val >> 16) & 0xff) + this.putUInt8(p + 6, (val >> 8) & 0xff) + this.putUInt8(p + 7, val & 0xff) + } + + /** + * @param position + * @param other + */ + putBytes (position, other) { + for (let i = 0, end = other.remaining(); i < end; i++) { + this.putUInt8(position + i, other.readUInt8()) + } + } + + /** + * Read from state position. + */ + readUInt8 () { + return this.getUInt8(this._updatePos(1)) + } + + /** + * Read from state position. + */ + readInt8 () { + return this.getInt8(this._updatePos(1)) + } + + /** + * Read from state position. + */ + readUInt16 () { + return this.getUInt16(this._updatePos(2)) + } + + /** + * Read from state position. + */ + readUInt32 () { + return this.getUInt32(this._updatePos(4)) + } + + /** + * Read from state position. + */ + readInt16 () { + return this.getInt16(this._updatePos(2)) + } + + /** + * Read from state position. + */ + readInt32 () { + return this.getInt32(this._updatePos(4)) + } + + /** + * Read from state position. + */ + readInt64 () { + return this.getInt32(this._updatePos(8)) + } + + /** + * Read from state position. + */ + readFloat64 () { + return this.getFloat64(this._updatePos(8)) + } + + /** + * Write to state position. + * @param val + */ + writeUInt8 (val) { + this.putUInt8(this._updatePos(1), val) + } + + /** + * Write to state position. + * @param val + */ + writeInt8 (val) { + this.putInt8(this._updatePos(1), val) + } + + /** + * Write to state position. + * @param val + */ + writeInt16 (val) { + this.putInt16(this._updatePos(2), val) + } + + /** + * Write to state position. + * @param val + */ + writeInt32 (val) { + this.putInt32(this._updatePos(4), val) + } + + /** + * Write to state position. + * @param val + */ + writeUInt32 (val) { + this.putUInt32(this._updatePos(4), val) + } + + /** + * Write to state position. + * @param val + */ + writeInt64 (val) { + this.putInt64(this._updatePos(8), val) + } + + /** + * Write to state position. + * @param val + */ + writeFloat64 (val) { + this.putFloat64(this._updatePos(8), val) + } + + /** + * Write to state position. + * @param val + */ + writeBytes (val) { + this.putBytes(this._updatePos(val.remaining()), val) + } + + /** + * Get a slice of this buffer. This method does not copy any data, + * but simply provides a slice view of this buffer + * @param length + */ + readSlice (length) { + return this.getSlice(this._updatePos(length), length) + } + + _updatePos (length) { + const p = this.position + this.position += length + return p + } + + /** + * Get remaining + */ + remaining () { + return this.length - this.position + } + + /** + * Has remaining + */ + hasRemaining () { + return this.remaining() > 0 + } + + /** + * Reset position state + */ + reset () { + this.position = 0 + } + + /** + * Get string representation of buffer and it's state. + * @return {string} Buffer as a string + */ + toString () { + return ( + this.constructor.name + + '( position=' + + this.position + + ' )\n ' + + this.toHex() + ) + } + + /** + * Get string representation of buffer. + * @return {string} Buffer as a string + */ + toHex () { + let out = '' + for (let i = 0; i < this.length; i++) { + let hexByte = this.getUInt8(i).toString(16) + if (hexByte.length === 1) { + hexByte = '0' + hexByte + } + out += hexByte + if (i !== this.length - 1) { + out += ' ' + } + } + return out + } +} + +/** + * Represents a view as slice of another buffer. + * @access private + */ +class SliceBuffer extends BaseBuffer { + constructor (start, length, inner) { + super(length) + this._start = start + this._inner = inner + } + + putUInt8 (position, val) { + this._inner.putUInt8(this._start + position, val) + } + + getUInt8 (position) { + return this._inner.getUInt8(this._start + position) + } + + putInt8 (position, val) { + this._inner.putInt8(this._start + position, val) + } + + putFloat64 (position, val) { + this._inner.putFloat64(this._start + position, val) + } + + getInt8 (position) { + return this._inner.getInt8(this._start + position) + } + + getFloat64 (position) { + return this._inner.getFloat64(this._start + position) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/buf/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/buf/index.js new file mode 100644 index 000000000..abf1a3394 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/buf/index.js @@ -0,0 +1,23 @@ +/** + * 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 BaseBuffer from './base-buf.js' + +export default BaseBuffer +export { BaseBuffer } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js new file mode 100644 index 000000000..565c99167 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js @@ -0,0 +1,419 @@ +/** + * 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. + */ +/* eslint-env browser */ +import ChannelBuffer from '../channel-buf.js' +import { newError, internal } from '../../../core/index.ts' + +const { + util: { ENCRYPTION_OFF, ENCRYPTION_ON } +} = internal + +// Just to be sure that these values are with us even after WebSocket is injected +// for tests. +// eslint-disable-next-line no-unused-vars +const WS_CONNECTING = 0 +const WS_OPEN = 1 +// eslint-disable-next-line no-unused-vars +const WS_CLOSING = 2 +const WS_CLOSED = 3 + +/** + * Create a new WebSocketChannel to be used in web browsers. + * @access private + */ +export default class WebSocketChannel { + /** + * Create new instance + * @param {ChannelConfig} config - configuration for this channel. + * @param {function(): string} protocolSupplier - function that detects protocol of the web page. Should only be used in tests. + */ + constructor ( + config, + protocolSupplier = detectWebPageProtocol, + socketFactory = url => new WebSocket(url) + ) { + this._open = true + this._pending = [] + this._error = null + this._handleConnectionError = this._handleConnectionError.bind(this) + this._config = config + this._receiveTimeout = null + this._receiveTimeoutStarted = false + this._receiveTimeoutId = null + + const { scheme, error } = determineWebSocketScheme(config, protocolSupplier) + if (error) { + this._error = error + return + } + + this._ws = createWebSocket(scheme, config.address, socketFactory) + this._ws.binaryType = 'arraybuffer' + + const self = this + // All connection errors are not sent to the error handler + // we must also check for dirty close calls + this._ws.onclose = function (e) { + if (e && !e.wasClean) { + self._handleConnectionError() + } + self._open = false + } + this._ws.onopen = function () { + // Connected! Cancel the connection timeout + self._clearConnectionTimeout() + + // Drain all pending messages + const pending = self._pending + self._pending = null + for (let i = 0; i < pending.length; i++) { + self.write(pending[i]) + } + } + this._ws.onmessage = event => { + this._resetTimeout() + if (self.onmessage) { + const b = new ChannelBuffer(event.data) + self.onmessage(b) + } + } + + this._ws.onerror = this._handleConnectionError + + this._connectionTimeoutFired = false + this._connectionTimeoutId = this._setupConnectionTimeout() + } + + _handleConnectionError () { + if (this._connectionTimeoutFired) { + // timeout fired - not connected within configured time + this._error = newError( + `Failed to establish connection in ${this._config.connectionTimeout}ms`, + this._config.connectionErrorCode + ) + + if (this.onerror) { + this.onerror(this._error) + } + return + } + + // onerror triggers on websocket close as well.. don't get me started. + if (this._open && !this._timedout) { + // http://stackoverflow.com/questions/25779831/how-to-catch-websocket-connection-to-ws-xxxnn-failed-connection-closed-be + this._error = newError( + 'WebSocket connection failure. Due to security ' + + 'constraints in your web browser, the reason for the failure is not available ' + + 'to this Neo4j Driver. Please use your browsers development console to determine ' + + 'the root cause of the failure. Common reasons include the database being ' + + 'unavailable, using the wrong connection URL or temporary network problems. ' + + 'If you have enabled encryption, ensure your browser is configured to trust the ' + + 'certificate Neo4j is configured to use. WebSocket `readyState` is: ' + + this._ws.readyState, + this._config.connectionErrorCode + ) + if (this.onerror) { + this.onerror(this._error) + } + } + } + + /** + * Write the passed in buffer to connection + * @param {ChannelBuffer} buffer - Buffer to write + */ + write (buffer) { + // If there is a pending queue, push this on that queue. This means + // we are not yet connected, so we queue things locally. + if (this._pending !== null) { + this._pending.push(buffer) + } else if (buffer instanceof ChannelBuffer) { + try { + this._ws.send(buffer._buffer) + } catch (error) { + if (this._ws.readyState !== WS_OPEN) { + // Websocket has been closed + this._handleConnectionError() + } else { + // Some other error occured + throw error + } + } + } else { + throw newError("Don't know how to send buffer: " + buffer) + } + } + + /** + * Close the connection + * @returns {Promise} A promise that will be resolved after channel is closed + */ + close () { + return new Promise((resolve, reject) => { + if (this._ws && this._ws.readyState !== WS_CLOSED) { + this._open = false + this._clearConnectionTimeout() + this._ws.onclose = () => resolve() + this._ws.close() + } else { + resolve() + } + }) + } + + /** + * Setup the receive timeout for the channel. + * + * Not supported for the browser channel. + * + * @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms) + * @returns {void} + */ + setupReceiveTimeout (receiveTimeout) { + this._receiveTimeout = receiveTimeout + } + + /** + * Stops the receive timeout for the channel. + */ + stopReceiveTimeout () { + if (this._receiveTimeout !== null && this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = false + if (this._receiveTimeoutId != null) { + clearTimeout(this._receiveTimeoutId) + } + this._receiveTimeoutId = null + } + } + + /** + * Start the receive timeout for the channel. + */ + startReceiveTimeout () { + if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = true + this._resetTimeout() + } + } + + _resetTimeout () { + if (!this._receiveTimeoutStarted) { + return + } + + if (this._receiveTimeoutId !== null) { + clearTimeout(this._receiveTimeoutId) + } + + this._receiveTimeoutId = setTimeout(() => { + this._receiveTimeoutId = null + this._timedout = true + this.stopReceiveTimeout() + this._error = newError( + `Connection lost. Server didn't respond in ${this._receiveTimeout}ms`, + this._config.connectionErrorCode + ) + + this.close() + if (this.onerror) { + this.onerror(this._error) + } + }, this._receiveTimeout) + } + + /** + * Set connection timeout on the given WebSocket, if configured. + * @return {number} the timeout id or null. + * @private + */ + _setupConnectionTimeout () { + const timeout = this._config.connectionTimeout + if (timeout) { + const webSocket = this._ws + + return setTimeout(() => { + if (webSocket.readyState !== WS_OPEN) { + this._connectionTimeoutFired = true + webSocket.close() + } + }, timeout) + } + return null + } + + /** + * Remove active connection timeout, if any. + * @private + */ + _clearConnectionTimeout () { + const timeoutId = this._connectionTimeoutId + if (timeoutId || timeoutId === 0) { + this._connectionTimeoutFired = false + this._connectionTimeoutId = null + clearTimeout(timeoutId) + } + } +} + +function createWebSocket (scheme, address, socketFactory) { + const url = scheme + '://' + address.asHostPort() + + try { + return socketFactory(url) + } catch (error) { + if (isIPv6AddressIssueOnWindows(error, address)) { + // WebSocket in IE and Edge browsers on Windows do not support regular IPv6 address syntax because they contain ':'. + // It's an invalid character for UNC (https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_UNC_path_names) + // and Windows requires IPv6 to be changes in the following way: + // 1) replace all ':' with '-' + // 2) replace '%' with 's' for link-local address + // 3) append '.ipv6-literal.net' suffix + // only then resulting string can be considered a valid IPv6 address. Yes, this is extremely weird! + // For more details see: + // https://social.msdn.microsoft.com/Forums/ie/en-US/06cca73b-63c2-4bf9-899b-b229c50449ff/whether-ie10-websocket-support-ipv6?forum=iewebdevelopment + // https://www.itdojo.com/ipv6-addresses-and-unc-path-names-overcoming-illegal/ + // Creation of WebSocket with unconverted address results in SyntaxError without message or stacktrace. + // That is why here we "catch" SyntaxError and rewrite IPv6 address if needed. + + const windowsFriendlyUrl = asWindowsFriendlyIPv6Address(scheme, address) + return socketFactory(windowsFriendlyUrl) + } else { + throw error + } + } +} + +function isIPv6AddressIssueOnWindows (error, address) { + return error.name === 'SyntaxError' && isIPv6Address(address.asHostPort()) +} + +function isIPv6Address (hostAndPort) { + return hostAndPort.charAt(0) === '[' && hostAndPort.indexOf(']') !== -1 +} + +function asWindowsFriendlyIPv6Address (scheme, address) { + // replace all ':' with '-' + const hostWithoutColons = address.host().replace(/:/g, '-') + + // replace '%' with 's' for link-local IPv6 address like 'fe80::1%lo0' + const hostWithoutPercent = hostWithoutColons.replace('%', 's') + + // append magic '.ipv6-literal.net' suffix + const ipv6Host = hostWithoutPercent + '.ipv6-literal.net' + + return `${scheme}://${ipv6Host}:${address.port()}` +} + +/** + * @param {ChannelConfig} config - configuration for the channel. + * @param {function(): string} protocolSupplier - function that detects protocol of the web page. + * @return {{scheme: string|null, error: Neo4jError|null}} object containing either scheme or error. + */ +function determineWebSocketScheme (config, protocolSupplier) { + const encryptionOn = isEncryptionExplicitlyTurnedOn(config) + const encryptionOff = isEncryptionExplicitlyTurnedOff(config) + const trust = config.trust + const secureProtocol = isProtocolSecure(protocolSupplier) + verifyEncryptionSettings(encryptionOn, encryptionOff, secureProtocol) + + if (encryptionOff) { + // encryption explicitly turned off in the config + return { scheme: 'ws', error: null } + } + + if (secureProtocol) { + // driver is used in a secure https web page, use 'wss' + return { scheme: 'wss', error: null } + } + + if (encryptionOn) { + // encryption explicitly requested in the config + if (!trust || trust === 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES') { + // trust strategy not specified or the only supported strategy is specified + return { scheme: 'wss', error: null } + } else { + const error = newError( + 'The browser version of this driver only supports one trust ' + + "strategy, 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES'. " + + trust + + ' is not supported. Please ' + + 'either use TRUST_SYSTEM_CA_SIGNED_CERTIFICATES or disable encryption by setting ' + + '`encrypted:"' + + ENCRYPTION_OFF + + '"` in the driver configuration.' + ) + return { scheme: null, error: error } + } + } + + // default to unencrypted web socket + return { scheme: 'ws', error: null } +} + +/** + * @param {ChannelConfig} config - configuration for the channel. + * @return {boolean} `true` if encryption enabled in the config, `false` otherwise. + */ +function isEncryptionExplicitlyTurnedOn (config) { + return config.encrypted === true || config.encrypted === ENCRYPTION_ON +} + +/** + * @param {ChannelConfig} config - configuration for the channel. + * @return {boolean} `true` if encryption disabled in the config, `false` otherwise. + */ +function isEncryptionExplicitlyTurnedOff (config) { + return config.encrypted === false || config.encrypted === ENCRYPTION_OFF +} + +/** + * @param {function(): string} protocolSupplier - function that detects protocol of the web page. + * @return {boolean} `true` if protocol returned by the given function is secure, `false` otherwise. + */ +function isProtocolSecure (protocolSupplier) { + const protocol = + typeof protocolSupplier === 'function' ? protocolSupplier() : '' + return protocol && protocol.toLowerCase().indexOf('https') >= 0 +} + +function verifyEncryptionSettings (encryptionOn, encryptionOff, secureProtocol) { + if (secureProtocol === null) { + // do nothing sice the protocol could not be identified + } else if (encryptionOn && !secureProtocol) { + // encryption explicitly turned on for a driver used on a HTTP web page + console.warn( + 'Neo4j driver is configured to use secure WebSocket on a HTTP web page. ' + + 'WebSockets might not work in a mixed content environment. ' + + 'Please consider configuring driver to not use encryption.' + ) + } else if (encryptionOff && secureProtocol) { + // encryption explicitly turned off for a driver used on a HTTPS web page + console.warn( + 'Neo4j driver is configured to use insecure WebSocket on a HTTPS web page. ' + + 'WebSockets might not work in a mixed content environment. ' + + 'Please consider configuring driver to use encryption.' + ) + } +} + +function detectWebPageProtocol () { + return typeof window !== 'undefined' && window.location + ? window.location.protocol + : null +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-host-name-resolver.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-host-name-resolver.js new file mode 100644 index 000000000..883c3474c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-host-name-resolver.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { internal } from '../../../core/index.ts' + +const { + resolver: { BaseHostNameResolver } +} = internal + +export default class BrowserHostNameResolver extends BaseHostNameResolver { + resolve (address) { + return this._resolveToItself(address) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js new file mode 100644 index 000000000..c006716aa --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/index.js @@ -0,0 +1,34 @@ +/** + * 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 WebSocketChannel from './browser-channel.js' +import BrowserHosNameResolver from './browser-host-name-resolver.js' + +/* + +This module exports a set of components to be used in browser environment. +They are not compatible with NodeJS environment. +All files import/require APIs from `node/index.js` by default. +Such imports are replaced at build time with `browser/index.js` when building a browser bundle. + +NOTE: exports in this module should have exactly the same names/structure as exports in `node/index.js`. + + */ +export const Channel = WebSocketChannel +export const HostNameResolver = BrowserHosNameResolver diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-buf.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-buf.js new file mode 100644 index 000000000..9a6074363 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-buf.js @@ -0,0 +1,101 @@ +/** + * 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 buffer from 'https://deno.land/std@0.119.0/node/buffer.ts' +import BaseBuffer from '../buf/index.js' + +export default class ChannelBuffer extends BaseBuffer { + constructor (arg) { + const buffer = newChannelJSBuffer(arg) + super(buffer.length) + this._buffer = buffer + } + + getUInt8 (position) { + return this._buffer.readUInt8(position) + } + + getInt8 (position) { + return this._buffer.readInt8(position) + } + + getFloat64 (position) { + return this._buffer.readDoubleBE(position) + } + + putUInt8 (position, val) { + this._buffer.writeUInt8(val, position) + } + + putInt8 (position, val) { + this._buffer.writeInt8(val, position) + } + + putFloat64 (position, val) { + this._buffer.writeDoubleBE(val, position) + } + + putBytes (position, val) { + if (val instanceof ChannelBuffer) { + const bytesToCopy = Math.min( + val.length - val.position, + this.length - position + ) + val._buffer.copy( + this._buffer, + position, + val.position, + val.position + bytesToCopy + ) + val.position += bytesToCopy + } else { + super.putBytes(position, val) + } + } + + getSlice (start, length) { + return new ChannelBuffer(this._buffer.slice(start, start + length)) + } +} + +/** + * Allocate a buffer + * + * @param {number} size The buffer sizzer + * @returns {BaseBuffer} The buffer + */ +export function alloc (size) { + return new ChannelBuffer(size) +} + +function newChannelJSBuffer (arg) { + if (arg instanceof buffer.Buffer) { + return arg + } else if ( + typeof arg === 'number' && + typeof buffer.Buffer.alloc === 'function' + ) { + // use static factory function present in newer NodeJS versions to allocate new buffer with specified size + return buffer.Buffer.alloc(arg) + } else { + // fallback to the old, potentially deprecated constructor + // eslint-disable-next-line node/no-deprecated-api + return new buffer.Buffer(arg) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js new file mode 100644 index 000000000..76c39903e --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/channel-config.js @@ -0,0 +1,89 @@ +/** + * 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 { newError, error, internal } from '../../core/index.ts' + +const { + util: { ENCRYPTION_OFF, ENCRYPTION_ON } +} = internal + +const { SERVICE_UNAVAILABLE } = error + +const ALLOWED_VALUES_ENCRYPTED = [ + null, + undefined, + true, + false, + ENCRYPTION_ON, + ENCRYPTION_OFF +] + +const ALLOWED_VALUES_TRUST = [ + null, + undefined, + 'TRUST_ALL_CERTIFICATES', + 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', + 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' +] + +export default class ChannelConfig { + /** + * @constructor + * @param {ServerAddress} address the address for the channel to connect to. + * @param {Object} driverConfig the driver config provided by the user when driver is created. + * @param {string} connectionErrorCode the default error code to use on connection errors. + */ + constructor (address, driverConfig, connectionErrorCode) { + this.address = address + this.encrypted = extractEncrypted(driverConfig) + this.trust = extractTrust(driverConfig) + this.trustedCertificates = extractTrustedCertificates(driverConfig) + this.knownHostsPath = extractKnownHostsPath(driverConfig) + this.connectionErrorCode = connectionErrorCode || SERVICE_UNAVAILABLE + this.connectionTimeout = driverConfig.connectionTimeout + } +} + +function extractEncrypted (driverConfig) { + const value = driverConfig.encrypted + if (ALLOWED_VALUES_ENCRYPTED.indexOf(value) === -1) { + throw newError( + `Illegal value of the encrypted setting ${value}. Expected one of ${ALLOWED_VALUES_ENCRYPTED}` + ) + } + return value +} + +function extractTrust (driverConfig) { + const value = driverConfig.trust + if (ALLOWED_VALUES_TRUST.indexOf(value) === -1) { + throw newError( + `Illegal value of the trust setting ${value}. Expected one of ${ALLOWED_VALUES_TRUST}` + ) + } + return value +} + +function extractTrustedCertificates (driverConfig) { + return driverConfig.trustedCertificates || [] +} + +function extractKnownHostsPath (driverConfig) { + return driverConfig.knownHosts || null +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/chunking.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/chunking.js new file mode 100644 index 000000000..d4c6e40e1 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/chunking.js @@ -0,0 +1,209 @@ +/** + * 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 BaseBuffer from '../buf/base-buf.js' +import { alloc } from './channel-buf.js' +import CombinedBuffer from './combined-buf.js' + +const _CHUNK_HEADER_SIZE = 2 +const _MESSAGE_BOUNDARY = 0x00 +const _DEFAULT_BUFFER_SIZE = 1400 // http://stackoverflow.com/questions/2613734/maximum-packet-size-for-a-tcp-connection + +/** + * Looks like a writable buffer, chunks output transparently into a channel below. + * @access private + */ +class Chunker extends BaseBuffer { + constructor (channel, bufferSize) { + super(0) + this._bufferSize = bufferSize || _DEFAULT_BUFFER_SIZE + this._ch = channel + this._buffer = alloc(this._bufferSize) + this._currentChunkStart = 0 + this._chunkOpen = false + } + + putUInt8 (position, val) { + this._ensure(1) + this._buffer.writeUInt8(val) + } + + putInt8 (position, val) { + this._ensure(1) + this._buffer.writeInt8(val) + } + + putFloat64 (position, val) { + this._ensure(8) + this._buffer.writeFloat64(val) + } + + putBytes (position, data) { + // TODO: If data is larger than our chunk size or so, we're very likely better off just passing this buffer on + // rather than doing the copy here TODO: *however* note that we need some way to find out when the data has been + // written (and thus the buffer can be re-used) if we take that approach + while (data.remaining() > 0) { + // Ensure there is an open chunk, and that it has at least one byte of space left + this._ensure(1) + if (this._buffer.remaining() > data.remaining()) { + this._buffer.writeBytes(data) + } else { + this._buffer.writeBytes(data.readSlice(this._buffer.remaining())) + } + } + return this + } + + flush () { + if (this._buffer.position > 0) { + this._closeChunkIfOpen() + + // Local copy and clear the buffer field. This ensures that the buffer is not re-released if the flush call fails + const out = this._buffer + this._buffer = null + + this._ch.write(out.getSlice(0, out.position)) + + // Alloc a new output buffer. We assume we're using NodeJS's buffer pooling under the hood here! + this._buffer = alloc(this._bufferSize) + this._chunkOpen = false + } + return this + } + + /** + * Bolt messages are encoded in one or more chunks, and the boundary between two messages + * is encoded as a 0-length chunk, `00 00`. This inserts such a message boundary, closing + * any currently open chunk as needed + */ + messageBoundary () { + this._closeChunkIfOpen() + + if (this._buffer.remaining() < _CHUNK_HEADER_SIZE) { + this.flush() + } + + // Write message boundary + this._buffer.writeInt16(_MESSAGE_BOUNDARY) + } + + /** Ensure at least the given size is available for writing */ + _ensure (size) { + const toWriteSize = this._chunkOpen ? size : size + _CHUNK_HEADER_SIZE + if (this._buffer.remaining() < toWriteSize) { + this.flush() + } + + if (!this._chunkOpen) { + this._currentChunkStart = this._buffer.position + this._buffer.position = this._buffer.position + _CHUNK_HEADER_SIZE + this._chunkOpen = true + } + } + + _closeChunkIfOpen () { + if (this._chunkOpen) { + const chunkSize = + this._buffer.position - (this._currentChunkStart + _CHUNK_HEADER_SIZE) + this._buffer.putUInt16(this._currentChunkStart, chunkSize) + this._chunkOpen = false + } + } +} + +/** + * Combines chunks until a complete message is gathered up, and then forwards that + * message to an 'onmessage' listener. + * @access private + */ +class Dechunker { + constructor () { + this._currentMessage = [] + this._partialChunkHeader = 0 + this._state = this.AWAITING_CHUNK + } + + AWAITING_CHUNK (buf) { + if (buf.remaining() >= 2) { + // Whole header available, read that + return this._onHeader(buf.readUInt16()) + } else { + // Only one byte available, read that and wait for the second byte + this._partialChunkHeader = buf.readUInt8() << 8 + return this.IN_HEADER + } + } + + IN_HEADER (buf) { + // First header byte read, now we read the next one + return this._onHeader((this._partialChunkHeader | buf.readUInt8()) & 0xffff) + } + + IN_CHUNK (buf) { + if (this._chunkSize <= buf.remaining()) { + // Current packet is larger than current chunk, or same size: + this._currentMessage.push(buf.readSlice(this._chunkSize)) + return this.AWAITING_CHUNK + } else { + // Current packet is smaller than the chunk we're reading, split the current chunk itself up + this._chunkSize -= buf.remaining() + this._currentMessage.push(buf.readSlice(buf.remaining())) + return this.IN_CHUNK + } + } + + CLOSED (buf) { + // no-op + } + + /** Called when a complete chunk header has been received */ + _onHeader (header) { + if (header === 0) { + // Message boundary + let message + switch (this._currentMessage.length) { + case 0: + // Keep alive chunk, sent by server to keep network alive. + return this.AWAITING_CHUNK + case 1: + // All data in one chunk, this signals the end of that chunk. + message = this._currentMessage[0] + break + default: + // A large chunk of data received, this signals that the last chunk has been received. + message = new CombinedBuffer(this._currentMessage) + break + } + this._currentMessage = [] + this.onmessage(message) + return this.AWAITING_CHUNK + } else { + this._chunkSize = header + return this.IN_CHUNK + } + } + + write (buf) { + while (buf.hasRemaining()) { + this._state = this._state(buf) + } + } +} + +export { Chunker, Dechunker } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/combined-buf.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/combined-buf.js new file mode 100644 index 000000000..81233eb1b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/combined-buf.js @@ -0,0 +1,71 @@ +/** + * 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 { BaseBuffer } from '../buf/index.js' +import { alloc } from './channel-buf.js' + +/** + * Buffer that combines multiple buffers, exposing them as one single buffer. + */ +export default class CombinedBuffer extends BaseBuffer { + constructor (buffers) { + let length = 0 + for (let i = 0; i < buffers.length; i++) { + length += buffers[i].length + } + super(length) + this._buffers = buffers + } + + getUInt8 (position) { + // Surely there's a faster way to do this.. some sort of lookup table thing? + for (let i = 0; i < this._buffers.length; i++) { + const buffer = this._buffers[i] + // If the position is not in the current buffer, skip the current buffer + if (position >= buffer.length) { + position -= buffer.length + } else { + return buffer.getUInt8(position) + } + } + } + + getInt8 (position) { + // Surely there's a faster way to do this.. some sort of lookup table thing? + for (let i = 0; i < this._buffers.length; i++) { + const buffer = this._buffers[i] + // If the position is not in the current buffer, skip the current buffer + if (position >= buffer.length) { + position -= buffer.length + } else { + return buffer.getInt8(position) + } + } + } + + getFloat64 (position) { + // At some point, a more efficient impl. For now, we copy the 8 bytes + // we want to read and depend on the platform impl of IEEE 754. + const b = alloc(8) + for (let i = 0; i < 8; i++) { + b.putUInt8(i, this.getUInt8(position + i)) + } + return b.getFloat64(0) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js new file mode 100644 index 000000000..a1322b4c7 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-channel.js @@ -0,0 +1,336 @@ +/** + * 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. + */ +/* eslint-disable */ +import ChannelBuffer from '../channel-buf.js' +import { newError, internal } from '../../../core/index.ts' +import { iterateReader } from 'https://deno.land/std@0.157.0/streams/conversion.ts'; + +const { + util: { ENCRYPTION_OFF, ENCRYPTION_ON } +} = internal + +let _CONNECTION_IDGEN = 0 +/** + * Create a new DenoChannel to be used in Deno runtime. + * @access private + */ +export default class DenoChannel { + /** + * Create new instance + * @param {ChannelConfig} config - configuration for this channel. + */ + constructor ( + config, + connect = _connect + ) { + this.id = _CONNECTION_IDGEN++ + this._conn = null + this._pending = [] + this._open = true + this._error = null + this._handleConnectionError = this._handleConnectionError.bind(this) + this._handleConnectionTerminated = this._handleConnectionTerminated.bind( + this + ) + this._connectionErrorCode = config.connectionErrorCode + this._receiveTimeout = null + this._receiveTimeoutStarted = false + this._receiveTimeoutId = null + + this._config = config + + connect(config) + .then(conn => { + this._clearConnectionTimeout() + if (!this._open) { + return conn.close() + } + this._conn = conn + + setupReader(this) + .catch(this._handleConnectionError) + + const pending = this._pending + this._pending = null + for (let i = 0; i < pending.length; i++) { + this.write(pending[i]) + } + }) + .catch(this._handleConnectionError) + + this._connectionTimeoutFired = false + this._connectionTimeoutId = this._setupConnectionTimeout() + } + + _setupConnectionTimeout () { + const timeout = this._config.connectionTimeout + if (timeout) { + return setTimeout(() => { + this._connectionTimeoutFired = true + this.close() + .then(e => this._handleConnectionError(newError(`Connection timeout after ${timeout} ms`))) + .catch(this._handleConnectionError) + }, timeout) + } + return null + } + + /** + * Remove active connection timeout, if any. + * @private + */ + _clearConnectionTimeout () { + const timeoutId = this._connectionTimeoutId + if (timeoutId !== null) { + this._connectionTimeoutFired = false + this._connectionTimeoutId = null + clearTimeout(timeoutId) + } + } + + _handleConnectionError (err) { + let msg = + 'Failed to connect to server. ' + + 'Please ensure that your database is listening on the correct host and port ' + + 'and that you have compatible encryption settings both on Neo4j server and driver. ' + + 'Note that the default encryption setting has changed in Neo4j 4.0.' + if (err.message) msg += ' Caused by: ' + err.message + this._error = newError(msg, this._connectionErrorCode) + if (this.onerror) { + this.onerror(this._error) + } + } + + _handleConnectionTerminated () { + this._open = false + this._error = newError( + 'Connection was closed by server', + this._connectionErrorCode + ) + if (this.onerror) { + this.onerror(this._error) + } + } + + + /** + * Write the passed in buffer to connection + * @param {ChannelBuffer} buffer - Buffer to write + */ + write (buffer) { + if (this._pending !== null) { + this._pending.push(buffer) + } else if (buffer instanceof ChannelBuffer) { + this._conn.write(buffer._buffer).catch(this._handleConnectionError) + } else { + throw newError("Don't know how to send buffer: " + buffer) + } + } + + /** + * Close the connection + * @returns {Promise} A promise that will be resolved after channel is closed + */ + async close () { + if (this._open) { + this._open = false + if (this._conn != null) { + await this._conn.close() + } + } + } + + /** + * Setup the receive timeout for the channel. + * + * Not supported for the browser channel. + * + * @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms) + * @returns {void} + */ + setupReceiveTimeout (receiveTimeout) { + this._receiveTimeout = receiveTimeout + } + + /** + * Stops the receive timeout for the channel. + */ + stopReceiveTimeout () { + if (this._receiveTimeout !== null && this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = false + if (this._receiveTimeoutId != null) { + clearTimeout(this._receiveTimeoutId) + } + this._receiveTimeoutId = null + } + } + + /** + * Start the receive timeout for the channel. + */ + startReceiveTimeout () { + if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = true + this._resetTimeout() + } + } + + _resetTimeout () { + if (!this._receiveTimeoutStarted) { + return + } + + if (this._receiveTimeoutId !== null) { + clearTimeout(this._receiveTimeoutId) + } + + this._receiveTimeoutId = setTimeout(() => { + this._receiveTimeoutId = null + this.stopReceiveTimeout() + this._error = newError( + `Connection lost. Server didn't respond in ${this._receiveTimeout}ms`, + this._config.connectionErrorCode + ) + + this.close() + .catch(() => { + // ignoring error during the close timeout connections since they + // not valid + }) + .finally(() => { + if (this.onerror) { + this.onerror(this._error) + } + }) + }, this._receiveTimeout) + } +} + +const TrustStrategy = { + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: async function (config) { + if ( + !config.trustedCertificates || + config.trustedCertificates.length === 0 + ) { + throw newError( + 'You are using TRUST_CUSTOM_CA_SIGNED_CERTIFICATES as the method ' + + 'to verify trust for encrypted connections, but have not configured any ' + + 'trustedCertificates. You must specify the path to at least one trusted ' + + 'X.509 certificate for this to work. Two other alternatives is to use ' + + 'TRUST_ALL_CERTIFICATES or to disable encryption by setting encrypted="' + + ENCRYPTION_OFF + + '"' + + 'in your driver configuration.' + ); + } + + const caCerts = await Promise.all( + config.trustedCertificates.map(f => Deno.readTextFile(f)) + ) + + return Deno.connectTls({ + hostname: config.address.resolvedHost(), + port: config.address.port(), + caCerts + }) + }, + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) { + return Deno.connectTls({ + hostname: config.address.resolvedHost(), + port: config.address.port() + }) + }, + TRUST_ALL_CERTIFICATES: function (config) { + throw newError( + `"${config.trust}" is not available in DenoJS. ` + + 'For trust in any certificates, you should use the DenoJS flag ' + + '"--unsafely-ignore-certificate-errors". '+ + 'See, https://deno.com/blog/v1.13#disable-tls-verification' + ) + } +} + +async function _connect (config) { + if (!isEncrypted(config)) { + return Deno.connect({ + hostname: config.address.resolvedHost(), + port: config.address.port() + }) + } + const trustStrategyName = getTrustStrategyName(config) + const trustStrategy = TrustStrategy[trustStrategyName] + + if (trustStrategy != null) { + return await trustStrategy(config) + } + + throw newError( + 'Unknown trust strategy: ' + + config.trust + + '. Please use either ' + + "trust:'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' configuration " + + 'or the System CA. ' + + 'Alternatively, you can disable encryption by setting ' + + '`encrypted:"' + + ENCRYPTION_OFF + + '"`. There is no mechanism to use encryption without trust verification, ' + + 'because this incurs the overhead of encryption without improving security. If ' + + 'the driver does not verify that the peer it is connected to is really Neo4j, it ' + + 'is very easy for an attacker to bypass the encryption by pretending to be Neo4j.' + + ) +} + +function isEncrypted (config) { + const encryptionNotConfigured = + config.encrypted == null || config.encrypted === undefined + if (encryptionNotConfigured) { + // default to using encryption if trust-all-certificates is available + return false + } + return config.encrypted === true || config.encrypted === ENCRYPTION_ON +} + +function getTrustStrategyName (config) { + if (config.trust) { + return config.trust + } + return 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' +} + +async function setupReader (channel) { + try { + for await (const message of iterateReader(channel._conn)) { + channel._resetTimeout() + + if (!channel._open) { + return + } + if (channel.onmessage) { + channel.onmessage(new ChannelBuffer(message)) + } + } + channel._handleConnectionTerminated() + } catch (error) { + if (channel._open) { + channel._handleConnectionError(error) + } + } +} + diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-host-name-resolver.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-host-name-resolver.js new file mode 100644 index 000000000..c56094653 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/deno-host-name-resolver.js @@ -0,0 +1,29 @@ +/** + * 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 { internal } from '../../../core/index.ts' + +const { + resolver: { BaseHostNameResolver } +} = internal + +export default class DenoHostNameResolver extends BaseHostNameResolver { + resolve (address) { + return this._resolveToItself(address) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js new file mode 100644 index 000000000..7a72241e1 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/deno/index.js @@ -0,0 +1,34 @@ +/** + * 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 DenoChannel from './deno-channel.js' +import DenoHostNameResolver from './deno-host-name-resolver.js' + +/* + + This module exports a set of components to be used in deno environment. + They are not compatible with NodeJS environment. + All files import/require APIs from `node/index.js` by default. + Such imports are replaced at build time with `deno/index.js` when building a deno bundle. + + NOTE: exports in this module should have exactly the same names/structure as exports in `node/index.js`. + + */ +export const Channel = DenoChannel +export const HostNameResolver = DenoHostNameResolver diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/index.js new file mode 100644 index 000000000..eff243c72 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/index.js @@ -0,0 +1,24 @@ +/** + * 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. + */ + +export * from './deno/index.js' +export * from './chunking.js' +export { default as ChannelConfig } from './channel-config.js' +export { alloc } from './channel-buf.js' +export { default as utf8 } from './utf8.js' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js new file mode 100644 index 000000000..ff258f56c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/index.js @@ -0,0 +1,35 @@ +/** + * 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 NodeChannel from './node-channel.js' +import NodeHostNameResolver from './node-host-name-resolver.js' + +/* + +This module exports a set of components to be used in NodeJS environment. +They are not compatible with browser environment. +All files that require environment-dependent APIs should import this file by default. +Imports/requires are replaced at build time with `browser/index.js` when building a browser bundle. + +NOTE: exports in this module should have exactly the same names/structure as exports in `browser/index.js`. + + */ + +export const Channel = NodeChannel +export const HostNameResolver = NodeHostNameResolver diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js new file mode 100644 index 000000000..c545adbbf --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-channel.js @@ -0,0 +1,423 @@ +/** + * 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 net from 'net' +import tls from 'tls' +import fs from 'fs' +import ChannelBuffer from '../channel-buf.js' +import { newError, internal } from '../../../core/index.ts' + +const { + util: { ENCRYPTION_OFF, ENCRYPTION_ON, isEmptyObjectOrNull } +} = internal + +let _CONNECTION_IDGEN = 0 + +const TrustStrategy = { + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) { + if ( + !config.trustedCertificates || + config.trustedCertificates.length === 0 + ) { + onFailure( + newError( + 'You are using TRUST_CUSTOM_CA_SIGNED_CERTIFICATES as the method ' + + 'to verify trust for encrypted connections, but have not configured any ' + + 'trustedCertificates. You must specify the path to at least one trusted ' + + 'X.509 certificate for this to work. Two other alternatives is to use ' + + 'TRUST_ALL_CERTIFICATES or to disable encryption by setting encrypted="' + + ENCRYPTION_OFF + + '"' + + 'in your driver configuration.' + ) + ) + return + } + + const tlsOpts = newTlsOptions( + config.address.host(), + config.trustedCertificates.map(f => fs.readFileSync(f)) + ) + const socket = tls.connect( + config.address.port(), + config.address.resolvedHost(), + tlsOpts, + function () { + if (!socket.authorized) { + onFailure( + newError( + 'Server certificate is not trusted. If you trust the database you are connecting to, add' + + ' the signing certificate, or the server certificate, to the list of certificates trusted by this driver' + + " using `neo4j.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " + + ' is a security measure to protect against man-in-the-middle attacks. If you are just trying ' + + ' Neo4j out and are not concerned about encryption, simply disable it using `encrypted="' + + ENCRYPTION_OFF + + '"`' + + ' in the driver options. Socket responded with: ' + + socket.authorizationError + ) + ) + } else { + onSuccess() + } + } + ) + socket.on('error', onFailure) + return configureSocket(socket) + }, + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config, onSuccess, onFailure) { + const tlsOpts = newTlsOptions(config.address.host()) + const socket = tls.connect( + config.address.port(), + config.address.resolvedHost(), + tlsOpts, + function () { + if (!socket.authorized) { + onFailure( + newError( + 'Server certificate is not trusted. If you trust the database you are connecting to, use ' + + 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES and add' + + ' the signing certificate, or the server certificate, to the list of certificates trusted by this driver' + + " using `neo4j.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " + + ' is a security measure to protect against man-in-the-middle attacks. If you are just trying ' + + ' Neo4j out and are not concerned about encryption, simply disable it using `encrypted="' + + ENCRYPTION_OFF + + '"`' + + ' in the driver options. Socket responded with: ' + + socket.authorizationError + ) + ) + } else { + onSuccess() + } + } + ) + socket.on('error', onFailure) + return configureSocket(socket) + }, + TRUST_ALL_CERTIFICATES: function (config, onSuccess, onFailure) { + const tlsOpts = newTlsOptions(config.address.host()) + const socket = tls.connect( + config.address.port(), + config.address.resolvedHost(), + tlsOpts, + function () { + const certificate = socket.getPeerCertificate() + if (isEmptyObjectOrNull(certificate)) { + onFailure( + newError( + 'Secure connection was successful but server did not return any valid ' + + 'certificates. Such connection can not be trusted. If you are just trying ' + + ' Neo4j out and are not concerned about encryption, simply disable it using ' + + '`encrypted="' + + ENCRYPTION_OFF + + '"` in the driver options. ' + + 'Socket responded with: ' + + socket.authorizationError + ) + ) + } else { + onSuccess() + } + } + ) + socket.on('error', onFailure) + return configureSocket(socket) + } +} + +/** + * Connect using node socket. + * @param {ChannelConfig} config - configuration of this channel. + * @param {function} onSuccess - callback to execute on connection success. + * @param {function} onFailure - callback to execute on connection failure. + * @return {*} socket connection. + */ +function _connect (config, onSuccess, onFailure = () => null) { + const trustStrategy = trustStrategyName(config) + if (!isEncrypted(config)) { + const socket = net.connect( + config.address.port(), + config.address.resolvedHost(), + onSuccess + ) + socket.on('error', onFailure) + return configureSocket(socket) + } else if (TrustStrategy[trustStrategy]) { + return TrustStrategy[trustStrategy](config, onSuccess, onFailure) + } else { + onFailure( + newError( + 'Unknown trust strategy: ' + + config.trust + + '. Please use either ' + + "trust:'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' or trust:'TRUST_ALL_CERTIFICATES' in your driver " + + 'configuration. Alternatively, you can disable encryption by setting ' + + '`encrypted:"' + + ENCRYPTION_OFF + + '"`. There is no mechanism to use encryption without trust verification, ' + + 'because this incurs the overhead of encryption without improving security. If ' + + 'the driver does not verify that the peer it is connected to is really Neo4j, it ' + + 'is very easy for an attacker to bypass the encryption by pretending to be Neo4j.' + ) + ) + } +} + +function isEncrypted (config) { + const encryptionNotConfigured = + config.encrypted == null || config.encrypted === undefined + if (encryptionNotConfigured) { + // default to using encryption if trust-all-certificates is available + return false + } + return config.encrypted === true || config.encrypted === ENCRYPTION_ON +} + +function trustStrategyName (config) { + if (config.trust) { + return config.trust + } + return 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' +} + +/** + * Create a new configuration options object for the {@code tls.connect()} call. + * @param {string} hostname the target hostname. + * @param {string|undefined} ca an optional CA. + * @return {Object} a new options object. + */ +function newTlsOptions (hostname, ca = undefined) { + return { + rejectUnauthorized: false, // we manually check for this in the connect callback, to give a more helpful error to the user + servername: hostname, // server name for the SNI (Server Name Indication) TLS extension + ca: ca // optional CA useful for TRUST_CUSTOM_CA_SIGNED_CERTIFICATES trust mode + } +} + +/** + * Update socket options for the newly created socket. Accepts either `net.Socket` or its subclass `tls.TLSSocket`. + * @param {net.Socket} socket the socket to configure. + * @return {net.Socket} the given socket. + */ +function configureSocket (socket) { + socket.setKeepAlive(true) + return socket +} + +/** + * In a Node.js environment the 'net' module is used + * as transport. + * @access private + */ +export default class NodeChannel { + /** + * Create new instance + * @param {ChannelConfig} config - configuration for this channel. + */ + constructor (config, connect = _connect) { + const self = this + + this.id = _CONNECTION_IDGEN++ + this._pending = [] + this._open = true + this._error = null + this._handleConnectionError = this._handleConnectionError.bind(this) + this._handleConnectionTerminated = this._handleConnectionTerminated.bind( + this + ) + this._connectionErrorCode = config.connectionErrorCode + this._receiveTimeout = null + this._receiveTimeoutStarted = false + + this._conn = connect( + config, + () => { + if (!self._open) { + return + } + + self._conn.on('data', buffer => { + if (self.onmessage) { + self.onmessage(new ChannelBuffer(buffer)) + } + }) + + self._conn.on('error', self._handleConnectionError) + self._conn.on('end', self._handleConnectionTerminated) + + // Drain all pending messages + const pending = self._pending + self._pending = null + for (let i = 0; i < pending.length; i++) { + self.write(pending[i]) + } + }, + this._handleConnectionError + ) + + this._setupConnectionTimeout(config, this._conn) + } + + _handleConnectionError (err) { + let msg = + 'Failed to connect to server. ' + + 'Please ensure that your database is listening on the correct host and port ' + + 'and that you have compatible encryption settings both on Neo4j server and driver. ' + + 'Note that the default encryption setting has changed in Neo4j 4.0.' + if (err.message) msg += ' Caused by: ' + err.message + this._error = newError(msg, this._connectionErrorCode) + if (this.onerror) { + this.onerror(this._error) + } + } + + _handleConnectionTerminated () { + this._open = false + this._error = newError( + 'Connection was closed by server', + this._connectionErrorCode + ) + if (this.onerror) { + this.onerror(this._error) + } + } + + /** + * Setup connection timeout on the socket, if configured. + * @param {ChannelConfig} config - configuration of this channel. + * @param {Object} socket - `net.Socket` or `tls.TLSSocket` object. + * @private + */ + _setupConnectionTimeout (config, socket) { + const timeout = config.connectionTimeout + if (timeout) { + const connectListener = () => { + // connected - clear connection timeout + socket.setTimeout(0) + } + + const timeoutListener = () => { + // timeout fired - not connected within configured time. cancel timeout and destroy socket + socket.setTimeout(0) + socket.destroy( + newError( + `Failed to establish connection in ${timeout}ms`, + config.connectionErrorCode + ) + ) + } + + socket.on('connect', connectListener) + socket.on('timeout', timeoutListener) + + this._removeConnectionTimeoutListeners = () => { + this._conn.off('connect', connectListener) + this._conn.off('timeout', timeoutListener) + } + + socket.setTimeout(timeout) + } + } + + /** + * Setup the receive timeout for the channel. + * + * @param {number} receiveTimeout How long the channel will wait for receiving data before timing out (ms) + * @returns {void} + */ + setupReceiveTimeout (receiveTimeout) { + if (this._removeConnectionTimeoutListeners) { + this._removeConnectionTimeoutListeners() + } + + this._conn.on('timeout', () => { + this._conn.destroy( + newError( + `Connection lost. Server didn't respond in ${receiveTimeout}ms`, + this._connectionErrorCode + ) + ) + }) + + this._receiveTimeout = receiveTimeout + } + + /** + * Stops the receive timeout for the channel. + */ + stopReceiveTimeout () { + if (this._receiveTimeout !== null && this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = false + this._conn.setTimeout(0) + } + } + + /** + * Start the receive timeout for the channel. + */ + startReceiveTimeout () { + if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) { + this._receiveTimeoutStarted = true + this._conn.setTimeout(this._receiveTimeout) + } + } + + /** + * Write the passed in buffer to connection + * @param {ChannelBuffer} buffer - Buffer to write + */ + write (buffer) { + // If there is a pending queue, push this on that queue. This means + // we are not yet connected, so we queue things locally. + if (this._pending !== null) { + this._pending.push(buffer) + } else if (buffer instanceof ChannelBuffer) { + this._conn.write(buffer._buffer) + } else { + throw newError("Don't know how to write: " + buffer) + } + } + + /** + * Close the connection + * @returns {Promise} A promise that will be resolved after channel is closed + */ + close () { + return new Promise((resolve, reject) => { + const cleanup = () => { + if (!this._conn.destroyed) { + this._conn.destroy() + } + + resolve() + } + + if (this._open) { + this._open = false + this._conn.removeListener('end', this._handleConnectionTerminated) + this._conn.on('end', () => cleanup()) + this._conn.on('close', () => cleanup()) + this._conn.end() + this._conn.destroy() + } else { + cleanup() + } + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-host-name-resolver.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-host-name-resolver.js new file mode 100644 index 000000000..6b1040bdd --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/node/node-host-name-resolver.js @@ -0,0 +1,42 @@ +/** + * 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 nodeDns from 'dns' +import { internal } from '../../../core/index.ts' + +const { + resolver: { BaseHostNameResolver } +} = internal + +export default class NodeHostNameResolver extends BaseHostNameResolver { + resolve (address) { + return new Promise(resolve => { + nodeDns.lookup(address.host(), { all: true }, (error, resolvedTo) => { + if (error) { + resolve([address]) + } else { + const resolvedAddresses = resolvedTo.map(a => + address.resolveWith(a.address) + ) + resolve(resolvedAddresses) + } + }) + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/utf8.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/utf8.js new file mode 100644 index 000000000..4a6eb8eb8 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/utf8.js @@ -0,0 +1,104 @@ +/** + * 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 ChannelBuffer from './channel-buf.js' +import { newError } from '../../core/index.ts' +import buffer from 'https://deno.land/std@0.119.0/node/buffer.ts' +import { StringDecoder } from 'https://deno.land/std@0.119.0/node/string_decoder.ts' + +const decoder = new StringDecoder('utf8') + +function encode (str) { + return new ChannelBuffer(newBuffer(str)) +} + +function decode (buffer, length) { + if (Object.prototype.hasOwnProperty.call(buffer, '_buffer')) { + return decodeChannelBuffer(buffer, length) + } else if (Object.prototype.hasOwnProperty.call(buffer, '_buffers')) { + return decodeCombinedBuffer(buffer, length) + } else { + throw newError(`Don't know how to decode strings from '${buffer}'`) + } +} + +function decodeChannelBuffer (buffer, length) { + const start = buffer.position + const end = start + length + buffer.position = Math.min(end, buffer.length) + return buffer._buffer.toString('utf8', start, end) +} + +function decodeCombinedBuffer (buffer, length) { + return streamDecodeCombinedBuffer( + buffer, + length, + partBuffer => decoder.write(partBuffer._buffer), + () => decoder.end() + ) +} + +function streamDecodeCombinedBuffer (combinedBuffers, length, decodeFn, endFn) { + let remainingBytesToRead = length + let position = combinedBuffers.position + combinedBuffers._updatePos( + Math.min(length, combinedBuffers.length - position) + ) + // Reduce CombinedBuffers to a decoded string + const out = combinedBuffers._buffers.reduce(function (last, partBuffer) { + if (remainingBytesToRead <= 0) { + return last + } else if (position >= partBuffer.length) { + position -= partBuffer.length + return '' + } else { + partBuffer._updatePos(position - partBuffer.position) + const bytesToRead = Math.min( + partBuffer.length - position, + remainingBytesToRead + ) + const lastSlice = partBuffer.readSlice(bytesToRead) + partBuffer._updatePos(bytesToRead) + remainingBytesToRead = Math.max( + remainingBytesToRead - lastSlice.length, + 0 + ) + position = 0 + return last + decodeFn(lastSlice) + } + }, '') + return out + endFn() +} + +function newBuffer (str) { + // use static factory function present in newer NodeJS versions to create a buffer containing the given string + // or fallback to the old, potentially deprecated constructor + + if (typeof buffer.Buffer.from === 'function') { + return buffer.Buffer.from(str, 'utf8') + } else { + // eslint-disable-next-line node/no-deprecated-api + return new buffer.Buffer(str, 'utf8') + } +} + +export default { + encode, + decode +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js new file mode 100644 index 000000000..9895701c7 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -0,0 +1,117 @@ +/** + * 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 PooledConnectionProvider from './connection-provider-pooled.js' +import { + createChannelConnection, + DelegateConnection, + ConnectionErrorHandler +} from '../connection/index.js' +import { internal, error } from '../../core/index.ts' + +const { + constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } +} = internal + +const { SERVICE_UNAVAILABLE } = error + +export default class DirectConnectionProvider extends PooledConnectionProvider { + constructor ({ id, config, log, address, userAgent, authToken }) { + super({ id, config, log, userAgent, authToken }) + + this._address = address + } + + /** + * See {@link ConnectionProvider} for more information about this method and + * its arguments. + */ + acquireConnection ({ accessMode, database, bookmarks } = {}) { + const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ + errorCode: SERVICE_UNAVAILABLE, + handleAuthorizationExpired: (error, address) => + this._handleAuthorizationExpired(error, address, database) + }) + + return this._connectionPool + .acquire(this._address) + .then( + connection => + new DelegateConnection(connection, databaseSpecificErrorHandler) + ) + } + + _handleAuthorizationExpired (error, address, database) { + this._log.warn( + `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` + ) + this._connectionPool.purge(address).catch(() => {}) + return error + } + + async _hasProtocolVersion (versionPredicate) { + const connection = await createChannelConnection( + this._address, + this._config, + this._createConnectionErrorHandler(), + this._log + ) + + const protocolVersion = connection.protocol() + ? connection.protocol().version + : null + + await connection.close() + + if (protocolVersion) { + return versionPredicate(protocolVersion) + } + + return false + } + + async supportsMultiDb () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V4_0 + ) + } + + getNegotiatedProtocolVersion () { + return new Promise((resolve, reject) => { + this._hasProtocolVersion(resolve) + .catch(reject) + }) + } + + async supportsTransactionConfig () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V3 + ) + } + + async supportsUserImpersonation () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V4_4 + ) + } + + async verifyConnectivityAndGetServerInfo () { + return await this._verifyConnectivityAndGetServerVersion({ address: this._address }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js new file mode 100644 index 000000000..208cbd585 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -0,0 +1,149 @@ +/** + * 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 { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' +import Pool, { PoolConfig } from '../pool/index.js' +import { error, ConnectionProvider, ServerInfo } from '../../core/index.ts' + +const { SERVICE_UNAVAILABLE } = error +export default class PooledConnectionProvider extends ConnectionProvider { + constructor ( + { id, config, log, userAgent, authToken }, + createChannelConnectionHook = null + ) { + super() + + this._id = id + this._config = config + this._log = log + this._userAgent = userAgent + this._authToken = authToken + this._createChannelConnection = + createChannelConnectionHook || + (address => { + return createChannelConnection( + address, + this._config, + this._createConnectionErrorHandler(), + this._log + ) + }) + this._connectionPool = new Pool({ + create: this._createConnection.bind(this), + destroy: this._destroyConnection.bind(this), + validate: this._validateConnection.bind(this), + installIdleObserver: PooledConnectionProvider._installIdleObserverOnConnection.bind( + this + ), + removeIdleObserver: PooledConnectionProvider._removeIdleObserverOnConnection.bind( + this + ), + config: PoolConfig.fromDriverConfig(config), + log: this._log + }) + this._openConnections = {} + } + + _createConnectionErrorHandler () { + return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) + } + + /** + * Create a new connection and initialize it. + * @return {Promise} promise resolved with a new connection or rejected when failed to connect. + * @access private + */ + _createConnection (address, release) { + return this._createChannelConnection(address).then(connection => { + connection._release = () => { + return release(address, connection) + } + this._openConnections[connection.id] = connection + return connection + .connect(this._userAgent, this._authToken) + .catch(error => { + // let's destroy this connection + this._destroyConnection(connection) + // propagate the error because connection failed to connect / initialize + throw error + }) + }) + } + + /** + * Check that a connection is usable + * @return {boolean} true if the connection is open + * @access private + **/ + _validateConnection (conn) { + if (!conn.isOpen()) { + return false + } + + const maxConnectionLifetime = this._config.maxConnectionLifetime + const lifetime = Date.now() - conn.creationTimestamp + return lifetime <= maxConnectionLifetime + } + + /** + * Dispose of a connection. + * @return {Connection} the connection to dispose. + * @access private + */ + _destroyConnection (conn) { + delete this._openConnections[conn.id] + return conn.close() + } + + /** + * Acquire a connection from the pool and return it ServerInfo + * @param {object} param + * @param {string} param.address the server address + * @return {Promise} the server info + */ + async _verifyConnectivityAndGetServerVersion ({ address }) { + const connection = await this._connectionPool.acquire(address) + const serverInfo = new ServerInfo(connection.server, connection.protocol().version) + try { + if (!connection.protocol().isLastMessageLogin()) { + await connection.resetAndFlush() + } + } finally { + await connection._release() + } + return serverInfo + } + + async close () { + // purge all idle connections in the connection pool + await this._connectionPool.close() + + // then close all connections driver has ever created + // it is needed to close connections that are active right now and are acquired from the pool + await Promise.all(Object.values(this._openConnections).map(c => c.close())) + } + + static _installIdleObserverOnConnection (conn, observer) { + conn._queueObserver(observer) + } + + static _removeIdleObserverOnConnection (conn) { + conn._updateCurrentObserver() + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js new file mode 100644 index 000000000..d07010cfc --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -0,0 +1,715 @@ +/** + * 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 { newError, error, int, Session, internal } from '../../core/index.ts' +import Rediscovery, { RoutingTable } from '../rediscovery/index.js' +import { HostNameResolver } from '../channel/index.js' +import SingleConnectionProvider from './connection-provider-single.js' +import PooledConnectionProvider from './connection-provider-pooled.js' +import { LeastConnectedLoadBalancingStrategy } from '../load-balancing/index.js' +import { + createChannelConnection, + ConnectionErrorHandler, + DelegateConnection +} from '../connection/index.js' + +const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error +const { + bookmarks: { Bookmarks }, + constants: { + ACCESS_MODE_READ: READ, + ACCESS_MODE_WRITE: WRITE, + BOLT_PROTOCOL_V3, + BOLT_PROTOCOL_V4_0, + BOLT_PROTOCOL_V4_4 + } +} = internal + +const PROCEDURE_NOT_FOUND_CODE = 'Neo.ClientError.Procedure.ProcedureNotFound' +const DATABASE_NOT_FOUND_CODE = 'Neo.ClientError.Database.DatabaseNotFound' +const INVALID_BOOKMARK_CODE = 'Neo.ClientError.Transaction.InvalidBookmark' +const INVALID_BOOKMARK_MIXTURE_CODE = + 'Neo.ClientError.Transaction.InvalidBookmarkMixture' +const AUTHORIZATION_EXPIRED_CODE = + 'Neo.ClientError.Security.AuthorizationExpired' + +const SYSTEM_DB_NAME = 'system' +const DEFAULT_DB_NAME = null +const DEFAULT_ROUTING_TABLE_PURGE_DELAY = int(30000) + +export default class RoutingConnectionProvider extends PooledConnectionProvider { + constructor ({ + id, + address, + routingContext, + hostNameResolver, + config, + log, + userAgent, + authToken, + routingTablePurgeDelay + }) { + super({ id, config, log, userAgent, authToken }, address => { + return createChannelConnection( + address, + this._config, + this._createConnectionErrorHandler(), + this._log, + this._routingContext + ) + }) + + this._routingContext = { ...routingContext, address: address.toString() } + this._seedRouter = address + this._rediscovery = new Rediscovery(this._routingContext) + this._loadBalancingStrategy = new LeastConnectedLoadBalancingStrategy( + this._connectionPool + ) + this._hostNameResolver = hostNameResolver + this._dnsResolver = new HostNameResolver() + this._log = log + this._useSeedRouter = true + this._routingTableRegistry = new RoutingTableRegistry( + routingTablePurgeDelay + ? int(routingTablePurgeDelay) + : DEFAULT_ROUTING_TABLE_PURGE_DELAY + ) + } + + _createConnectionErrorHandler () { + // connection errors mean SERVICE_UNAVAILABLE for direct driver but for routing driver they should only + // result in SESSION_EXPIRED because there might still exist other servers capable of serving the request + return new ConnectionErrorHandler(SESSION_EXPIRED) + } + + _handleUnavailability (error, address, database) { + this._log.warn( + `Routing driver ${this._id} will forget ${address} for database '${database}' because of an error ${error.code} '${error.message}'` + ) + this.forget(address, database || DEFAULT_DB_NAME) + return error + } + + _handleAuthorizationExpired (error, address, database) { + this._log.warn( + `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` + ) + this._connectionPool.purge(address).catch(() => {}) + return error + } + + _handleWriteFailure (error, address, database) { + this._log.warn( + `Routing driver ${this._id} will forget writer ${address} for database '${database}' because of an error ${error.code} '${error.message}'` + ) + this.forgetWriter(address, database || DEFAULT_DB_NAME) + return newError( + 'No longer possible to write to server at ' + address, + SESSION_EXPIRED, + error + ) + } + + /** + * See {@link ConnectionProvider} for more information about this method and + * its arguments. + */ + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + let name + let address + const context = { database: database || DEFAULT_DB_NAME } + + const databaseSpecificErrorHandler = new ConnectionErrorHandler( + SESSION_EXPIRED, + (error, address) => this._handleUnavailability(error, address, context.database), + (error, address) => this._handleWriteFailure(error, address, context.database), + (error, address) => + this._handleAuthorizationExpired(error, address, context.database) + ) + + const routingTable = await this._freshRoutingTable({ + accessMode, + database: context.database, + bookmarks, + impersonatedUser, + onDatabaseNameResolved: (databaseName) => { + context.database = context.database || databaseName + if (onDatabaseNameResolved) { + onDatabaseNameResolved(databaseName) + } + } + }) + + // select a target server based on specified access mode + if (accessMode === READ) { + address = this._loadBalancingStrategy.selectReader(routingTable.readers) + name = 'read' + } else if (accessMode === WRITE) { + address = this._loadBalancingStrategy.selectWriter(routingTable.writers) + name = 'write' + } else { + throw newError('Illegal mode ' + accessMode) + } + + // we couldn't select a target server + if (!address) { + throw newError( + `Failed to obtain connection towards ${name} server. Known routing table is: ${routingTable}`, + SESSION_EXPIRED + ) + } + + try { + const connection = await this._acquireConnectionToServer( + address, + name, + routingTable + ) + + return new DelegateConnection(connection, databaseSpecificErrorHandler) + } catch (error) { + const transformed = databaseSpecificErrorHandler.handleAndTransformError( + error, + address + ) + throw transformed + } + } + + async _hasProtocolVersion (versionPredicate) { + const addresses = await this._resolveSeedRouter(this._seedRouter) + + let lastError + for (let i = 0; i < addresses.length; i++) { + try { + const connection = await createChannelConnection( + addresses[i], + this._config, + this._createConnectionErrorHandler(), + this._log + ) + const protocolVersion = connection.protocol() + ? connection.protocol().version + : null + + await connection.close() + + if (protocolVersion) { + return versionPredicate(protocolVersion) + } + + return false + } catch (error) { + lastError = error + } + } + + if (lastError) { + throw lastError + } + + return false + } + + async supportsMultiDb () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V4_0 + ) + } + + async supportsTransactionConfig () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V3 + ) + } + + async supportsUserImpersonation () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V4_4 + ) + } + + getNegotiatedProtocolVersion () { + return new Promise((resolve, reject) => { + this._hasProtocolVersion(resolve) + .catch(reject) + }) + } + + async verifyConnectivityAndGetServerInfo ({ database, accessMode }) { + const context = { database: database || DEFAULT_DB_NAME } + + const routingTable = await this._freshRoutingTable({ + accessMode, + database: context.database, + onDatabaseNameResolved: (databaseName) => { + context.database = context.database || databaseName + } + }) + + const servers = accessMode === WRITE ? routingTable.writers : routingTable.readers + + let error = newError( + `No servers available for database '${context.database}' with access mode '${accessMode}'`, + SERVICE_UNAVAILABLE + ) + + for (const address of servers) { + try { + const serverInfo = await this._verifyConnectivityAndGetServerVersion({ address }) + return serverInfo + } catch (e) { + error = e + } + } + throw error + } + + forget (address, database) { + this._routingTableRegistry.apply(database, { + applyWhenExists: routingTable => routingTable.forget(address) + }) + + // We're firing and forgetting this operation explicitly and listening for any + // errors to avoid unhandled promise rejection + this._connectionPool.purge(address).catch(() => {}) + } + + forgetWriter (address, database) { + this._routingTableRegistry.apply(database, { + applyWhenExists: routingTable => routingTable.forgetWriter(address) + }) + } + + _acquireConnectionToServer (address, serverName, routingTable) { + return this._connectionPool.acquire(address) + } + + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + const currentRoutingTable = this._routingTableRegistry.get( + database, + () => new RoutingTable({ database }) + ) + + if (!currentRoutingTable.isStaleFor(accessMode)) { + return currentRoutingTable + } + this._log.info( + `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` + ) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) + } + + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) { + const knownRouters = currentRoutingTable.routers + + if (this._useSeedRouter) { + return this._fetchRoutingTableFromSeedRouterFallbackToKnownRouters( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser, + onDatabaseNameResolved + ) + } + return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser, + onDatabaseNameResolved + ) + } + + async _fetchRoutingTableFromSeedRouterFallbackToKnownRouters ( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser, + onDatabaseNameResolved + ) { + // we start with seed router, no routers were probed before + const seenRouters = [] + let [newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter( + seenRouters, + this._seedRouter, + currentRoutingTable, + bookmarks, + impersonatedUser + ) + + if (newRoutingTable) { + this._useSeedRouter = false + } else { + // seed router did not return a valid routing table - try to use other known routers + const [newRoutingTable2, error2] = await this._fetchRoutingTableUsingKnownRouters( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser + ) + newRoutingTable = newRoutingTable2 + error = error2 || error + } + + return await this._applyRoutingTableIfPossible( + currentRoutingTable, + newRoutingTable, + onDatabaseNameResolved, + error + ) + } + + async _fetchRoutingTableFromKnownRoutersFallbackToSeedRouter ( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser, + onDatabaseNameResolved + ) { + let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser + ) + + if (!newRoutingTable) { + // none of the known routers returned a valid routing table - try to use seed router address for rediscovery + [newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter( + knownRouters, + this._seedRouter, + currentRoutingTable, + bookmarks, + impersonatedUser + ) + } + + return await this._applyRoutingTableIfPossible( + currentRoutingTable, + newRoutingTable, + onDatabaseNameResolved, + error + ) + } + + async _fetchRoutingTableUsingKnownRouters ( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser + ) { + const [newRoutingTable, error] = await this._fetchRoutingTable( + knownRouters, + currentRoutingTable, + bookmarks, + impersonatedUser + ) + + if (newRoutingTable) { + // one of the known routers returned a valid routing table - use it + return [newRoutingTable, null] + } + + // returned routing table was undefined, this means a connection error happened and the last known + // router did not return a valid routing table, so we need to forget it + const lastRouterIndex = knownRouters.length - 1 + RoutingConnectionProvider._forgetRouter( + currentRoutingTable, + knownRouters, + lastRouterIndex + ) + + return [null, error] + } + + async _fetchRoutingTableUsingSeedRouter ( + seenRouters, + seedRouter, + routingTable, + bookmarks, + impersonatedUser + ) { + const resolvedAddresses = await this._resolveSeedRouter(seedRouter) + + // filter out all addresses that we've already tried + const newAddresses = resolvedAddresses.filter( + address => seenRouters.indexOf(address) < 0 + ) + + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser) + } + + async _resolveSeedRouter (seedRouter) { + const resolvedAddresses = await this._hostNameResolver.resolve(seedRouter) + const dnsResolvedAddresses = await Promise.all( + resolvedAddresses.map(address => this._dnsResolver.resolve(address)) + ) + + return [].concat.apply([], dnsResolvedAddresses) + } + + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) { + return routerAddresses.reduce( + async (refreshedTablePromise, currentRouter, currentIndex) => { + const [newRoutingTable] = await refreshedTablePromise + + if (newRoutingTable) { + // valid routing table was fetched - just return it, try next router otherwise + return [newRoutingTable, null] + } else { + // returned routing table was undefined, this means a connection error happened and we need to forget the + // previous router and try the next one + const previousRouterIndex = currentIndex - 1 + RoutingConnectionProvider._forgetRouter( + routingTable, + routerAddresses, + previousRouterIndex + ) + } + + // try next router + const [session, error] = await this._createSessionForRediscovery( + currentRouter, + bookmarks, + impersonatedUser + ) + if (session) { + try { + return [await this._rediscovery.lookupRoutingTableOnRouter( + session, + routingTable.database, + currentRouter, + impersonatedUser + ), null] + } catch (error) { + return this._handleRediscoveryError(error, currentRouter) + } finally { + session.close() + } + } else { + // unable to acquire connection and create session towards the current router + // return null to signal that the next router should be tried + return [null, error] + } + }, + Promise.resolve([null, null]) + ) + } + + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser) { + try { + const connection = await this._connectionPool.acquire(routerAddress) + + const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ + errorCode: SESSION_EXPIRED, + handleAuthorizationExpired: (error, address) => this._handleAuthorizationExpired(error, address) + }) + + const connectionProvider = new SingleConnectionProvider( + new DelegateConnection(connection, databaseSpecificErrorHandler)) + + const protocolVersion = connection.protocol().version + if (protocolVersion < 4.0) { + return [new Session({ + mode: WRITE, + bookmarks: Bookmarks.empty(), + connectionProvider + }), null] + } + + return [new Session({ + mode: READ, + database: SYSTEM_DB_NAME, + bookmarks, + connectionProvider, + impersonatedUser + }), null] + } catch (error) { + return this._handleRediscoveryError(error, routerAddress) + } + } + + _handleRediscoveryError (error, routerAddress) { + if (_isFailFastError(error) || _isFailFastSecurityError(error)) { + throw error + } else if (error.code === PROCEDURE_NOT_FOUND_CODE) { + // throw when getServers procedure not found because this is clearly a configuration issue + throw newError( + `Server at ${routerAddress.asHostPort()} can't perform routing. Make sure you are connecting to a causal cluster`, + SERVICE_UNAVAILABLE, + error + ) + } + this._log.warn( + `unable to fetch routing table because of an error ${error}` + ) + return [null, error] + } + + async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable, onDatabaseNameResolved, error) { + if (!newRoutingTable) { + // none of routing servers returned valid routing table, throw exception + throw newError( + `Could not perform discovery. No routing servers available. Known routing table: ${currentRoutingTable}`, + SERVICE_UNAVAILABLE, + error + ) + } + + if (newRoutingTable.writers.length === 0) { + // use seed router next time. this is important when cluster is partitioned. it tries to make sure driver + // does not always get routing table without writers because it talks exclusively to a minority partition + this._useSeedRouter = true + } + + await this._updateRoutingTable(newRoutingTable, onDatabaseNameResolved) + + return newRoutingTable + } + + async _updateRoutingTable (newRoutingTable, onDatabaseNameResolved) { + // close old connections to servers not present in the new routing table + await this._connectionPool.keepAll(newRoutingTable.allServers()) + this._routingTableRegistry.removeExpired() + this._routingTableRegistry.register( + newRoutingTable + ) + + onDatabaseNameResolved(newRoutingTable.database) + + this._log.info(`Updated routing table ${newRoutingTable}`) + } + + static _forgetRouter (routingTable, routersArray, routerIndex) { + const address = routersArray[routerIndex] + if (routingTable && address) { + routingTable.forgetRouter(address) + } + } +} + +/** + * Responsible for keeping track of the existing routing tables + */ +class RoutingTableRegistry { + /** + * Constructor + * @param {int} routingTablePurgeDelay The routing table purge delay + */ + constructor (routingTablePurgeDelay) { + this._tables = new Map() + this._routingTablePurgeDelay = routingTablePurgeDelay + } + + /** + * Put a routing table in the registry + * + * @param {RoutingTable} table The routing table + * @returns {RoutingTableRegistry} this + */ + register (table) { + this._tables.set(table.database, table) + return this + } + + /** + * Apply function in the routing table for an specific database. If the database name is not defined, the function will + * be applied for each element + * + * @param {string} database The database name + * @param {object} callbacks The actions + * @param {function (RoutingTable)} callbacks.applyWhenExists Call when the db exists or when the database property is not informed + * @param {function ()} callbacks.applyWhenDontExists Call when the database doesn't have the routing table registred + * @returns {RoutingTableRegistry} this + */ + apply (database, { applyWhenExists, applyWhenDontExists = () => {} } = {}) { + if (this._tables.has(database)) { + applyWhenExists(this._tables.get(database)) + } else if (typeof database === 'string' || database === null) { + applyWhenDontExists() + } else { + this._forEach(applyWhenExists) + } + return this + } + + /** + * Retrieves a routing table from a given database name + * + * @param {string|impersonatedUser} impersonatedUser The impersonated User + * @param {string} database The database name + * @param {function()|RoutingTable} defaultSupplier The routing table supplier, if it's not a function or not exists, it will return itself as default value + * @returns {RoutingTable} The routing table for the respective database + */ + get (database, defaultSupplier) { + if (this._tables.has(database)) { + return this._tables.get(database) + } + return typeof defaultSupplier === 'function' + ? defaultSupplier() + : defaultSupplier + } + + /** + * Remove the routing table which is already expired + * @returns {RoutingTableRegistry} this + */ + removeExpired () { + return this._removeIf(value => + value.isExpiredFor(this._routingTablePurgeDelay) + ) + } + + _forEach (apply) { + for (const [, value] of this._tables) { + apply(value) + } + return this + } + + _remove (key) { + this._tables.delete(key) + return this + } + + _removeIf (predicate) { + for (const [key, value] of this._tables) { + if (predicate(value)) { + this._remove(key) + } + } + return this + } +} + +function _isFailFastError (error) { + return [ + DATABASE_NOT_FOUND_CODE, + INVALID_BOOKMARK_CODE, + INVALID_BOOKMARK_MIXTURE_CODE + ].includes(error.code) +} + +function _isFailFastSecurityError (error) { + return error.code.startsWith('Neo.ClientError.Security.') && + ![ + AUTHORIZATION_EXPIRED_CODE + ].includes(error.code) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-single.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-single.js new file mode 100644 index 000000000..52b618725 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-single.js @@ -0,0 +1,37 @@ +/** + * 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 { ConnectionProvider } from '../../core/index.ts' + +export default class SingleConnectionProvider extends ConnectionProvider { + constructor (connection) { + super() + this._connection = connection + } + + /** + * See {@link ConnectionProvider} for more information about this method and + * its arguments. + */ + acquireConnection ({ accessMode, database, bookmarks } = {}) { + const connection = this._connection + this._connection = null + return Promise.resolve(connection) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/index.js new file mode 100644 index 000000000..e6d230b1f --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/index.js @@ -0,0 +1,22 @@ +/** + * 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. + */ +export { default as SingleConnectionProvider } from './connection-provider-single.js' +export { default as PooledConnectionProvider } from './connection-provider-pooled.js' +export { default as DirectConnectionProvider } from './connection-provider-direct.js' +export { default as RoutingConnectionProvider } from './connection-provider-routing.js' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js new file mode 100644 index 000000000..8dbcbc509 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -0,0 +1,448 @@ +/** + * 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 { Chunker, Dechunker, ChannelConfig, Channel } from '../channel/index.js' +import { newError, error, json, internal, toNumber } from '../../core/index.ts' +import Connection from './connection.js' +import Bolt from '../bolt/index.js' + +const { PROTOCOL_ERROR } = error +const { + logger: { Logger } +} = internal + +let idGenerator = 0 + +/** + * Crete new connection to the provided address. Returned connection is not connected. + * @param {ServerAddress} address - the Bolt endpoint to connect to. + * @param {Object} config - the driver configuration. + * @param {ConnectionErrorHandler} errorHandler - the error handler for connection errors. + * @param {Logger} log - configured logger. + * @return {Connection} - new connection. + */ +export function createChannelConnection ( + address, + config, + errorHandler, + log, + serversideRouting = null, + createChannel = channelConfig => new Channel(channelConfig) +) { + const channelConfig = new ChannelConfig( + address, + config, + errorHandler.errorCode() + ) + + const channel = createChannel(channelConfig) + + return Bolt.handshake(channel) + .then(({ protocolVersion: version, consumeRemainingBuffer }) => { + const chunker = new Chunker(channel) + const dechunker = new Dechunker() + const createProtocol = conn => + Bolt.create({ + version, + channel, + chunker, + dechunker, + disableLosslessIntegers: config.disableLosslessIntegers, + useBigInt: config.useBigInt, + serversideRouting, + server: conn.server, + log: conn.logger, + observer: { + onPendingObserversChange: conn._handleOngoingRequestsNumberChange.bind(conn), + onError: conn._handleFatalError.bind(conn), + onFailure: conn._resetOnFailure.bind(conn), + onProtocolError: conn._handleProtocolError.bind(conn), + onErrorApplyTransformation: error => + conn.handleAndTransformError(error, conn._address) + } + }) + + const connection = new ChannelConnection( + channel, + errorHandler, + address, + log, + config.disableLosslessIntegers, + serversideRouting, + chunker, + createProtocol + ) + + // forward all pending bytes to the dechunker + consumeRemainingBuffer(buffer => dechunker.write(buffer)) + + return connection + }) + .catch(reason => + channel.close().then(() => { + throw reason + }) + ) +} +export default class ChannelConnection extends Connection { + /** + * @constructor + * @param {Channel} channel - channel with a 'write' function and a 'onmessage' callback property. + * @param {ConnectionErrorHandler} errorHandler the error handler. + * @param {ServerAddress} address - the server address to connect to. + * @param {Logger} log - the configured logger. + * @param {boolean} disableLosslessIntegers if this connection should convert all received integers to native JS numbers. + * @param {Chunker} chunker the chunker + * @param protocolSupplier Bolt protocol supplier + */ + constructor ( + channel, + errorHandler, + address, + log, + disableLosslessIntegers = false, + serversideRouting = null, + chunker, // to be removed, + protocolSupplier + ) { + super(errorHandler) + + this._reseting = false + this._resetObservers = [] + this._id = idGenerator++ + this._address = address + this._server = { address: address.asHostPort() } + this.creationTimestamp = Date.now() + this._disableLosslessIntegers = disableLosslessIntegers + this._ch = channel + this._chunker = chunker + this._log = createConnectionLogger(this, log) + this._serversideRouting = serversideRouting + + // connection from the database, returned in response for HELLO message and might not be available + this._dbConnectionId = null + + // bolt protocol is initially not initialized + /** + * @private + * @type {BoltProtocol} + */ + this._protocol = protocolSupplier(this) + + // Set to true on fatal errors, to get this out of connection pool. + this._isBroken = false + + if (this._log.isDebugEnabled()) { + this._log.debug(`created towards ${address}`) + } + } + + get id () { + return this._id + } + + get databaseId () { + return this._dbConnectionId + } + + set databaseId (value) { + this._dbConnectionId = value + } + + /** + * Send initialization message. + * @param {string} userAgent the user agent for this driver. + * @param {Object} authToken the object containing auth information. + * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. + */ + connect (userAgent, authToken) { + return this._initialize(userAgent, authToken) + } + + /** + * Perform protocol-specific initialization which includes authentication. + * @param {string} userAgent the user agent for this driver. + * @param {Object} authToken the object containing auth information. + * @return {Promise} promise resolved with the current connection if initialization is successful. Rejected promise otherwise. + */ + _initialize (userAgent, authToken) { + const self = this + return new Promise((resolve, reject) => { + this._protocol.initialize({ + userAgent, + authToken, + onError: err => reject(err), + onComplete: metadata => { + if (metadata) { + // read server version from the response metadata, if it is available + const serverVersion = metadata.server + if (!this.version || serverVersion) { + this.version = serverVersion + } + + // read database connection id from the response metadata, if it is available + const dbConnectionId = metadata.connection_id + if (!this.databaseId) { + this.databaseId = dbConnectionId + } + + if (metadata.hints) { + const receiveTimeoutRaw = + metadata.hints['connection.recv_timeout_seconds'] + if ( + receiveTimeoutRaw !== null && + receiveTimeoutRaw !== undefined + ) { + const receiveTimeoutInSeconds = toNumber(receiveTimeoutRaw) + if ( + Number.isInteger(receiveTimeoutInSeconds) && + receiveTimeoutInSeconds > 0 + ) { + this._ch.setupReceiveTimeout(receiveTimeoutInSeconds * 1000) + } else { + this._log.info( + `Server located at ${this._address} supplied an invalid connection receive timeout value (${receiveTimeoutInSeconds}). ` + + 'Please, verify the server configuration and status because this can be the symptom of a bigger issue.' + ) + } + } + } + } + resolve(self) + } + }) + }) + } + + /** + * Get the Bolt protocol for the connection. + * @return {BoltProtocol} the protocol. + */ + protocol () { + return this._protocol + } + + get address () { + return this._address + } + + /** + * Get the version of the connected server. + * Available only after initialization + * + * @returns {ServerVersion} version + */ + get version () { + return this._server.version + } + + set version (value) { + this._server.version = value + } + + get server () { + return this._server + } + + get logger () { + return this._log + } + + /** + * "Fatal" means the connection is dead. Only call this if something + * happens that cannot be recovered from. This will lead to all subscribers + * failing, and the connection getting ejected from the session pool. + * + * @param error an error object, forwarded to all current and future subscribers + */ + _handleFatalError (error) { + this._isBroken = true + this._error = this.handleAndTransformError( + this._protocol.currentFailure || error, + this._address + ) + + if (this._log.isErrorEnabled()) { + this._log.error( + `experienced a fatal error caused by ${this._error} (${json.stringify(this._error)})` + ) + } + + this._protocol.notifyFatalError(this._error) + } + + /** + * This method still here because it's used by the {@link PooledConnectionProvider} + * + * @param {any} observer + */ + _queueObserver (observer) { + return this._protocol.queueObserverIfProtocolIsNotBroken(observer) + } + + hasOngoingObservableRequests () { + return this._protocol.hasOngoingObservableRequests() + } + + /** + * Send a RESET-message to the database. Message is immediately flushed to the network. + * @return {Promise} promise resolved when SUCCESS-message response arrives, or failed when other response messages arrives. + */ + resetAndFlush () { + return new Promise((resolve, reject) => { + this._reset({ + onError: error => { + if (this._isBroken) { + // handling a fatal error, no need to raise a protocol violation + reject(error) + } else { + const neo4jError = this._handleProtocolError( + 'Received FAILURE as a response for RESET: ' + error + ) + reject(neo4jError) + } + }, + onComplete: () => { + resolve() + } + }) + }) + } + + _resetOnFailure () { + if (!this.isOpen()) { + return + } + + this._reset({ + onError: () => { + this._protocol.resetFailure() + }, + onComplete: () => { + this._protocol.resetFailure() + } + }) + } + + _reset(observer) { + if (this._reseting) { + if (!this._protocol.isLastMessageReset()) { + this._protocol.reset({ + onError: error => { + observer.onError(error) + }, onComplete: () => { + observer.onComplete() + } + }) + } else { + this._resetObservers.push(observer) + } + return + } + + this._resetObservers.push(observer) + this._reseting = true + + const notifyFinish = (notify) => { + this._reseting = false + const observers = this._resetObservers + this._resetObservers = [] + observers.forEach(notify) + } + + this._protocol.reset({ + onError: error => { + notifyFinish(obs => obs.onError(error)) + }, onComplete: () => { + notifyFinish(obs => obs.onComplete()) + } + }) + } + + /* + * Pop next pending observer form the list of observers and make it current observer. + * @protected + */ + _updateCurrentObserver () { + this._protocol.updateCurrentObserver() + } + + /** Check if this connection is in working condition */ + isOpen () { + return !this._isBroken && this._ch._open + } + + /** + * Starts and stops the receive timeout timer. + * @param {number} requestsNumber Ongoing requests number + */ + _handleOngoingRequestsNumberChange (requestsNumber) { + if (requestsNumber === 0) { + this._ch.stopReceiveTimeout() + } else { + this._ch.startReceiveTimeout() + } + } + + /** + * Call close on the channel. + * @returns {Promise} - A promise that will be resolved when the underlying channel is closed. + */ + async close () { + if (this._log.isDebugEnabled()) { + this._log.debug('closing') + } + + if (this._protocol && this.isOpen()) { + // protocol has been initialized and this connection is healthy + // notify the database about the upcoming close of the connection + this._protocol.prepareToClose() + } + + await this._ch.close() + + if (this._log.isDebugEnabled()) { + this._log.debug('closed') + } + } + + toString () { + return `Connection [${this.id}][${this.databaseId || ''}]` + } + + _handleProtocolError (message) { + this._protocol.resetFailure() + this._updateCurrentObserver() + const error = newError(message, PROTOCOL_ERROR) + this._handleFatalError(error) + return error + } +} + +/** + * Creates a log with the connection info as prefix + * @param {Connection} connection The connection + * @param {Logger} logger The logger + * @returns {Logger} The new logger with enriched messages + */ +function createConnectionLogger (connection, logger) { + return new Logger(logger._level, (level, message) => + logger._loggerFunction(level, `${connection} ${message}`) + ) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js new file mode 100644 index 000000000..6d195d1d9 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js @@ -0,0 +1,101 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Connection from './connection.js' + +export default class DelegateConnection extends Connection { + /** + * @param delegate {Connection} the delegated connection + * @param errorHandler {ConnectionErrorHandler} the error handler + */ + constructor (delegate, errorHandler) { + super(errorHandler) + + if (errorHandler) { + this._originalErrorHandler = delegate._errorHandler + delegate._errorHandler = this._errorHandler + } + + this._delegate = delegate + } + + get id () { + return this._delegate.id + } + + get databaseId () { + return this._delegate.databaseId + } + + set databaseId (value) { + this._delegate.databaseId = value + } + + get server () { + return this._delegate.server + } + + get address () { + return this._delegate.address + } + + get version () { + return this._delegate.version + } + + set version (value) { + this._delegate.version = value + } + + isOpen () { + return this._delegate.isOpen() + } + + protocol () { + return this._delegate.protocol() + } + + connect (userAgent, authToken) { + return this._delegate.connect(userAgent, authToken) + } + + write (message, observer, flush) { + return this._delegate.write(message, observer, flush) + } + + resetAndFlush () { + return this._delegate.resetAndFlush() + } + + hasOngoingObservableRequests () { + return this._delegate.hasOngoingObservableRequests() + } + + close () { + return this._delegate.close() + } + + _release () { + if (this._originalErrorHandler) { + this._delegate._errorHandler = this._originalErrorHandler + } + + return this._delegate._release() + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js new file mode 100644 index 000000000..8544c4c10 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js @@ -0,0 +1,109 @@ +/** + * 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 { error } from '../../core/index.ts' + +const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error + +export default class ConnectionErrorHandler { + constructor ( + errorCode, + handleUnavailability, + handleWriteFailure, + handleAuthorizationExpired + ) { + this._errorCode = errorCode + this._handleUnavailability = handleUnavailability || noOpHandler + this._handleWriteFailure = handleWriteFailure || noOpHandler + this._handleAuthorizationExpired = handleAuthorizationExpired || noOpHandler + } + + static create ({ + errorCode, + handleUnavailability, + handleWriteFailure, + handleAuthorizationExpired + }) { + return new ConnectionErrorHandler( + errorCode, + handleUnavailability, + handleWriteFailure, + handleAuthorizationExpired + ) + } + + /** + * Error code to use for network errors. + * @return {string} the error code. + */ + errorCode () { + return this._errorCode + } + + /** + * Handle and transform the error. + * @param {Neo4jError} error the original error. + * @param {ServerAddress} address the address of the connection where the error happened. + * @return {Neo4jError} new error that should be propagated to the user. + */ + handleAndTransformError (error, address) { + if (isAutorizationExpiredError(error)) { + return this._handleAuthorizationExpired(error, address) + } + if (isAvailabilityError(error)) { + return this._handleUnavailability(error, address) + } + if (isFailureToWrite(error)) { + return this._handleWriteFailure(error, address) + } + return error + } +} + +function isAutorizationExpiredError (error) { + return error && ( + error.code === 'Neo.ClientError.Security.AuthorizationExpired' || + error.code === 'Neo.ClientError.Security.TokenExpired' + ) +} + +function isAvailabilityError (error) { + if (error) { + return ( + error.code === SESSION_EXPIRED || + error.code === SERVICE_UNAVAILABLE || + error.code === 'Neo.TransientError.General.DatabaseUnavailable' + ) + } + return false +} + +function isFailureToWrite (error) { + if (error) { + return ( + error.code === 'Neo.ClientError.Cluster.NotALeader' || + error.code === 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase' + ) + } + return false +} + +function noOpHandler (error) { + return error +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js new file mode 100644 index 000000000..dc996522c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js @@ -0,0 +1,132 @@ +/** + * 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. + */ +// eslint-disable-next-line no-unused-vars +import { ResultStreamObserver, BoltProtocol } from '../bolt/index.js' + +export default class Connection { + /** + * @param {ConnectionErrorHandler} errorHandler the error handler + */ + constructor (errorHandler) { + this._errorHandler = errorHandler + } + + get id () { + throw new Error('not implemented') + } + + get databaseId () { + throw new Error('not implemented') + } + + set databaseId (value) { + throw new Error('not implemented') + } + + /** + * @returns {boolean} whether this connection is in a working condition + */ + isOpen () { + throw new Error('not implemented') + } + + /** + * @returns {BoltProtocol} the underlying bolt protocol assigned to this connection + */ + protocol () { + throw new Error('not implemented') + } + + /** + * @returns {ServerAddress} the server address this connection is opened against + */ + get address () { + throw new Error('not implemented') + } + + /** + * @returns {ServerVersion} the version of the server this connection is connected to + */ + get version () { + throw new Error('not implemented') + } + + set version (value) { + throw new Error('not implemented') + } + + get server () { + throw new Error('not implemented') + } + + /** + * Connect to the target address, negotiate Bolt protocol and send initialization message. + * @param {string} userAgent the user agent for this driver. + * @param {Object} authToken the object containing auth information. + * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. + */ + connect (userAgent, authToken) { + throw new Error('not implemented') + } + + /** + * Write a message to the network channel. + * @param {RequestMessage} message the message to write. + * @param {ResultStreamObserver} observer the response observer. + * @param {boolean} flush `true` if flush should happen after the message is written to the buffer. + */ + write (message, observer, flush) { + throw new Error('not implemented') + } + + /** + * Send a RESET-message to the database. Message is immediately flushed to the network. + * @return {Promise} promise resolved when SUCCESS-message response arrives, or failed when other response messages arrives. + */ + resetAndFlush () { + throw new Error('not implemented') + } + + hasOngoingObservableRequests () { + throw new Error('not implemented') + } + + /** + * Call close on the channel. + * @returns {Promise} - A promise that will be resolved when the connection is closed. + * + */ + close () { + throw new Error('not implemented') + } + + /** + * + * @param error + * @param address + * @returns {Neo4jError|*} + */ + handleAndTransformError (error, address) { + if (this._errorHandler) { + return this._errorHandler.handleAndTransformError(error, address) + } + + return error + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/index.js new file mode 100644 index 000000000..3f82b83d5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Connection from './connection.js' +import ChannelConnection, { + createChannelConnection +} from './connection-channel.js' +import DelegateConnection from './connection-delegate.js' +import ConnectionErrorHandler from './connection-error-handler.js' + +export default Connection +export { + Connection, + ChannelConnection, + DelegateConnection, + ConnectionErrorHandler, + createChannelConnection +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/index.js new file mode 100644 index 000000000..76ecf194e --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/index.js @@ -0,0 +1,27 @@ +/** + * 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. + */ + +export * as loadBalancing from './load-balancing/index.js' +export * as bolt from './bolt/index.js' +export * as buf from './buf/index.js' +export * as channel from './channel/index.js' +export * as packstream from './packstream/index.js' +export * as pool from './pool/index.js' + +export * from './connection-provider/index.js' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/lang/functional.js b/packages/neo4j-driver-deno/lib/bolt-connection/lang/functional.js new file mode 100644 index 000000000..24aa87184 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/lang/functional.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Identity function. + * + * Identity functions are function which returns the input as output. + * + * @param {any} x + * @returns {any} the x + */ +export function identity (x) { + return x +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js new file mode 100644 index 000000000..2c7efd846 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js @@ -0,0 +1,20 @@ +/** + * 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. + */ + +export * as functional from './functional.js' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/index.js new file mode 100644 index 000000000..6b007e9b3 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/index.js @@ -0,0 +1,23 @@ +/** + * 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 LoadBalancingStrategy from './load-balancing-strategy.js' +import LeastConnectedLoadBalancingStrategy from './least-connected-load-balancing-strategy.js' + +export default LeastConnectedLoadBalancingStrategy +export { LoadBalancingStrategy, LeastConnectedLoadBalancingStrategy } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/least-connected-load-balancing-strategy.js b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/least-connected-load-balancing-strategy.js new file mode 100644 index 000000000..6b182066b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/least-connected-load-balancing-strategy.js @@ -0,0 +1,83 @@ +/** + * 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 RoundRobinArrayIndex from './round-robin-array-index.js' +import LoadBalancingStrategy from './load-balancing-strategy.js' + +export default class LeastConnectedLoadBalancingStrategy extends LoadBalancingStrategy { + /** + * @constructor + * @param {Pool} connectionPool the connection pool of this driver. + */ + constructor (connectionPool) { + super() + this._readersIndex = new RoundRobinArrayIndex() + this._writersIndex = new RoundRobinArrayIndex() + this._connectionPool = connectionPool + } + + /** + * @inheritDoc + */ + selectReader (knownReaders) { + return this._select(knownReaders, this._readersIndex) + } + + /** + * @inheritDoc + */ + selectWriter (knownWriters) { + return this._select(knownWriters, this._writersIndex) + } + + _select (addresses, roundRobinIndex) { + const length = addresses.length + if (length === 0) { + return null + } + + // choose start index for iteration in round-robin fashion + const startIndex = roundRobinIndex.next(length) + let index = startIndex + + let leastConnectedAddress = null + let leastActiveConnections = Number.MAX_SAFE_INTEGER + + // iterate over the array to find least connected address + do { + const address = addresses[index] + const activeConnections = this._connectionPool.activeResourceCount( + address + ) + + if (activeConnections < leastActiveConnections) { + leastConnectedAddress = address + leastActiveConnections = activeConnections + } + + // loop over to the start of the array when end is reached + if (index === length - 1) { + index = 0 + } else { + index++ + } + } while (index !== startIndex) + + return leastConnectedAddress + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/load-balancing-strategy.js b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/load-balancing-strategy.js new file mode 100644 index 000000000..6ad539c69 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/load-balancing-strategy.js @@ -0,0 +1,41 @@ +/** + * 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. + */ + +/** + * A facility to select most appropriate reader or writer among the given addresses for request processing. + */ +export default class LoadBalancingStrategy { + /** + * Select next most appropriate reader from the list of given readers. + * @param {string[]} knownReaders an array of currently known readers to select from. + * @return {string} most appropriate reader or `null` if given array is empty. + */ + selectReader (knownReaders) { + throw new Error('Abstract function') + } + + /** + * Select next most appropriate writer from the list of given writers. + * @param {string[]} knownWriters an array of currently known writers to select from. + * @return {string} most appropriate writer or `null` if given array is empty. + */ + selectWriter (knownWriters) { + throw new Error('Abstract function') + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/round-robin-array-index.js b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/round-robin-array-index.js new file mode 100644 index 000000000..cb36bac6f --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/load-balancing/round-robin-array-index.js @@ -0,0 +1,47 @@ +/** + * 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. + */ + +export default class RoundRobinArrayIndex { + /** + * @constructor + * @param {number} [initialOffset=0] the initial offset for round robin. + */ + constructor (initialOffset) { + this._offset = initialOffset || 0 + } + + /** + * Get next index for an array with given length. + * @param {number} arrayLength the array length. + * @return {number} index in the array. + */ + next (arrayLength) { + if (arrayLength === 0) { + return -1 + } + + const nextOffset = this._offset + this._offset += 1 + if (this._offset === Number.MAX_SAFE_INTEGER) { + this._offset = 0 + } + + return nextOffset % arrayLength + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/index.js new file mode 100644 index 000000000..1c66becf5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/index.js @@ -0,0 +1,26 @@ +/** + * 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 v1 from './packstream-v1.js' +import * as v2 from './packstream-v2.js' +import * as structure from './structure.js' + +export { v1, v2, structure } + +export default v2 diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js new file mode 100644 index 000000000..e648ccc85 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v1.js @@ -0,0 +1,547 @@ +/** + * 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 { utf8 } from '../channel/index.js' +import { functional } from '../lang/index.js' +import { Structure } from './structure.js' +import { + newError, + error, + int, + isInt, + Integer +} from '../../core/index.ts' + +const { PROTOCOL_ERROR } = error + +const TINY_STRING = 0x80 +const TINY_LIST = 0x90 +const TINY_MAP = 0xa0 +const TINY_STRUCT = 0xb0 +const NULL = 0xc0 +const FLOAT_64 = 0xc1 +const FALSE = 0xc2 +const TRUE = 0xc3 +const INT_8 = 0xc8 +const INT_16 = 0xc9 +const INT_32 = 0xca +const INT_64 = 0xcb +const STRING_8 = 0xd0 +const STRING_16 = 0xd1 +const STRING_32 = 0xd2 +const LIST_8 = 0xd4 +const LIST_16 = 0xd5 +const LIST_32 = 0xd6 +const BYTES_8 = 0xcc +const BYTES_16 = 0xcd +const BYTES_32 = 0xce +const MAP_8 = 0xd8 +const MAP_16 = 0xd9 +const MAP_32 = 0xda +const STRUCT_8 = 0xdc +const STRUCT_16 = 0xdd + +/** + * Class to pack + * @access private + */ +class Packer { + /** + * @constructor + * @param {Chunker} channel the chunker backed by a network channel. + */ + constructor (channel) { + this._ch = channel + this._byteArraysSupported = true + } + + /** + * Creates a packable function out of the provided value + * @param x the value to pack + * @returns Function + */ + packable (x, dehydrateStruct = functional.identity) { + try { + x = dehydrateStruct(x) + } catch (ex) { + return () => { throw ex } + } + + if (x === null) { + return () => this._ch.writeUInt8(NULL) + } else if (x === true) { + return () => this._ch.writeUInt8(TRUE) + } else if (x === false) { + return () => this._ch.writeUInt8(FALSE) + } else if (typeof x === 'number') { + return () => this.packFloat(x) + } else if (typeof x === 'string') { + return () => this.packString(x) + } else if (typeof x === 'bigint') { + return () => this.packInteger(int(x)) + } else if (isInt(x)) { + return () => this.packInteger(x) + } else if (x instanceof Int8Array) { + return () => this.packBytes(x) + } else if (x instanceof Array) { + return () => { + this.packListHeader(x.length) + for (let i = 0; i < x.length; i++) { + this.packable(x[i] === undefined ? null : x[i], dehydrateStruct)() + } + } + } else if (isIterable(x)) { + return this.packableIterable(x, dehydrateStruct) + } else if (x instanceof Structure) { + const packableFields = [] + for (let i = 0; i < x.fields.length; i++) { + packableFields[i] = this.packable(x.fields[i], dehydrateStruct) + } + return () => this.packStruct(x.signature, packableFields) + } else if (typeof x === 'object') { + return () => { + const keys = Object.keys(x) + + let count = 0 + for (let i = 0; i < keys.length; i++) { + if (x[keys[i]] !== undefined) { + count++ + } + } + this.packMapHeader(count) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + if (x[key] !== undefined) { + this.packString(key) + this.packable(x[key], dehydrateStruct)() + } + } + } + } else { + return this._nonPackableValue(`Unable to pack the given value: ${x}`) + } + } + + packableIterable (iterable, dehydrateStruct) { + try { + const array = Array.from(iterable) + return this.packable(array, dehydrateStruct) + } catch (e) { + // handle errors from iterable to array conversion + throw newError(`Cannot pack given iterable, ${e.message}: ${iterable}`) + } + } + + /** + * Packs a struct + * @param signature the signature of the struct + * @param packableFields the fields of the struct, make sure you call `packable on all fields` + */ + packStruct (signature, packableFields) { + packableFields = packableFields || [] + this.packStructHeader(packableFields.length, signature) + for (let i = 0; i < packableFields.length; i++) { + packableFields[i]() + } + } + + packInteger (x) { + const high = x.high + const low = x.low + + if (x.greaterThanOrEqual(-0x10) && x.lessThan(0x80)) { + this._ch.writeInt8(low) + } else if (x.greaterThanOrEqual(-0x80) && x.lessThan(-0x10)) { + this._ch.writeUInt8(INT_8) + this._ch.writeInt8(low) + } else if (x.greaterThanOrEqual(-0x8000) && x.lessThan(0x8000)) { + this._ch.writeUInt8(INT_16) + this._ch.writeInt16(low) + } else if (x.greaterThanOrEqual(-0x80000000) && x.lessThan(0x80000000)) { + this._ch.writeUInt8(INT_32) + this._ch.writeInt32(low) + } else { + this._ch.writeUInt8(INT_64) + this._ch.writeInt32(high) + this._ch.writeInt32(low) + } + } + + packFloat (x) { + this._ch.writeUInt8(FLOAT_64) + this._ch.writeFloat64(x) + } + + packString (x) { + const bytes = utf8.encode(x) + const size = bytes.length + if (size < 0x10) { + this._ch.writeUInt8(TINY_STRING | size) + this._ch.writeBytes(bytes) + } else if (size < 0x100) { + this._ch.writeUInt8(STRING_8) + this._ch.writeUInt8(size) + this._ch.writeBytes(bytes) + } else if (size < 0x10000) { + this._ch.writeUInt8(STRING_16) + this._ch.writeUInt8((size / 256) >> 0) + this._ch.writeUInt8(size % 256) + this._ch.writeBytes(bytes) + } else if (size < 0x100000000) { + this._ch.writeUInt8(STRING_32) + this._ch.writeUInt8(((size / 16777216) >> 0) % 256) + this._ch.writeUInt8(((size / 65536) >> 0) % 256) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + this._ch.writeBytes(bytes) + } else { + throw newError('UTF-8 strings of size ' + size + ' are not supported') + } + } + + packListHeader (size) { + if (size < 0x10) { + this._ch.writeUInt8(TINY_LIST | size) + } else if (size < 0x100) { + this._ch.writeUInt8(LIST_8) + this._ch.writeUInt8(size) + } else if (size < 0x10000) { + this._ch.writeUInt8(LIST_16) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + } else if (size < 0x100000000) { + this._ch.writeUInt8(LIST_32) + this._ch.writeUInt8(((size / 16777216) >> 0) % 256) + this._ch.writeUInt8(((size / 65536) >> 0) % 256) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + } else { + throw newError('Lists of size ' + size + ' are not supported') + } + } + + packBytes (array) { + if (this._byteArraysSupported) { + this.packBytesHeader(array.length) + for (let i = 0; i < array.length; i++) { + this._ch.writeInt8(array[i]) + } + } else { + throw newError( + 'Byte arrays are not supported by the database this driver is connected to' + ) + } + } + + packBytesHeader (size) { + if (size < 0x100) { + this._ch.writeUInt8(BYTES_8) + this._ch.writeUInt8(size) + } else if (size < 0x10000) { + this._ch.writeUInt8(BYTES_16) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + } else if (size < 0x100000000) { + this._ch.writeUInt8(BYTES_32) + this._ch.writeUInt8(((size / 16777216) >> 0) % 256) + this._ch.writeUInt8(((size / 65536) >> 0) % 256) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + } else { + throw newError('Byte arrays of size ' + size + ' are not supported') + } + } + + packMapHeader (size) { + if (size < 0x10) { + this._ch.writeUInt8(TINY_MAP | size) + } else if (size < 0x100) { + this._ch.writeUInt8(MAP_8) + this._ch.writeUInt8(size) + } else if (size < 0x10000) { + this._ch.writeUInt8(MAP_16) + this._ch.writeUInt8((size / 256) >> 0) + this._ch.writeUInt8(size % 256) + } else if (size < 0x100000000) { + this._ch.writeUInt8(MAP_32) + this._ch.writeUInt8(((size / 16777216) >> 0) % 256) + this._ch.writeUInt8(((size / 65536) >> 0) % 256) + this._ch.writeUInt8(((size / 256) >> 0) % 256) + this._ch.writeUInt8(size % 256) + } else { + throw newError('Maps of size ' + size + ' are not supported') + } + } + + packStructHeader (size, signature) { + if (size < 0x10) { + this._ch.writeUInt8(TINY_STRUCT | size) + this._ch.writeUInt8(signature) + } else if (size < 0x100) { + this._ch.writeUInt8(STRUCT_8) + this._ch.writeUInt8(size) + this._ch.writeUInt8(signature) + } else if (size < 0x10000) { + this._ch.writeUInt8(STRUCT_16) + this._ch.writeUInt8((size / 256) >> 0) + this._ch.writeUInt8(size % 256) + } else { + throw newError('Structures of size ' + size + ' are not supported') + } + } + + disableByteArrays () { + this._byteArraysSupported = false + } + + _nonPackableValue (message) { + return () => { + throw newError(message, PROTOCOL_ERROR) + } + } +} + +/** + * Class to unpack + * @access private + */ +class Unpacker { + /** + * @constructor + * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. + * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint + */ + constructor (disableLosslessIntegers = false, useBigInt = false) { + this._disableLosslessIntegers = disableLosslessIntegers + this._useBigInt = useBigInt + } + + unpack (buffer, hydrateStructure = functional.identity) { + const marker = buffer.readUInt8() + const markerHigh = marker & 0xf0 + const markerLow = marker & 0x0f + + if (marker === NULL) { + return null + } + + const boolean = this._unpackBoolean(marker) + if (boolean !== null) { + return boolean + } + + const numberOrInteger = this._unpackNumberOrInteger(marker, buffer) + if (numberOrInteger !== null) { + if (isInt(numberOrInteger)) { + if (this._useBigInt) { + return numberOrInteger.toBigInt() + } else if (this._disableLosslessIntegers) { + return numberOrInteger.toNumberOrInfinity() + } + } + return numberOrInteger + } + + const string = this._unpackString(marker, markerHigh, markerLow, buffer) + if (string !== null) { + return string + } + + const list = this._unpackList(marker, markerHigh, markerLow, buffer, hydrateStructure) + if (list !== null) { + return list + } + + const byteArray = this._unpackByteArray(marker, buffer) + if (byteArray !== null) { + return byteArray + } + + const map = this._unpackMap(marker, markerHigh, markerLow, buffer, hydrateStructure) + if (map !== null) { + return map + } + + const struct = this._unpackStruct(marker, markerHigh, markerLow, buffer, hydrateStructure) + if (struct !== null) { + return struct + } + + throw newError('Unknown packed value with marker ' + marker.toString(16)) + } + + unpackInteger (buffer) { + const marker = buffer.readUInt8() + const result = this._unpackInteger(marker, buffer) + if (result == null) { + throw newError( + 'Unable to unpack integer value with marker ' + marker.toString(16) + ) + } + return result + } + + _unpackBoolean (marker) { + if (marker === TRUE) { + return true + } else if (marker === FALSE) { + return false + } else { + return null + } + } + + _unpackNumberOrInteger (marker, buffer) { + if (marker === FLOAT_64) { + return buffer.readFloat64() + } else { + return this._unpackInteger(marker, buffer) + } + } + + _unpackInteger (marker, buffer) { + if (marker >= 0 && marker < 128) { + return int(marker) + } else if (marker >= 240 && marker < 256) { + return int(marker - 256) + } else if (marker === INT_8) { + return int(buffer.readInt8()) + } else if (marker === INT_16) { + return int(buffer.readInt16()) + } else if (marker === INT_32) { + const b = buffer.readInt32() + return int(b) + } else if (marker === INT_64) { + const high = buffer.readInt32() + const low = buffer.readInt32() + return new Integer(low, high) + } else { + return null + } + } + + _unpackString (marker, markerHigh, markerLow, buffer) { + if (markerHigh === TINY_STRING) { + return utf8.decode(buffer, markerLow) + } else if (marker === STRING_8) { + return utf8.decode(buffer, buffer.readUInt8()) + } else if (marker === STRING_16) { + return utf8.decode(buffer, buffer.readUInt16()) + } else if (marker === STRING_32) { + return utf8.decode(buffer, buffer.readUInt32()) + } else { + return null + } + } + + _unpackList (marker, markerHigh, markerLow, buffer, hydrateStructure) { + if (markerHigh === TINY_LIST) { + return this._unpackListWithSize(markerLow, buffer, hydrateStructure) + } else if (marker === LIST_8) { + return this._unpackListWithSize(buffer.readUInt8(), buffer, hydrateStructure) + } else if (marker === LIST_16) { + return this._unpackListWithSize(buffer.readUInt16(), buffer, hydrateStructure) + } else if (marker === LIST_32) { + return this._unpackListWithSize(buffer.readUInt32(), buffer, hydrateStructure) + } else { + return null + } + } + + _unpackListWithSize (size, buffer, hydrateStructure) { + const value = [] + for (let i = 0; i < size; i++) { + value.push(this.unpack(buffer, hydrateStructure)) + } + return value + } + + _unpackByteArray (marker, buffer) { + if (marker === BYTES_8) { + return this._unpackByteArrayWithSize(buffer.readUInt8(), buffer) + } else if (marker === BYTES_16) { + return this._unpackByteArrayWithSize(buffer.readUInt16(), buffer) + } else if (marker === BYTES_32) { + return this._unpackByteArrayWithSize(buffer.readUInt32(), buffer) + } else { + return null + } + } + + _unpackByteArrayWithSize (size, buffer) { + const value = new Int8Array(size) + for (let i = 0; i < size; i++) { + value[i] = buffer.readInt8() + } + return value + } + + _unpackMap (marker, markerHigh, markerLow, buffer, hydrateStructure) { + if (markerHigh === TINY_MAP) { + return this._unpackMapWithSize(markerLow, buffer, hydrateStructure) + } else if (marker === MAP_8) { + return this._unpackMapWithSize(buffer.readUInt8(), buffer, hydrateStructure) + } else if (marker === MAP_16) { + return this._unpackMapWithSize(buffer.readUInt16(), buffer, hydrateStructure) + } else if (marker === MAP_32) { + return this._unpackMapWithSize(buffer.readUInt32(), buffer, hydrateStructure) + } else { + return null + } + } + + _unpackMapWithSize (size, buffer, hydrateStructure) { + const value = {} + for (let i = 0; i < size; i++) { + const key = this.unpack(buffer, hydrateStructure) + value[key] = this.unpack(buffer, hydrateStructure) + } + return value + } + + _unpackStruct (marker, markerHigh, markerLow, buffer, hydrateStructure) { + if (markerHigh === TINY_STRUCT) { + return this._unpackStructWithSize(markerLow, buffer, hydrateStructure) + } else if (marker === STRUCT_8) { + return this._unpackStructWithSize(buffer.readUInt8(), buffer, hydrateStructure) + } else if (marker === STRUCT_16) { + return this._unpackStructWithSize(buffer.readUInt16(), buffer, hydrateStructure) + } else { + return null + } + } + + _unpackStructWithSize (structSize, buffer, hydrateStructure) { + const signature = buffer.readUInt8() + const structure = new Structure(signature, []) + for (let i = 0; i < structSize; i++) { + structure.fields.push(this.unpack(buffer, hydrateStructure)) + } + + return hydrateStructure(structure) + } +} + +function isIterable (obj) { + if (obj == null) { + return false + } + return typeof obj[Symbol.iterator] === 'function' +} + +export { Packer, Unpacker } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js new file mode 100644 index 000000000..3d83affeb --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/packstream-v2.js @@ -0,0 +1,37 @@ +/** + * 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 v1 from './packstream-v1.js' + +export class Packer extends v1.Packer { + disableByteArrays () { + throw new Error('Bolt V2 should always support byte arrays') + } +} + +export class Unpacker extends v1.Unpacker { + /** + * @constructor + * @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers. + * @param {boolean} useBigInt if this unpacker should convert all received integers to Bigint + */ + constructor (disableLosslessIntegers = false, useBigInt = false) { + super(disableLosslessIntegers, useBigInt) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/packstream/structure.js b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/structure.js new file mode 100644 index 000000000..23e67a7be --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/packstream/structure.js @@ -0,0 +1,63 @@ +/** + * 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 { newError, error } from '../../core/index.ts' + +const { + PROTOCOL_ERROR +} = error + +/** + * A Structure have a signature and fields. + */ +export class Structure { + /** + * Create new instance + */ + constructor (signature, fields) { + this.signature = signature + this.fields = fields + } + + get size () { + return this.fields.length + } + + toString () { + let fieldStr = '' + for (let i = 0; i < this.fields.length; i++) { + if (i > 0) { + fieldStr += ', ' + } + fieldStr += this.fields[i] + } + return 'Structure(' + this.signature + ', [' + fieldStr + '])' + } +} + +export function verifyStructSize (structName, expectedSize, actualSize) { + if (expectedSize !== actualSize) { + throw newError( + `Wrong struct size for ${structName}, expected ${expectedSize} but was ${actualSize}`, + PROTOCOL_ERROR + ) + } +} + +export default Structure diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/index.js new file mode 100644 index 000000000..df21d8ab0 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/index.js @@ -0,0 +1,27 @@ +/** + * 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 PoolConfig, { + DEFAULT_ACQUISITION_TIMEOUT, + DEFAULT_MAX_SIZE +} from './pool-config.js' +import Pool from './pool.js' + +export default Pool +export { Pool, PoolConfig, DEFAULT_ACQUISITION_TIMEOUT, DEFAULT_MAX_SIZE } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool-config.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool-config.js new file mode 100644 index 000000000..94596ac22 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool-config.js @@ -0,0 +1,60 @@ +/** + * 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. + */ + +const DEFAULT_MAX_SIZE = 100 +const DEFAULT_ACQUISITION_TIMEOUT = 60 * 1000 // 60 seconds + +export default class PoolConfig { + constructor (maxSize, acquisitionTimeout) { + this.maxSize = valueOrDefault(maxSize, DEFAULT_MAX_SIZE) + this.acquisitionTimeout = valueOrDefault( + acquisitionTimeout, + DEFAULT_ACQUISITION_TIMEOUT + ) + } + + static defaultConfig () { + return new PoolConfig(DEFAULT_MAX_SIZE, DEFAULT_ACQUISITION_TIMEOUT) + } + + static fromDriverConfig (config) { + const maxSizeConfigured = isConfigured(config.maxConnectionPoolSize) + const maxSize = maxSizeConfigured + ? config.maxConnectionPoolSize + : DEFAULT_MAX_SIZE + const acquisitionTimeoutConfigured = isConfigured( + config.connectionAcquisitionTimeout + ) + const acquisitionTimeout = acquisitionTimeoutConfigured + ? config.connectionAcquisitionTimeout + : DEFAULT_ACQUISITION_TIMEOUT + + return new PoolConfig(maxSize, acquisitionTimeout) + } +} + +function valueOrDefault (value, defaultValue) { + return value === 0 || value ? value : defaultValue +} + +function isConfigured (value) { + return value === 0 || value +} + +export { DEFAULT_MAX_SIZE, DEFAULT_ACQUISITION_TIMEOUT } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js new file mode 100644 index 000000000..148c5f1b9 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -0,0 +1,450 @@ +/** + * 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 PoolConfig from './pool-config.js' +import { newError, internal } from '../../core/index.ts' + +const { + logger: { Logger } +} = internal + +class Pool { + /** + * @param {function(address: ServerAddress, function(address: ServerAddress, resource: object): Promise): Promise} create + * an allocation function that creates a promise with a new resource. It's given an address for which to + * allocate the connection and a function that will return the resource to the pool if invoked, which is + * meant to be called on .dispose or .close or whatever mechanism the resource uses to finalize. + * @param {function(resource: object): Promise} destroy + * called with the resource when it is evicted from this pool + * @param {function(resource: object): boolean} validate + * called at various times (like when an instance is acquired and when it is returned. + * If this returns false, the resource will be evicted + * @param {function(resource: object, observer: { onError }): void} installIdleObserver + * called when the resource is released back to pool + * @param {function(resource: object): void} removeIdleObserver + * called when the resource is acquired from the pool + * @param {PoolConfig} config configuration for the new driver. + * @param {Logger} log the driver logger. + */ + constructor ({ + create = (address, release) => Promise.resolve(), + destroy = conn => Promise.resolve(), + validate = conn => true, + installIdleObserver = (conn, observer) => {}, + removeIdleObserver = conn => {}, + config = PoolConfig.defaultConfig(), + log = Logger.noOp() + } = {}) { + this._create = create + this._destroy = destroy + this._validate = validate + this._installIdleObserver = installIdleObserver + this._removeIdleObserver = removeIdleObserver + this._maxSize = config.maxSize + this._acquisitionTimeout = config.acquisitionTimeout + this._pools = {} + this._pendingCreates = {} + this._acquireRequests = {} + this._activeResourceCounts = {} + this._release = this._release.bind(this) + this._log = log + this._closed = false + } + + /** + * Acquire and idle resource fom the pool or create a new one. + * @param {ServerAddress} address the address for which we're acquiring. + * @return {Promise} resource that is ready to use. + */ + acquire (address) { + const key = address.asKey() + + // We're out of resources and will try to acquire later on when an existing resource is released. + const allRequests = this._acquireRequests + const requests = allRequests[key] + if (!requests) { + allRequests[key] = [] + } + return new Promise((resolve, reject) => { + let request = null + + const timeoutId = setTimeout(() => { + // acquisition timeout fired + + // remove request from the queue of pending requests, if it's still there + // request might've been taken out by the release operation + const pendingRequests = allRequests[key] + if (pendingRequests) { + allRequests[key] = pendingRequests.filter(item => item !== request) + } + + if (request.isCompleted()) { + // request already resolved/rejected by the release operation; nothing to do + } else { + // request is still pending and needs to be failed + const activeCount = this.activeResourceCount(address) + const idleCount = this.has(address) ? this._pools[key].length : 0 + request.reject( + newError( + `Connection acquisition timed out in ${this._acquisitionTimeout} ms. Pool status: Active conn count = ${activeCount}, Idle conn count = ${idleCount}.` + ) + ) + } + }, this._acquisitionTimeout) + + request = new PendingRequest(key, resolve, reject, timeoutId, this._log) + allRequests[key].push(request) + this._processPendingAcquireRequests(address) + }) + } + + /** + * Destroy all idle resources for the given address. + * @param {ServerAddress} address the address of the server to purge its pool. + * @returns {Promise} A promise that is resolved when the resources are purged + */ + purge (address) { + return this._purgeKey(address.asKey()) + } + + /** + * Destroy all idle resources in this pool. + * @returns {Promise} A promise that is resolved when the resources are purged + */ + async close () { + this._closed = true + /** + * The lack of Promise consuming was making the driver do not close properly in the scenario + * captured at result.test.js:it('should handle missing onCompleted'). The test was timing out + * because while wainting for the driver close. + * + * Consuming the Promise.all or by calling then or by awaiting in the result inside this method solved + * the issue somehow. + * + * PS: the return of this method was already awaited at PooledConnectionProvider.close, but the await bellow + * seems to be need also. + */ + return await Promise.all( + Object.keys(this._pools).map(key => this._purgeKey(key)) + ) + } + + /** + * Keep the idle resources for the provided addresses and purge the rest. + * @returns {Promise} A promise that is resolved when the other resources are purged + */ + keepAll (addresses) { + const keysToKeep = addresses.map(a => a.asKey()) + const keysPresent = Object.keys(this._pools) + const keysToPurge = keysPresent.filter(k => keysToKeep.indexOf(k) === -1) + + return Promise.all(keysToPurge.map(key => this._purgeKey(key))) + } + + /** + * Check if this pool contains resources for the given address. + * @param {ServerAddress} address the address of the server to check. + * @return {boolean} `true` when pool contains entries for the given key, false otherwise. + */ + has (address) { + return address.asKey() in this._pools + } + + /** + * Get count of active (checked out of the pool) resources for the given key. + * @param {ServerAddress} address the address of the server to check. + * @return {number} count of resources acquired by clients. + */ + activeResourceCount (address) { + return this._activeResourceCounts[address.asKey()] || 0 + } + + _getOrInitializePoolFor (key) { + let pool = this._pools[key] + if (!pool) { + pool = new SingleAddressPool() + this._pools[key] = pool + this._pendingCreates[key] = 0 + } + return pool + } + + async _acquire (address) { + if (this._closed) { + throw newError('Pool is closed, it is no more able to serve requests.') + } + + const key = address.asKey() + const pool = this._getOrInitializePoolFor(key) + while (pool.length) { + const resource = pool.pop() + + if (this._validate(resource)) { + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + + // idle resource is valid and can be acquired + resourceAcquired(key, this._activeResourceCounts) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} acquired from the pool ${key}`) + } + return { resource, pool } + } else { + await this._destroy(resource) + } + } + + // Ensure requested max pool size + if (this._maxSize > 0) { + // Include pending creates when checking pool size since these probably will add + // to the number when fulfilled. + const numConnections = + this.activeResourceCount(address) + this._pendingCreates[key] + if (numConnections >= this._maxSize) { + // Will put this request in queue instead since the pool is full + return { resource: null, pool } + } + } + + // there exist no idle valid resources, create a new one for acquisition + // Keep track of how many pending creates there are to avoid making too many connections. + this._pendingCreates[key] = this._pendingCreates[key] + 1 + let resource + try { + // Invoke callback that creates actual connection + resource = await this._create(address, (address, resource) => this._release(address, resource, pool)) + + resourceAcquired(key, this._activeResourceCounts) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} created for the pool ${key}`) + } + } finally { + this._pendingCreates[key] = this._pendingCreates[key] - 1 + } + return { resource, pool } + } + + async _release (address, resource, pool) { + const key = address.asKey() + + if (pool.isActive()) { + // there exist idle connections for the given key + if (!this._validate(resource)) { + if (this._log.isDebugEnabled()) { + this._log.debug( + `${resource} destroyed and can't be released to the pool ${key} because it is not functional` + ) + } + await this._destroy(resource) + } else { + if (this._installIdleObserver) { + this._installIdleObserver(resource, { + onError: error => { + this._log.debug( + `Idle connection ${resource} destroyed because of error: ${error}` + ) + const pool = this._pools[key] + if (pool) { + this._pools[key] = pool.filter(r => r !== resource) + } + // let's not care about background clean-ups due to errors but just trigger the destroy + // process for the resource, we especially catch any errors and ignore them to avoid + // unhandled promise rejection warnings + this._destroy(resource).catch(() => {}) + } + }) + } + pool.push(resource) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} released to the pool ${key}`) + } + } + } else { + // key has been purged, don't put it back, just destroy the resource + if (this._log.isDebugEnabled()) { + this._log.debug( + `${resource} destroyed and can't be released to the pool ${key} because pool has been purged` + ) + } + await this._destroy(resource) + } + resourceReleased(key, this._activeResourceCounts) + + this._processPendingAcquireRequests(address) + } + + async _purgeKey (key) { + const pool = this._pools[key] + const destructionList = [] + if (pool) { + while (pool.length) { + const resource = pool.pop() + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + destructionList.push(this._destroy(resource)) + } + pool.close() + delete this._pools[key] + await Promise.all(destructionList) + } + } + + _processPendingAcquireRequests (address) { + const key = address.asKey() + const requests = this._acquireRequests[key] + if (requests) { + const pendingRequest = requests.shift() // pop a pending acquire request + + if (pendingRequest) { + this._acquire(address) + .catch(error => { + // failed to acquire/create a new connection to resolve the pending acquire request + // propagate the error by failing the pending request + pendingRequest.reject(error) + return { resource: null } + }) + .then(({ resource, pool }) => { + if (resource) { + // managed to acquire a valid resource from the pool + + if (pendingRequest.isCompleted()) { + // request has been completed, most likely failed by a timeout + // return the acquired resource back to the pool + this._release(address, resource, pool) + } else { + // request is still pending and can be resolved with the newly acquired resource + pendingRequest.resolve(resource) // resolve the pending request with the acquired resource + } + } else { + // failed to acquire a valid resource from the pool + // return the pending request back to the pool + if (!pendingRequest.isCompleted()) { + if (!this._acquireRequests[key]) { + this._acquireRequests[key] = [] + } + this._acquireRequests[key].unshift(pendingRequest) + } + } + }) + } else { + delete this._acquireRequests[key] + } + } + } +} + +/** + * Increment active (checked out of the pool) resource counter. + * @param {string} key the resource group identifier (server address for connections). + * @param {Object.} activeResourceCounts the object holding active counts per key. + */ +function resourceAcquired (key, activeResourceCounts) { + const currentCount = activeResourceCounts[key] || 0 + activeResourceCounts[key] = currentCount + 1 +} + +/** + * Decrement active (checked out of the pool) resource counter. + * @param {string} key the resource group identifier (server address for connections). + * @param {Object.} activeResourceCounts the object holding active counts per key. + */ +function resourceReleased (key, activeResourceCounts) { + const currentCount = activeResourceCounts[key] || 0 + const nextCount = currentCount - 1 + + if (nextCount > 0) { + activeResourceCounts[key] = nextCount + } else { + delete activeResourceCounts[key] + } +} + +class PendingRequest { + constructor (key, resolve, reject, timeoutId, log) { + this._key = key + this._resolve = resolve + this._reject = reject + this._timeoutId = timeoutId + this._log = log + this._completed = false + } + + isCompleted () { + return this._completed + } + + resolve (resource) { + if (this._completed) { + return + } + this._completed = true + + clearTimeout(this._timeoutId) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} acquired from the pool ${this._key}`) + } + this._resolve(resource) + } + + reject (error) { + if (this._completed) { + return + } + this._completed = true + + clearTimeout(this._timeoutId) + this._reject(error) + } +} + +class SingleAddressPool { + constructor () { + this._active = true + this._elements = [] + } + + isActive () { + return this._active + } + + close () { + this._active = false + } + + filter (predicate) { + this._elements = this._elements.filter(predicate) + return this + } + + get length () { + return this._elements.length + } + + pop () { + return this._elements.pop() + } + + push (element) { + return this._elements.push(element) + } +} + +export default Pool diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/index.js new file mode 100644 index 000000000..43b9dd59e --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/index.js @@ -0,0 +1,24 @@ +/** + * 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 Rediscovery from './rediscovery.js' +import RoutingTable from './routing-table.js' + +export default Rediscovery +export { Rediscovery, RoutingTable } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/rediscovery.js b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/rediscovery.js new file mode 100644 index 000000000..664e3f2e8 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/rediscovery.js @@ -0,0 +1,78 @@ +/** + * 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 RoutingTable from './routing-table.js' +// eslint-disable-next-line no-unused-vars +import { Session } from '../../core/index.ts' + +export default class Rediscovery { + /** + * @constructor + * @param {object} routingContext + */ + constructor (routingContext) { + this._routingContext = routingContext + } + + /** + * Try to fetch new routing table from the given router. + * @param {Session} session the session to use. + * @param {string} database the database for which to lookup routing table. + * @param {ServerAddress} routerAddress the URL of the router. + * @param {string} impersonatedUser The impersonated user + * @return {Promise} promise resolved with new routing table or null when connection error happened. + */ + lookupRoutingTableOnRouter (session, database, routerAddress, impersonatedUser) { + return session._acquireConnection(connection => { + return this._requestRawRoutingTable( + connection, + session, + database, + routerAddress, + impersonatedUser + ).then(rawRoutingTable => { + if (rawRoutingTable.isNull) { + return null + } + return RoutingTable.fromRawRoutingTable( + database, + routerAddress, + rawRoutingTable + ) + }) + }) + } + + _requestRawRoutingTable (connection, session, database, routerAddress, impersonatedUser) { + return new Promise((resolve, reject) => { + connection.protocol().requestRoutingInformation({ + routingContext: this._routingContext, + databaseName: database, + impersonatedUser, + sessionContext: { + bookmarks: session._lastBookmarks, + mode: session._mode, + database: session._database, + afterComplete: session._onComplete + }, + onCompleted: resolve, + onError: reject + }) + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/routing-table.js b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/routing-table.js new file mode 100644 index 000000000..f5d50844c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/rediscovery/routing-table.js @@ -0,0 +1,266 @@ +/** + * 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 { + newError, + error, + Integer, + int, + internal, + json +} from '../../core/index.ts' + +const { + constants: { ACCESS_MODE_WRITE: WRITE, ACCESS_MODE_READ: READ }, + serverAddress: { ServerAddress } +} = internal +const { PROTOCOL_ERROR } = error + +const MIN_ROUTERS = 1 + +/** + * The routing table object used to determine the role of the servers in the driver. + */ +export default class RoutingTable { + constructor ({ + database, + routers, + readers, + writers, + expirationTime, + ttl + } = {}) { + this.database = database || null + this.databaseName = database || 'default database' + this.routers = routers || [] + this.readers = readers || [] + this.writers = writers || [] + this.expirationTime = expirationTime || int(0) + this.ttl = ttl + } + + /** + * Create a valid routing table from a raw object + * + * @param {string} database the database name. It is used for logging purposes + * @param {ServerAddress} routerAddress The router address, it is used for loggin purposes + * @param {RawRoutingTable} rawRoutingTable Method used to get the raw routing table to be processed + * @param {RoutingTable} The valid Routing Table + */ + static fromRawRoutingTable (database, routerAddress, rawRoutingTable) { + return createValidRoutingTable(database, routerAddress, rawRoutingTable) + } + + forget (address) { + // Don't remove it from the set of routers, since that might mean we lose our ability to re-discover, + // just remove it from the set of readers and writers, so that we don't use it for actual work without + // performing discovery first. + + this.readers = removeFromArray(this.readers, address) + this.writers = removeFromArray(this.writers, address) + } + + forgetRouter (address) { + this.routers = removeFromArray(this.routers, address) + } + + forgetWriter (address) { + this.writers = removeFromArray(this.writers, address) + } + + /** + * Check if this routing table is fresh to perform the required operation. + * @param {string} accessMode the type of operation. Allowed values are {@link READ} and {@link WRITE}. + * @return {boolean} `true` when this table contains servers to serve the required operation, `false` otherwise. + */ + isStaleFor (accessMode) { + return ( + this.expirationTime.lessThan(Date.now()) || + this.routers.length < MIN_ROUTERS || + (accessMode === READ && this.readers.length === 0) || + (accessMode === WRITE && this.writers.length === 0) + ) + } + + /** + * Check if this routing table is expired for specified amount of duration + * + * @param {Integer} duration amount of duration in milliseconds to check for expiration + * @returns {boolean} + */ + isExpiredFor (duration) { + return this.expirationTime.add(duration).lessThan(Date.now()) + } + + allServers () { + return [...this.routers, ...this.readers, ...this.writers] + } + + toString () { + return ( + 'RoutingTable[' + + `database=${this.databaseName}, ` + + `expirationTime=${this.expirationTime}, ` + + `currentTime=${Date.now()}, ` + + `routers=[${this.routers}], ` + + `readers=[${this.readers}], ` + + `writers=[${this.writers}]]` + ) + } +} + +/** + * Remove all occurrences of the element in the array. + * @param {Array} array the array to filter. + * @param {Object} element the element to remove. + * @return {Array} new filtered array. + */ +function removeFromArray (array, element) { + return array.filter(item => item.asKey() !== element.asKey()) +} + +/** + * Create a valid routing table from a raw object + * + * @param {string} db the database name. It is used for logging purposes + * @param {ServerAddress} routerAddress The router address, it is used for loggin purposes + * @param {RawRoutingTable} rawRoutingTable Method used to get the raw routing table to be processed + * @param {RoutingTable} The valid Routing Table + */ +export function createValidRoutingTable ( + database, + routerAddress, + rawRoutingTable +) { + const ttl = rawRoutingTable.ttl + const expirationTime = calculateExpirationTime(rawRoutingTable, routerAddress) + const { routers, readers, writers } = parseServers( + rawRoutingTable, + routerAddress + ) + + assertNonEmpty(routers, 'routers', routerAddress) + assertNonEmpty(readers, 'readers', routerAddress) + + return new RoutingTable({ + database: database || rawRoutingTable.db, + routers, + readers, + writers, + expirationTime, + ttl + }) +} + +/** + * Parse server from the RawRoutingTable. + * + * @param {RawRoutingTable} rawRoutingTable the raw routing table + * @param {string} routerAddress the router address + * @returns {Object} The object with the list of routers, readers and writers + */ +function parseServers (rawRoutingTable, routerAddress) { + try { + let routers = [] + let readers = [] + let writers = [] + + rawRoutingTable.servers.forEach(server => { + const role = server.role + const addresses = server.addresses + + if (role === 'ROUTE') { + routers = parseArray(addresses).map(address => + ServerAddress.fromUrl(address) + ) + } else if (role === 'WRITE') { + writers = parseArray(addresses).map(address => + ServerAddress.fromUrl(address) + ) + } else if (role === 'READ') { + readers = parseArray(addresses).map(address => + ServerAddress.fromUrl(address) + ) + } + }) + + return { + routers: routers, + readers: readers, + writers: writers + } + } catch (error) { + throw newError( + `Unable to parse servers entry from router ${routerAddress} from addresses:\n${json.stringify( + rawRoutingTable.servers + )}\nError message: ${error.message}`, + PROTOCOL_ERROR + ) + } +} + +/** + * Call the expiration time using the ttls from the raw routing table and return it + * + * @param {RawRoutingTable} rawRoutingTable the routing table + * @param {string} routerAddress the router address + * @returns {number} the ttl + */ +function calculateExpirationTime (rawRoutingTable, routerAddress) { + try { + const now = int(Date.now()) + const expires = int(rawRoutingTable.ttl) + .multiply(1000) + .add(now) + // if the server uses a really big expire time like Long.MAX_VALUE this may have overflowed + if (expires.lessThan(now)) { + return Integer.MAX_VALUE + } + return expires + } catch (error) { + throw newError( + `Unable to parse TTL entry from router ${routerAddress} from raw routing table:\n${json.stringify( + rawRoutingTable + )}\nError message: ${error.message}`, + PROTOCOL_ERROR + ) + } +} + +/** + * Assert if serverAddressesArray is not empty, throws and PROTOCOL_ERROR otherwise + * + * @param {string[]} serverAddressesArray array of addresses + * @param {string} serversName the server name + * @param {string} routerAddress the router address + */ +function assertNonEmpty (serverAddressesArray, serversName, routerAddress) { + if (serverAddressesArray.length === 0) { + throw newError( + 'Received no ' + serversName + ' from router ' + routerAddress, + PROTOCOL_ERROR + ) + } +} + +function parseArray (addresses) { + if (!Array.isArray(addresses)) { + throw new TypeError('Array expected but got: ' + addresses) + } + return Array.from(addresses) +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/types/index.d.ts b/packages/neo4j-driver-deno/lib/bolt-connection/types/index.d.ts new file mode 100644 index 000000000..a310791fb --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/types/index.d.ts @@ -0,0 +1,34 @@ +/** + * 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 { + ConnectionProvider +} from '../../core/index.ts' + +declare class DirectConnectionProvider extends ConnectionProvider { + constructor (config: any) +} + +declare class RoutingConnectionProvider extends ConnectionProvider { + constructor (config: any) +} + +export { + DirectConnectionProvider, + RoutingConnectionProvider +} diff --git a/packages/neo4j-driver-deno/lib/core/auth.ts b/packages/neo4j-driver-deno/lib/core/auth.ts new file mode 100644 index 000000000..502166a0e --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/auth.ts @@ -0,0 +1,89 @@ +/** + * 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. + */ + +/** + * @property {function(username: string, password: string, realm: ?string)} basic the function to create a + * basic authentication token. + * @property {function(base64EncodedTicket: string)} kerberos the function to create a Kerberos authentication token. + * Accepts a single string argument - base64 encoded Kerberos ticket. + * @property {function(base64EncodedTicket: string)} bearer the function to create a Bearer authentication token. + * Accepts a single string argument - base64 encoded Bearer ticket. + * @property {function(principal: string, credentials: string, realm: string, scheme: string, parameters: ?object)} custom + * the function to create a custom authentication token. + */ +const auth = { + basic: (username: string, password: string, realm?: string) => { + if (realm != null) { + return { + scheme: 'basic', + principal: username, + credentials: password, + realm: realm + } + } else { + return { scheme: 'basic', principal: username, credentials: password } + } + }, + kerberos: (base64EncodedTicket: string) => { + return { + scheme: 'kerberos', + principal: '', // This empty string is required for backwards compatibility. + credentials: base64EncodedTicket + } + }, + bearer: (base64EncodedToken: string) => { + return { + scheme: 'bearer', + credentials: base64EncodedToken + } + }, + custom: ( + principal: string, + credentials: string, + realm: string, + scheme: string, + parameters?: object + ) => { + const output: any = { + scheme: scheme, + principal: principal + } + if (isNotEmpty(credentials)) { + output.credentials = credentials + } + if (isNotEmpty(realm)) { + output.realm = realm + } + if (isNotEmpty(parameters)) { + output.parameters = parameters + } + return output + } +} + +function isNotEmpty (value: T | null | undefined): boolean { + return !( + value === null || + value === undefined || + value === '' || + (Object.getPrototypeOf(value) === Object.prototype && Object.keys(value).length === 0) + ) +} + +export default auth diff --git a/packages/neo4j-driver-deno/lib/core/bookmark-manager.ts b/packages/neo4j-driver-deno/lib/core/bookmark-manager.ts new file mode 100644 index 000000000..3d2a36168 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/bookmark-manager.ts @@ -0,0 +1,187 @@ +/** + * 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. + */ + +/** + * Interface for the piece of software responsible for keeping track of current active bookmarks accross the driver. + * @interface + * @since 5.0 + * @experimental + */ +export default class BookmarkManager { + /** + * @constructor + * @private + */ + private constructor () { + throw new Error('Not implemented') + } + + /** + * Method called when the bookmarks get updated when a transaction finished. + * + * This method will be called when auto-commit queries finish and when explicit transactions + * get commited. + * + * @param {string} database The database which the bookmarks belongs to + * @param {Iterable} previousBookmarks The bookmarks used when starting the transaction + * @param {Iterable} newBookmarks The new bookmarks received at the end of the transaction. + * @returns {void} + */ + async updateBookmarks (database: string, previousBookmarks: Iterable, newBookmarks: Iterable): Promise { + throw new Error('Not implemented') + } + + /** + * Method called by the driver to get the bookmarks for one specific database + * + * @param {string} database The database which the bookmarks belong to + * @returns {Iterable} The set of bookmarks + */ + async getBookmarks (database: string): Promise> { + throw new Error('Not implemented') + } + + /** + * Method called by the driver for getting all the bookmarks. + * + * This method should return all bookmarks for all databases present in the BookmarkManager. + * + * @returns {Iterable} The set of bookmarks + */ + async getAllBookmarks (): Promise> { + throw new Error('Not implemented') + } + + /** + * Forget the databases and its bookmarks + * + * This method is not called by the driver. Forgetting unused databases is the user's responsibility. + * + * @param {Iterable} databases The databases which the bookmarks will be removed for. + */ + async forget (databases: Iterable): Promise { + throw new Error('Not implemented') + } +} + +export interface BookmarkManagerConfig { + initialBookmarks?: Map> + bookmarksSupplier?: (database?: string) => Promise> + bookmarksConsumer?: (database: string, bookmarks: Iterable) => Promise +} + +/** + * @typedef {Object} BookmarkManagerConfig + * + * @since 5.0 + * @experimental + * @property {Map>} [initialBookmarks@experimental] Defines the initial set of bookmarks. The key is the database name and the values are the bookmarks. + * @property {function([database]: string):Promise>} [bookmarksSupplier] Called for supplying extra bookmarks to the BookmarkManager + * 1. supplying bookmarks from the given database when the default BookmarkManager's `.getBookmarks(database)` gets called. + * 2. supplying all the bookmarks when the default BookmarkManager's `.getAllBookmarks()` gets called + * @property {function(database: string, bookmarks: Iterable): Promise} [bookmarksConsumer] Called when the set of bookmarks for database get updated + */ +/** + * Provides an configured {@link BookmarkManager} instance. + * + * @since 5.0 + * @experimental + * @param {BookmarkManagerConfig} [config={}] + * @returns {BookmarkManager} + */ +export function bookmarkManager (config: BookmarkManagerConfig = {}): BookmarkManager { + const initialBookmarks = new Map>() + + config.initialBookmarks?.forEach((v, k) => initialBookmarks.set(k, new Set(v))) + + return new Neo4jBookmarkManager( + initialBookmarks, + config.bookmarksSupplier, + config.bookmarksConsumer + ) +} + +class Neo4jBookmarkManager implements BookmarkManager { + constructor ( + private readonly _bookmarksPerDb: Map>, + private readonly _bookmarksSupplier?: (database?: string) => Promise>, + private readonly _bookmarksConsumer?: (database: string, bookmark: Iterable) => Promise + ) { + + } + + async updateBookmarks (database: string, previousBookmarks: Iterable, newBookmarks: Iterable): Promise { + const bookmarks = this._getOrInitializeBookmarks(database) + for (const bm of previousBookmarks) { + bookmarks.delete(bm) + } + for (const bm of newBookmarks) { + bookmarks.add(bm) + } + if (typeof this._bookmarksConsumer === 'function') { + await this._bookmarksConsumer(database, [...bookmarks]) + } + } + + private _getOrInitializeBookmarks (database: string): Set { + let maybeBookmarks = this._bookmarksPerDb.get(database) + if (maybeBookmarks === undefined) { + maybeBookmarks = new Set() + this._bookmarksPerDb.set(database, maybeBookmarks) + } + return maybeBookmarks + } + + async getBookmarks (database: string): Promise> { + const bookmarks = new Set(this._bookmarksPerDb.get(database)) + + if (typeof this._bookmarksSupplier === 'function') { + const suppliedBookmarks = await this._bookmarksSupplier(database) ?? [] + for (const bm of suppliedBookmarks) { + bookmarks.add(bm) + } + } + + return [...bookmarks] + } + + async getAllBookmarks (): Promise> { + const bookmarks = new Set() + + for (const [, dbBookmarks] of this._bookmarksPerDb) { + for (const bm of dbBookmarks) { + bookmarks.add(bm) + } + } + if (typeof this._bookmarksSupplier === 'function') { + const suppliedBookmarks = await this._bookmarksSupplier() ?? [] + for (const bm of suppliedBookmarks) { + bookmarks.add(bm) + } + } + + return bookmarks + } + + async forget (databases: Iterable): Promise { + for (const database of databases) { + this._bookmarksPerDb.delete(database) + } + } +} diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts new file mode 100644 index 000000000..e8bd12133 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -0,0 +1,123 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ + +import Connection from './connection.ts' +import { bookmarks } from './internal/index.ts' +import { ServerInfo } from './result-summary.ts' + +/** + * Inteface define a common way to acquire a connection + * + * @private + */ +class ConnectionProvider { + /** + * This method acquires a connection against the specified database. + * + * Access mode and Bookmarks only applies to routing driver. Access mode only + * differentiates the target server for the connection, where WRITE selects a + * WRITER server, whereas READ selects a READ server. Bookmarks, when specified, + * is only passed to the routing discovery procedure, for the system database to + * synchronize on creation of databases and is never used in direct drivers. + * + * @param {object} param - object parameter + * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * @property {string} param.database - the target database for the to-be-acquired connection + * @property {Bookmarks} param.bookmarks - the bookmarks to send to routing discovery + * @property {string} param.impersonatedUser - the impersonated user + * @property {function (databaseName:string?)} param.onDatabaseNameResolved - Callback called when the database name get resolved + */ + acquireConnection (param?: { + accessMode?: string + database?: string + bookmarks: bookmarks.Bookmarks + impersonatedUser?: string + onDatabaseNameResolved?: (databaseName?: string) => void + }): Promise { + throw Error('Not implemented') + } + + /** + * This method checks whether the backend database supports multi database functionality + * by checking protocol handshake result. + * + * @returns {Promise} + */ + supportsMultiDb (): Promise { + throw Error('Not implemented') + } + + /** + * This method checks whether the backend database supports transaction config functionality + * by checking protocol handshake result. + * + * @returns {Promise} + */ + supportsTransactionConfig (): Promise { + throw Error('Not implemented') + } + + /** + * This method checks whether the backend database supports transaction config functionality + * by checking protocol handshake result. + * + * @returns {Promise} + */ + supportsUserImpersonation (): Promise { + throw Error('Not implemented') + } + + /** + * This method verifies the connectivity of the database by trying to acquire a connection + * for each server available in the cluster. + * + * @param {object} param - object parameter + * @property {string} param.database - the target database for the to-be-acquired connection + * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * + * @returns {Promise} promise resolved with server info or rejected with error. + */ + verifyConnectivityAndGetServerInfo (param?: { database?: string, accessMode?: string }): Promise { + throw Error('Not implemented') + } + + /** + * Returns the protocol version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} the protocol version negotiated via handshake. + * @throws {Error} When protocol negotiation fails + */ + getNegotiatedProtocolVersion (): Promise { + throw Error('Not Implemented') + } + + /** + * Closes this connection provider along with its internals (connections, pools, etc.) + * + * @returns {Promise} + */ + close (): Promise { + throw Error('Not implemented') + } +} + +export default ConnectionProvider diff --git a/packages/neo4j-driver-deno/lib/core/connection.ts b/packages/neo4j-driver-deno/lib/core/connection.ts new file mode 100644 index 000000000..30c896932 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/connection.ts @@ -0,0 +1,122 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ + +import { ServerAddress } from './internal/server-address.ts' + +/** + * Interface which defines the raw connection with the database + * @private + */ +class Connection { + get id (): string { + return '' + } + + get databaseId (): string { + return '' + } + + get server (): any { + return {} + } + + /** + * @property {ServerAddress} the server address this connection is opened against + */ + get address (): ServerAddress | undefined { + return undefined + } + + /** + * @property {ServerVersion} the version of the server this connection is connected to + */ + get version (): any { + return undefined + } + + /** + * @returns {boolean} whether this connection is in a working condition + */ + isOpen (): boolean { + return false + } + + /** + * @todo be removed and internalize the methods + * @returns {any} the underlying bolt protocol assigned to this connection + */ + protocol (): any { + throw Error('Not implemented') + } + + /** + * Connect to the target address, negotiate Bolt protocol and send initialization message. + * @param {string} userAgent the user agent for this driver. + * @param {Object} authToken the object containing auth information. + * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. + */ + connect (userAgent: string, authToken: any): Promise { + throw Error('Not implemented') + } + + /** + * Write a message to the network channel. + * @param {RequestMessage} message the message to write. + * @param {ResultStreamObserver} observer the response observer. + * @param {boolean} flush `true` if flush should happen after the message is written to the buffer. + */ + write (message: any, observer: any, flush: boolean): void { + throw Error('Not implemented') + } + + /** + * Send a RESET-message to the database. Message is immediately flushed to the network. + * @return {Promise} promise resolved when SUCCESS-message response arrives, or failed when other response messages arrives. + */ + resetAndFlush (): Promise { + throw Error('Not implemented') + } + + /** + * Checks if there is an ongoing request being handled + * @return {boolean} `true` if there is an ongoing request being handled + */ + hasOngoingObservableRequests (): boolean { + throw Error('Not implemented') + } + + /** + * Call close on the channel. + * @returns {Promise} - A promise that will be resolved when the connection is closed. + * + */ + close (): Promise { + throw Error('Not implemented') + } + + /** + * Called to release the connection + */ + _release (): Promise { + return Promise.resolve() + } +} + +export default Connection diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts new file mode 100644 index 000000000..cde7a6865 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -0,0 +1,594 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ +import ConnectionProvider from './connection-provider.ts' +import { Bookmarks } from './internal/bookmarks.ts' +import ConfiguredCustomResolver from './internal/resolver/configured-custom-resolver.ts' + +import { + ACCESS_MODE_READ, + ACCESS_MODE_WRITE, + FETCH_ALL, + DEFAULT_CONNECTION_TIMEOUT_MILLIS, + DEFAULT_POOL_ACQUISITION_TIMEOUT, + DEFAULT_POOL_MAX_SIZE +} from './internal/constants.ts' +import { Logger } from './internal/logger.ts' +import Session from './session.ts' +import { ServerInfo } from './result-summary.ts' +import { ENCRYPTION_ON } from './internal/util.ts' +import { + EncryptionLevel, + LoggingConfig, + TrustStrategy, + SessionMode +} from './types.ts' +import { ServerAddress } from './internal/server-address.ts' +import BookmarkManager from './bookmark-manager.ts' + +const DEFAULT_MAX_CONNECTION_LIFETIME: number = 60 * 60 * 1000 // 1 hour + +/** + * The default record fetch size. This is used in Bolt V4 protocol to pull query execution result in batches. + * @type {number} + */ +const DEFAULT_FETCH_SIZE: number = 1000 + +/** + * Constant that represents read session access mode. + * Should be used like this: `driver.session({ defaultAccessMode: neo4j.session.READ })`. + * @type {string} + */ +const READ: SessionMode = ACCESS_MODE_READ + +/** + * Constant that represents write session access mode. + * Should be used like this: `driver.session({ defaultAccessMode: neo4j.session.WRITE })`. + * @type {string} + */ +const WRITE: SessionMode = ACCESS_MODE_WRITE + +let idGenerator = 0 + +interface MetaInfo { + routing: boolean + typename: string + address: string | ServerAddress +} + +type CreateConnectionProvider = ( + id: number, + config: Object, + log: Logger, + hostNameResolver: ConfiguredCustomResolver +) => ConnectionProvider + +type CreateSession = (args: { + mode: SessionMode + connectionProvider: ConnectionProvider + bookmarks?: Bookmarks + database: string + config: any + reactive: boolean + fetchSize: number + impersonatedUser?: string + bookmarkManager?: BookmarkManager +}) => Session + +interface DriverConfig { + encrypted?: EncryptionLevel | boolean + trust?: TrustStrategy + fetchSize?: number + logging?: LoggingConfig +} + +/** + * The session configuration + * + * @interface + */ +class SessionConfig { + defaultAccessMode?: SessionMode + bookmarks?: string | string[] + database?: string + impersonatedUser?: string + fetchSize?: number + bookmarkManager?: BookmarkManager + + /** + * @constructor + * @private + */ + constructor () { + /** + * The access mode of this session, allowed values are {@link READ} and {@link WRITE}. + * **Default**: {@link WRITE} + * @type {string} + */ + this.defaultAccessMode = WRITE + /** + * The initial reference or references to some previous + * transactions. Value is optional and absence indicates that that the bookmarks do not exist or are unknown. + * @type {string|string[]|undefined} + */ + this.bookmarks = [] + + /** + * The database this session will operate on. + * + * @type {string|undefined} + */ + this.database = '' + + /** + * The username which the user wants to impersonate for the duration of the session. + * + * @type {string|undefined} + */ + this.impersonatedUser = undefined + + /** + * The record fetch size of each batch of this session. + * + * Use {@link FETCH_ALL} to always pull all records in one batch. This will override the config value set on driver config. + * + * @type {number|undefined} + */ + this.fetchSize = undefined + /** + * Configure a BookmarkManager for the session to use + * + * A BookmarkManager is a piece of software responsible for keeping casual consistency between different sessions by sharing bookmarks + * between the them. + * Enabling it is done by supplying an BookmarkManager implementation instance to this param. + * A default implementation could be acquired by calling the factory function {@link bookmarkManager}. + * + * **Warning**: Share the same BookmarkManager instance accross all session can have a negative impact + * on performance since all the queries will wait for the latest changes being propagated across the cluster. + * For keeping consistency between a group of queries, use {@link Session} for grouping them. + * For keeping consistency between a group of sessions, use {@link BookmarkManager} instance for groupping them. + * + * @example + * const bookmarkManager = neo4j.bookmarkManager() + * const linkedSession1 = driver.session({ database:'neo4j', bookmarkManager }) + * const linkedSession2 = driver.session({ database:'neo4j', bookmarkManager }) + * const unlinkedSession = driver.session({ database:'neo4j' }) + * + * // Creating Driver User + * const createUserQueryResult = await linkedSession1.run('CREATE (p:Person {name: $name})', { name: 'Driver User'}) + * + * // Reading Driver User will *NOT* wait of the changes being propagated to the server before RUN the query + * // So the 'Driver User' person might not exist in the Result + * const unlinkedReadResult = await unlinkedSession.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) + * + * // Reading Driver User will wait of the changes being propagated to the server before RUN the query + * // So the 'Driver User' person should exist in the Result, unless deleted. + * const linkedSesssion2 = await linkedSession2.run('CREATE (p:Person {name: $name}) RETURN p', { name: 'Driver User'}) + * + * await linkedSession1.close() + * await linkedSession2.close() + * await unlinkedSession.close() + * + * @experimental + * @type {BookmarkManager|undefined} + * @since 5.0 + */ + this.bookmarkManager = undefined + } +} + +/** + * A driver maintains one or more {@link Session}s with a remote + * Neo4j instance. Through the {@link Session}s you can send queries + * and retrieve results from the database. + * + * Drivers are reasonably expensive to create - you should strive to keep one + * driver instance around per Neo4j Instance you connect to. + * + * @access public + */ +class Driver { + private readonly _id: number + private readonly _meta: MetaInfo + private readonly _config: DriverConfig + private readonly _log: Logger + private readonly _createConnectionProvider: CreateConnectionProvider + private _connectionProvider: ConnectionProvider | null + private readonly _createSession: CreateSession + + /** + * You should not be calling this directly, instead use {@link driver}. + * @constructor + * @protected + * @param {Object} meta Metainformation about the driver + * @param {Object} config + * @param {function(id: number, config:Object, log:Logger, hostNameResolver: ConfiguredCustomResolver): ConnectionProvider } createConnectonProvider Creates the connection provider + * @param {function(args): Session } createSession Creates the a session + */ + constructor ( + meta: MetaInfo, + config: DriverConfig = {}, + createConnectonProvider: CreateConnectionProvider, + createSession: CreateSession = args => new Session(args) + ) { + sanitizeConfig(config) + + const log = Logger.create(config) + + validateConfig(config, log) + + this._id = idGenerator++ + this._meta = meta + this._config = config + this._log = log + this._createConnectionProvider = createConnectonProvider + this._createSession = createSession + + /** + * Reference to the connection provider. Initialized lazily by {@link _getOrCreateConnectionProvider}. + * @type {ConnectionProvider} + * @protected + */ + this._connectionProvider = null + + this._afterConstruction() + } + + /** + * Verifies connectivity of this driver by trying to open a connection with the provided driver options. + * + * @deprecated This return of this method will change in 6.0.0 to not async return the {@link ServerInfo} and + * async return {@link void} instead. If you need to use the server info, use {@link getServerInfo} instead. + * + * @public + * @param {Object} param - The object parameter + * @param {string} param.database - The target database to verify connectivity for. + * @returns {Promise} promise resolved with server info or rejected with error. + */ + verifyConnectivity ({ database = '' }: { database?: string } = {}): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.verifyConnectivityAndGetServerInfo({ database, accessMode: READ }) + } + + /** + * Get ServerInfo for the giver database. + * + * @param {Object} param - The object parameter + * @param {string} param.database - The target database to verify connectivity for. + * @returns {Promise} promise resolved with void or rejected with error. + */ + getServerInfo ({ database = '' }: { database?: string } = {}): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.verifyConnectivityAndGetServerInfo({ database, accessMode: READ }) + } + + /** + * Returns whether the server supports multi database capabilities based on the protocol + * version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsMultiDb (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsMultiDb() + } + + /** + * Returns whether the server supports transaction config capabilities based on the protocol + * version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsTransactionConfig (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsTransactionConfig() + } + + /** + * Returns whether the server supports user impersonation capabilities based on the protocol + * version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsUserImpersonation (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsUserImpersonation() + } + + /** + * Returns the protocol version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} the protocol version negotiated via handshake. + * @throws {Error} When protocol negotiation fails + */ + getNegotiatedProtocolVersion (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.getNegotiatedProtocolVersion() + } + + /** + * Returns boolean to indicate if driver has been configured with encryption enabled. + * + * @returns {boolean} + */ + isEncrypted (): boolean { + return this._isEncrypted() + } + + /** + * @protected + * @returns {boolean} + */ + _supportsRouting (): boolean { + return this._meta.routing + } + + /** + * Returns boolean to indicate if driver has been configured with encryption enabled. + * + * @protected + * @returns {boolean} + */ + _isEncrypted (): boolean { + return this._config.encrypted === ENCRYPTION_ON || this._config.encrypted === true + } + + /** + * Returns the configured trust strategy that the driver has been configured with. + * + * @protected + * @returns {TrustStrategy} + */ + _getTrust (): TrustStrategy | undefined { + return this._config.trust + } + + /** + * Acquire a session to communicate with the database. The session will + * borrow connections from the underlying connection pool as required and + * should be considered lightweight and disposable. + * + * This comes with some responsibility - make sure you always call + * {@link close} when you are done using a session, and likewise, + * make sure you don't close your session before you are done using it. Once + * it is closed, the underlying connection will be released to the connection + * pool and made available for others to use. + * + * @public + * @param {SessionConfig} param - The session configuration + * @return {Session} new session. + */ + session ({ + defaultAccessMode = WRITE, + bookmarks: bookmarkOrBookmarks, + database = '', + impersonatedUser, + fetchSize, + bookmarkManager + }: SessionConfig = {}): Session { + return this._newSession({ + defaultAccessMode, + bookmarkOrBookmarks, + database, + reactive: false, + impersonatedUser, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), + bookmarkManager + }) + } + + /** + * Close all open sessions and other associated resources. You should + * make sure to use this when you are done with this driver instance. + * @public + * @return {Promise} promise resolved when the driver is closed. + */ + close (): Promise { + this._log.info(`Driver ${this._id} closing`) + if (this._connectionProvider != null) { + return this._connectionProvider.close() + } + return Promise.resolve() + } + + /** + * @protected + */ + _afterConstruction (): void { + this._log.info( + `${this._meta.typename} driver ${this._id} created for server address ${this._meta.address.toString()}` + ) + } + + /** + * @private + */ + _newSession ({ + defaultAccessMode, + bookmarkOrBookmarks, + database, + reactive, + impersonatedUser, + fetchSize, + bookmarkManager + }: { + defaultAccessMode: SessionMode + bookmarkOrBookmarks?: string | string[] + database: string + reactive: boolean + impersonatedUser?: string + fetchSize: number + bookmarkManager?: BookmarkManager + }): Session { + const sessionMode = Session._validateSessionMode(defaultAccessMode) + const connectionProvider = this._getOrCreateConnectionProvider() + const bookmarks = bookmarkOrBookmarks != null + ? new Bookmarks(bookmarkOrBookmarks) + : Bookmarks.empty() + + return this._createSession({ + mode: sessionMode, + database: database ?? '', + connectionProvider, + bookmarks, + config: this._config, + reactive, + impersonatedUser, + fetchSize, + bookmarkManager + }) + } + + /** + * @private + */ + _getOrCreateConnectionProvider (): ConnectionProvider { + if (this._connectionProvider == null) { + this._connectionProvider = this._createConnectionProvider( + this._id, + this._config, + this._log, + createHostNameResolver(this._config) + ) + } + + return this._connectionProvider + } +} + +/** + * @private + * @returns {Object} the given config. + */ +function validateConfig (config: any, log: Logger): any { + const resolver = config.resolver + if (resolver !== null && resolver !== undefined && typeof resolver !== 'function') { + throw new TypeError( + `Configured resolver should be a function. Got: ${typeof resolver}` + ) + } + + if (config.connectionAcquisitionTimeout < config.connectionTimeout) { + log.warn( + 'Configuration for "connectionAcquisitionTimeout" should be greater than ' + + 'or equal to "connectionTimeout". Otherwise, the connection acquisition ' + + 'timeout will take precedence for over the connection timeout in scenarios ' + + 'where a new connection is created while it is acquired' + ) + } + return config +} + +/** + * @private + */ +function sanitizeConfig (config: any): void { + config.maxConnectionLifetime = sanitizeIntValue( + config.maxConnectionLifetime, + DEFAULT_MAX_CONNECTION_LIFETIME + ) + config.maxConnectionPoolSize = sanitizeIntValue( + config.maxConnectionPoolSize, + DEFAULT_POOL_MAX_SIZE + ) + config.connectionAcquisitionTimeout = sanitizeIntValue( + config.connectionAcquisitionTimeout, + DEFAULT_POOL_ACQUISITION_TIMEOUT + ) + config.fetchSize = validateFetchSizeValue( + config.fetchSize, + DEFAULT_FETCH_SIZE + ) + config.connectionTimeout = extractConnectionTimeout(config) +} + +/** + * @private + */ +function sanitizeIntValue (rawValue: any, defaultWhenAbsent: number): number { + const sanitizedValue = parseInt(rawValue, 10) + if (sanitizedValue > 0 || sanitizedValue === 0) { + return sanitizedValue + } else if (sanitizedValue < 0) { + return Number.MAX_SAFE_INTEGER + } else { + return defaultWhenAbsent + } +} + +/** + * @private + */ +function validateFetchSizeValue ( + rawValue: any, + defaultWhenAbsent: number +): number { + const fetchSize = parseInt(rawValue, 10) + if (fetchSize > 0 || fetchSize === FETCH_ALL) { + return fetchSize + } else if (fetchSize === 0 || fetchSize < 0) { + throw new Error( + `The fetch size can only be a positive value or ${FETCH_ALL} for ALL. However fetchSize = ${fetchSize}` + ) + } else { + return defaultWhenAbsent + } +} + +/** + * @private + */ +function extractConnectionTimeout (config: any): number | null { + const configuredTimeout = parseInt(config.connectionTimeout, 10) + if (configuredTimeout === 0) { + // timeout explicitly configured to 0 + return null + } else if (!isNaN(configuredTimeout) && configuredTimeout < 0) { + // timeout explicitly configured to a negative value + return null + } else if (isNaN(configuredTimeout)) { + // timeout not configured, use default value + return DEFAULT_CONNECTION_TIMEOUT_MILLIS + } else { + // timeout configured, use the provided value + return configuredTimeout + } +} + +/** + * @private + * @returns {ConfiguredCustomResolver} new custom resolver that wraps the passed-in resolver function. + * If resolved function is not specified, it defaults to an identity resolver. + */ +function createHostNameResolver (config: any): ConfiguredCustomResolver { + return new ConfiguredCustomResolver(config.resolver) +} + +export { Driver, READ, WRITE } +export type { SessionConfig } +export default Driver diff --git a/packages/neo4j-driver-deno/lib/core/error.ts b/packages/neo4j-driver-deno/lib/core/error.ts new file mode 100644 index 000000000..a2454be8f --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/error.ts @@ -0,0 +1,160 @@ +/** + * 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. + */ + +// A common place for constructing error objects, to keep them +// uniform across the driver surface. + +/** + * Error code representing complete loss of service. Used by {@link Neo4jError#code}. + * @type {string} + */ +const SERVICE_UNAVAILABLE: string = 'ServiceUnavailable' + +/** + * Error code representing transient loss of service. Used by {@link Neo4jError#code}. + * @type {string} + */ +const SESSION_EXPIRED: string = 'SessionExpired' + +/** + * Error code representing serialization/deserialization issue in the Bolt protocol. Used by {@link Neo4jError#code}. + * @type {string} + */ +const PROTOCOL_ERROR: string = 'ProtocolError' + +/** + * Error code representing an no classified error. Used by {@link Neo4jError#code}. + * @type {string} + */ +const NOT_AVAILABLE: string = 'N/A' + +/** + * Possible error codes in the {@link Neo4jError} + */ +type Neo4jErrorCode = + | typeof SERVICE_UNAVAILABLE + | typeof SESSION_EXPIRED + | typeof PROTOCOL_ERROR + | typeof NOT_AVAILABLE + +/// TODO: Remove definitions of this.constructor and this.__proto__ +/** + * Class for all errors thrown/returned by the driver. + */ +class Neo4jError extends Error { + /** + * Optional error code. Will be populated when error originates in the database. + */ + code: Neo4jErrorCode + retriable: boolean + __proto__: Neo4jError + + /** + * @constructor + * @param {string} message - the error message + * @param {string} code - Optional error code. Will be populated when error originates in the database. + */ + constructor (message: string, code: Neo4jErrorCode, cause?: Error) { + // eslint-disable-next-line + // @ts-ignore: not available in ES6 yet + super(message, cause != null ? { cause } : undefined) + this.constructor = Neo4jError + // eslint-disable-next-line no-proto + this.__proto__ = Neo4jError.prototype + this.code = code + this.name = 'Neo4jError' + /** + * Indicates if the error is retriable. + * @type {boolean} - true if the error is retriable + */ + this.retriable = _isRetriableCode(code) + } + + /** + * Verifies if the given error is retriable. + * + * @param {object|undefined|null} error the error object + * @returns {boolean} true if the error is retriable + */ + static isRetriable (error?: any | null): boolean { + return error !== null && + error !== undefined && + error instanceof Neo4jError && + error.retriable + } +} + +/** + * Create a new error from a message and error code + * @param message the error message + * @param code the error code + * @return {Neo4jError} an {@link Neo4jError} + * @private + */ +function newError (message: string, code?: Neo4jErrorCode, cause?: Error): Neo4jError { + return new Neo4jError(message, code ?? NOT_AVAILABLE, cause) +} + +/** + * Verifies if the given error is retriable. + * + * @public + * @param {object|undefined|null} error the error object + * @returns {boolean} true if the error is retriable + */ +const isRetriableError = Neo4jError.isRetriable + +/** + * @private + * @param {string} code the error code + * @returns {boolean} true if the error is a retriable error + */ +function _isRetriableCode (code?: Neo4jErrorCode): boolean { + return code === SERVICE_UNAVAILABLE || + code === SESSION_EXPIRED || + _isAuthorizationExpired(code) || + _isTransientError(code) +} + +/** + * @private + * @param {string} code the error to check + * @return {boolean} true if the error is a transient error + */ +function _isTransientError (code?: Neo4jErrorCode): boolean { + return code?.includes('TransientError') === true +} + +/** + * @private + * @param {string} code the error to check + * @returns {boolean} true if the error is a service unavailable error + */ +function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean { + return code === 'Neo.ClientError.Security.AuthorizationExpired' +} + +export { + newError, + isRetriableError, + Neo4jError, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED, + PROTOCOL_ERROR +} diff --git a/packages/neo4j-driver-deno/lib/core/graph-types.ts b/packages/neo4j-driver-deno/lib/core/graph-types.ts new file mode 100644 index 000000000..8ec873917 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/graph-types.ts @@ -0,0 +1,474 @@ +/** + * 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 Integer from './integer.ts' +import { stringify } from './json.ts' + +type StandardDate = Date +/** + * @typedef {number | Integer | bigint} NumberOrInteger + */ +type NumberOrInteger = number | Integer | bigint +interface Properties { [key: string]: any } + +const IDENTIFIER_PROPERTY_ATTRIBUTES = { + value: true, + enumerable: false, + configurable: false, + writable: false +} + +const NODE_IDENTIFIER_PROPERTY: string = '__isNode__' +const RELATIONSHIP_IDENTIFIER_PROPERTY: string = '__isRelationship__' +const UNBOUND_RELATIONSHIP_IDENTIFIER_PROPERTY: string = + '__isUnboundRelationship__' +const PATH_IDENTIFIER_PROPERTY: string = '__isPath__' +const PATH_SEGMENT_IDENTIFIER_PROPERTY: string = '__isPathSegment__' + +function hasIdentifierProperty (obj: any, property: string): boolean { + return obj != null && obj[property] === true +} + +/** + * Class for Node Type. + */ +class Node { + identity: T + labels: string[] + properties: P + elementId: string + /** + * @constructor + * @protected + * @param {NumberOrInteger} identity - Unique identity + * @param {Array} 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, elementId?: string) { + /** + * Identity of the node. + * @type {NumberOrInteger} + * @deprecated use {@link Node#elementId} instead + */ + this.identity = identity + /** + * Labels of the node. + * @type {string[]} + */ + this.labels = labels + /** + * Properties of the node. + * @type {Properties} + */ + this.properties = properties + /** + * The Node element identifier. + * @type {string} + */ + this.elementId = _valueOrGetDefault(elementId, () => identity.toString()) + } + + /** + * @ignore + */ + toString (): string { + let s = '(' + this.elementId + for (let i = 0; i < this.labels.length; i++) { + s += ':' + this.labels[i] + } + const keys = Object.keys(this.properties) + if (keys.length > 0) { + s += ' {' + for (let i = 0; i < keys.length; i++) { + if (i > 0) s += ',' + s += keys[i] + ':' + stringify(this.properties[keys[i]]) + } + s += '}' + } + s += ')' + return s + } +} + +Object.defineProperty( + Node.prototype, + NODE_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Node} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Node}, `false` otherwise. + */ +function isNode (obj: object): obj is Node { + return hasIdentifierProperty(obj, NODE_IDENTIFIER_PROPERTY) +} + +/** + * Class for Relationship Type. + */ +class Relationship { + identity: T + start: T + end: T + type: string + properties: P + elementId: string + startNodeElementId: string + endNodeElementId: string + + /** + * @constructor + * @protected + * @param {NumberOrInteger} identity - Unique identity + * @param {NumberOrInteger} start - Identity of start Node + * @param {NumberOrInteger} end - Identity of end Node + * @param {string} type - Relationship type + * @param {Properties} properties - Map with relationship properties + * @param {string} elementId - Relationship element identifier + * @param {string} startNodeElementId - Start Node element identifier + * @param {string} endNodeElementId - End Node element identifier + */ + constructor ( + identity: T, start: T, end: T, type: string, properties: P, + elementId?: string, startNodeElementId?: string, endNodeElementId?: string + ) { + /** + * Identity of the relationship. + * @type {NumberOrInteger} + * @deprecated use {@link Relationship#elementId} instead + */ + this.identity = identity + /** + * Identity of the start node. + * @type {NumberOrInteger} + * @deprecated use {@link Relationship#startNodeElementId} instead + */ + this.start = start + /** + * Identity of the end node. + * @type {NumberOrInteger} + * @deprecated use {@link Relationship#endNodeElementId} instead + */ + this.end = end + /** + * Type of the relationship. + * @type {string} + */ + this.type = type + /** + * Properties of the relationship. + * @type {Properties} + */ + this.properties = properties + + /** + * The Relationship element identifier. + * @type {string} + */ + this.elementId = _valueOrGetDefault(elementId, () => 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.startNodeElementId + ')-[:' + this.type + const keys = Object.keys(this.properties) + if (keys.length > 0) { + s += ' {' + for (let i = 0; i < keys.length; i++) { + if (i > 0) s += ',' + s += keys[i] + ':' + stringify(this.properties[keys[i]]) + } + s += '}' + } + s += ']->(' + this.endNodeElementId + ')' + return s + } +} + +Object.defineProperty( + Relationship.prototype, + RELATIONSHIP_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Relationship} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Relationship}, `false` otherwise. + */ +function isRelationship (obj: object): obj is Relationship { + return hasIdentifierProperty(obj, RELATIONSHIP_IDENTIFIER_PROPERTY) +} + +/** + * Class for UnboundRelationship Type. + * @access private + */ +class UnboundRelationship { + identity: T + type: string + properties: P + elementId: string + + /** + * @constructor + * @protected + * @param {NumberOrInteger} identity - Unique identity + * @param {string} type - Relationship type + * @param {Properties} properties - Map with relationship properties + * @param {string} elementId - Relationship element identifier + */ + constructor (identity: T, type: string, properties: any, elementId?: string) { + /** + * Identity of the relationship. + * @type {NumberOrInteger} + * @deprecated use {@link UnboundRelationship#elementId} instead + */ + this.identity = identity + /** + * Type of the relationship. + * @type {string} + */ + this.type = type + /** + * Properties of the relationship. + * @type {Properties} + */ + this.properties = properties + + /** + * The Relationship element identifier. + * @type {string} + */ + this.elementId = _valueOrGetDefault(elementId, () => 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 + */ + bind (start: T, end: T): Relationship { + return new Relationship( + this.identity, + start, + end, + this.type, + this.properties, + this.elementId + ) + } + + /** + * Bind relationship + * + * @protected + * @param {Node} start - Start Node + * @param {Node} end - End Node + * @return {Relationship} - Created relationship + */ + bindTo (start: Node, end: Node): Relationship { + return new Relationship( + this.identity, + start.identity, + end.identity, + this.type, + this.properties, + this.elementId, + start.elementId, + end.elementId + ) + } + + /** + * @ignore + */ + toString (): string { + let s = '-[:' + this.type + const keys = Object.keys(this.properties) + if (keys.length > 0) { + s += ' {' + for (let i = 0; i < keys.length; i++) { + if (i > 0) s += ',' + s += keys[i] + ':' + stringify(this.properties[keys[i]]) + } + s += '}' + } + s += ']->' + return s + } +} + +Object.defineProperty( + UnboundRelationship.prototype, + UNBOUND_RELATIONSHIP_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link UnboundRelationship} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link UnboundRelationship}, `false` otherwise. + */ +function isUnboundRelationship (obj: object): obj is UnboundRelationship { + return hasIdentifierProperty(obj, UNBOUND_RELATIONSHIP_IDENTIFIER_PROPERTY) +} + +/** + * Class for PathSegment Type. + */ +class PathSegment { + start: Node + relationship: Relationship + end: Node + /** + * @constructor + * @protected + * @param {Node} start - start node + * @param {Relationship} rel - relationship that connects start and end node + * @param {Node} end - end node + */ + constructor (start: Node, rel: Relationship, end: Node) { + /** + * Start node. + * @type {Node} + */ + this.start = start + /** + * Relationship. + * @type {Relationship} + */ + this.relationship = rel + /** + * End node. + * @type {Node} + */ + this.end = end + } +} + +Object.defineProperty( + PathSegment.prototype, + PATH_SEGMENT_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link PathSegment} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link PathSegment}, `false` otherwise. + */ +function isPathSegment (obj: object): obj is PathSegment { + return hasIdentifierProperty(obj, PATH_SEGMENT_IDENTIFIER_PROPERTY) +} + +/** + * Class for Path Type. + */ +class Path { + start: Node + end: Node + segments: Array> + length: number + /** + * @constructor + * @protected + * @param {Node} start - start node + * @param {Node} end - end node + * @param {Array} segments - Array of Segments + */ + constructor (start: Node, end: Node, segments: Array>) { + /** + * Start node. + * @type {Node} + */ + this.start = start + /** + * End node. + * @type {Node} + */ + this.end = end + /** + * Segments. + * @type {Array} + */ + this.segments = segments + /** + * Length of the segments. + * @type {Number} + */ + this.length = segments.length + } +} + +Object.defineProperty( + Path.prototype, + PATH_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Path} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Path}, `false` otherwise. + */ +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, + Relationship, + isRelationship, + UnboundRelationship, + isUnboundRelationship, + Path, + isPath, + PathSegment, + isPathSegment +} +export type { + StandardDate, + NumberOrInteger +} diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts new file mode 100644 index 000000000..1cd30eeea --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -0,0 +1,227 @@ +/** + * 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 { + newError, + Neo4jError, + isRetriableError, + PROTOCOL_ERROR, + SERVICE_UNAVAILABLE, + SESSION_EXPIRED +} from './error.ts' +import Integer, { int, isInt, inSafeRange, toNumber, toString } from './integer.ts' +import { + Date, + DateTime, + Duration, + isDate, + isDateTime, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time +} from './temporal-types.ts' +import { + StandardDate, + NumberOrInteger, + Node, + isNode, + Relationship, + isRelationship, + UnboundRelationship, + isUnboundRelationship, + Path, + isPath, + PathSegment, + isPathSegment +} from './graph-types.ts' +import Record from './record.ts' +import { isPoint, Point } from './spatial-types.ts' +import ResultSummary, { + queryType, + ServerInfo, + Notification, + NotificationPosition, + Plan, + ProfiledPlan, + QueryStatistics, + Stats +} from './result-summary.ts' +import Result, { QueryResult, ResultObserver } from './result.ts' +import ConnectionProvider from './connection-provider.ts' +import Connection from './connection.ts' +import Transaction from './transaction.ts' +import ManagedTransaction from './transaction-managed.ts' +import TransactionPromise from './transaction-promise.ts' +import Session, { TransactionConfig } from './session.ts' +import Driver, * as driver from './driver.ts' +import auth from './auth.ts' +import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' +import { SessionConfig } from './driver.ts' +import * as types from './types.ts' +import * as json from './json.ts' +import * as internal from './internal/index.ts' + +/** + * Object containing string constants representing predefined {@link Neo4jError} codes. + */ +const error = { + SERVICE_UNAVAILABLE, + SESSION_EXPIRED, + PROTOCOL_ERROR +} + +/** + * @private + */ +const forExport = { + newError, + Neo4jError, + isRetriableError, + error, + Integer, + int, + isInt, + inSafeRange, + toNumber, + toString, + internal, + isPoint, + Point, + Date, + DateTime, + Duration, + isDate, + isDateTime, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time, + Node, + isNode, + Relationship, + isRelationship, + UnboundRelationship, + isUnboundRelationship, + Path, + isPath, + PathSegment, + isPathSegment, + Record, + ResultSummary, + queryType, + ServerInfo, + Notification, + Plan, + ProfiledPlan, + QueryStatistics, + Stats, + Result, + Transaction, + ManagedTransaction, + TransactionPromise, + Session, + Driver, + Connection, + types, + driver, + json, + auth, + bookmarkManager +} + +export { + newError, + Neo4jError, + isRetriableError, + error, + Integer, + int, + isInt, + inSafeRange, + toNumber, + toString, + internal, + isPoint, + Point, + Date, + DateTime, + Duration, + isDate, + isDateTime, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time, + Node, + isNode, + Relationship, + isRelationship, + UnboundRelationship, + isUnboundRelationship, + Path, + isPath, + PathSegment, + isPathSegment, + Record, + ResultSummary, + queryType, + ServerInfo, + Notification, + Plan, + ProfiledPlan, + QueryStatistics, + Stats, + Result, + ConnectionProvider, + Connection, + Transaction, + ManagedTransaction, + TransactionPromise, + Session, + Driver, + types, + driver, + json, + auth, + bookmarkManager +} + +export type { + StandardDate, + NumberOrInteger, + NotificationPosition, + QueryResult, + ResultObserver, + TransactionConfig, + BookmarkManager, + BookmarkManagerConfig, + SessionConfig +} + +export default forExport diff --git a/packages/neo4j-driver-deno/lib/core/integer.ts b/packages/neo4j-driver-deno/lib/core/integer.ts new file mode 100644 index 000000000..3f70960ed --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/integer.ts @@ -0,0 +1,1093 @@ +/** + * 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. + */ + +// 64-bit Integer library, originally from Long.js by dcodeIO +// https://github.com/dcodeIO/Long.js +// License Apache 2 + +import { newError } from './error.ts' + +/** + * A cache of the Integer representations of small integer values. + * @type {!Object} + * @inner + * @private + */ +// eslint-disable-next-line no-use-before-define +const INT_CACHE: Map = new Map() + +/** + * Constructs a 64 bit two's-complement integer, given its low and high 32 bit values as *signed* integers. + * See exported functions for more convenient ways of operating integers. + * Use `int()` function to create new integers, `isInt()` to check if given object is integer, + * `inSafeRange()` to check if it is safe to convert given value to native number, + * `toNumber()` and `toString()` to convert given integer to number or string respectively. + * @access public + * @exports Integer + * @class A Integer class for representing a 64 bit two's-complement integer value. + * @param {number} low The low (signed) 32 bits of the long + * @param {number} high The high (signed) 32 bits of the long + * + * @constructor + */ +class Integer { + low: number + high: number + + constructor (low?: number, high?: number) { + /** + * The low 32 bits as a signed value. + * @type {number} + * @expose + */ + this.low = low ?? 0 + + /** + * The high 32 bits as a signed value. + * @type {number} + * @expose + */ + this.high = high ?? 0 + } + + // The internal representation of an Integer is the two given signed, 32-bit values. + // We use 32-bit pieces because these are the size of integers on which + // JavaScript performs bit-operations. For operations like addition and + // multiplication, we split each number into 16 bit pieces, which can easily be + // multiplied within JavaScript's floating-point representation without overflow + // or change in sign. + // + // In the algorithms below, we frequently reduce the negative case to the + // positive case by negating the input(s) and then post-processing the result. + // Note that we must ALWAYS check specially whether those values are MIN_VALUE + // (-2^63) because -MIN_VALUE == MIN_VALUE (since 2^63 cannot be represented as + // a positive number, it overflows back into a negative). Not handling this + // case would often result in infinite recursion. + // + // Common constant values ZERO, ONE, NEG_ONE, etc. are defined below the from* + // methods on which they depend. + + inSafeRange (): boolean { + return ( + this.greaterThanOrEqual(Integer.MIN_SAFE_VALUE) && + this.lessThanOrEqual(Integer.MAX_SAFE_VALUE) + ) + } + + /** + * Converts the Integer to an exact javascript Number, assuming it is a 32 bit integer. + * @returns {number} + * @expose + */ + toInt (): number { + return this.low + } + + /** + * Converts the Integer to a the nearest floating-point representation of this value (double, 53 bit mantissa). + * @returns {number} + * @expose + */ + toNumber (): number { + return this.high * TWO_PWR_32_DBL + (this.low >>> 0) + } + + /** + * Converts the Integer to a BigInt representation of this value + * @returns {bigint} + * @expose + */ + toBigInt (): bigint { + if (this.isZero()) { + return BigInt(0) + } else if (this.isPositive()) { + return ( + BigInt(this.high >>> 0) * BigInt(TWO_PWR_32_DBL) + + BigInt(this.low >>> 0) + ) + } else { + const negate = this.negate() + return ( + BigInt(-1) * + (BigInt(negate.high >>> 0) * BigInt(TWO_PWR_32_DBL) + + BigInt(negate.low >>> 0)) + ) + } + } + + /** + * Converts the Integer to native number or -Infinity/+Infinity when it does not fit. + * @return {number} + * @package + */ + toNumberOrInfinity (): number { + if (this.lessThan(Integer.MIN_SAFE_VALUE)) { + return Number.NEGATIVE_INFINITY + } else if (this.greaterThan(Integer.MAX_SAFE_VALUE)) { + return Number.POSITIVE_INFINITY + } else { + return this.toNumber() + } + } + + /** + * Converts the Integer to a string written in the specified radix. + * @param {number=} radix Radix (2-36), defaults to 10 + * @returns {string} + * @override + * @throws {RangeError} If `radix` is out of range + * @expose + */ + toString (radix?: number): string { + radix = radix ?? 10 + if (radix < 2 || radix > 36) { + throw RangeError('radix out of range: ' + radix.toString()) + } + if (this.isZero()) { + return '0' + } + let rem: Integer + if (this.isNegative()) { + if (this.equals(Integer.MIN_VALUE)) { + // We need to change the Integer value before it can be negated, so we remove + // the bottom-most digit in this base and then recurse to do the rest. + const radixInteger = Integer.fromNumber(radix) + const div = this.div(radixInteger) + rem = div.multiply(radixInteger).subtract(this) + return div.toString(radix) + rem.toInt().toString(radix) + } else { + return '-' + this.negate().toString(radix) + } + } + + // Do several (6) digits each time through the loop, so as to + // minimize the calls to the very expensive emulated div. + const radixToPower = Integer.fromNumber(Math.pow(radix, 6)) + rem = this + let result = '' + while (true) { + const remDiv = rem.div(radixToPower) + const intval = rem.subtract(remDiv.multiply(radixToPower)).toInt() >>> 0 + let digits = intval.toString(radix) + rem = remDiv + if (rem.isZero()) { + return digits + result + } else { + while (digits.length < 6) { + digits = '0' + digits + } + result = '' + digits + result + } + } + } + + /** + * Gets the high 32 bits as a signed integer. + * @returns {number} Signed high bits + * @expose + */ + getHighBits (): number { + return this.high + } + + /** + * Gets the low 32 bits as a signed integer. + * @returns {number} Signed low bits + * @expose + */ + getLowBits (): number { + return this.low + } + + /** + * Gets the number of bits needed to represent the absolute value of this Integer. + * @returns {number} + * @expose + */ + getNumBitsAbs (): number { + if (this.isNegative()) { + return this.equals(Integer.MIN_VALUE) ? 64 : this.negate().getNumBitsAbs() + } + const val = this.high !== 0 ? this.high : this.low + let bit = 0 + for (bit = 31; bit > 0; bit--) { + if ((val & (1 << bit)) !== 0) { + break + } + } + return this.high !== 0 ? bit + 33 : bit + 1 + } + + /** + * Tests if this Integer's value equals zero. + * @returns {boolean} + * @expose + */ + isZero (): boolean { + return this.high === 0 && this.low === 0 + } + + /** + * Tests if this Integer's value is negative. + * @returns {boolean} + * @expose + */ + isNegative (): boolean { + return this.high < 0 + } + + /** + * Tests if this Integer's value is positive. + * @returns {boolean} + * @expose + */ + isPositive (): boolean { + return this.high >= 0 + } + + /** + * Tests if this Integer's value is odd. + * @returns {boolean} + * @expose + */ + isOdd (): boolean { + return (this.low & 1) === 1 + } + + /** + * Tests if this Integer's value is even. + * @returns {boolean} + * @expose + */ + isEven (): boolean { + return (this.low & 1) === 0 + } + + /** + * Tests if this Integer's value equals the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + equals (other: Integerable): boolean { + const theOther = Integer.fromValue(other) + return this.high === theOther.high && this.low === theOther.low + } + + /** + * Tests if this Integer's value differs from the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + notEquals (other: Integerable): boolean { + return !this.equals(/* validates */ other) + } + + /** + * Tests if this Integer's value is less than the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + lessThan (other: Integerable): boolean { + return this.compare(/* validates */ other) < 0 + } + + /** + * Tests if this Integer's value is less than or equal the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + lessThanOrEqual (other: Integerable): boolean { + return this.compare(/* validates */ other) <= 0 + } + + /** + * Tests if this Integer's value is greater than the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + greaterThan (other: Integerable): boolean { + return this.compare(/* validates */ other) > 0 + } + + /** + * Tests if this Integer's value is greater than or equal the specified's. + * @param {!Integer|number|string} other Other value + * @returns {boolean} + * @expose + */ + greaterThanOrEqual (other: Integerable): boolean { + return this.compare(/* validates */ other) >= 0 + } + + /** + * Compares this Integer's value with the specified's. + * @param {!Integer|number|string} other Other value + * @returns {number} 0 if they are the same, 1 if the this is greater and -1 + * if the given one is greater + * @expose + */ + compare (other: Integerable): number { + const theOther = Integer.fromValue(other) + + if (this.equals(theOther)) { + return 0 + } + const thisNeg = this.isNegative() + const otherNeg = theOther.isNegative() + if (thisNeg && !otherNeg) { + return -1 + } + if (!thisNeg && otherNeg) { + return 1 + } + // At this point the sign bits are the same + return this.subtract(theOther).isNegative() ? -1 : 1 + } + + /** + * Negates this Integer's value. + * @returns {!Integer} Negated Integer + * @expose + */ + negate (): Integer { + if (this.equals(Integer.MIN_VALUE)) { + return Integer.MIN_VALUE + } + return this.not().add(Integer.ONE) + } + + /** + * Returns the sum of this and the specified Integer. + * @param {!Integer|number|string} addend Addend + * @returns {!Integer} Sum + * @expose + */ + add (addend: Integerable): Integer { + const theAddend = Integer.fromValue(addend) + + // Divide each number into 4 chunks of 16 bits, and then sum the chunks. + + const a48 = this.high >>> 16 + const a32 = this.high & 0xffff + const a16 = this.low >>> 16 + const a00 = this.low & 0xffff + + const b48 = theAddend.high >>> 16 + const b32 = theAddend.high & 0xffff + const b16 = theAddend.low >>> 16 + const b00 = theAddend.low & 0xffff + + let c48 = 0 + let c32 = 0 + let c16 = 0 + let c00 = 0 + c00 += a00 + b00 + c16 += c00 >>> 16 + c00 &= 0xffff + c16 += a16 + b16 + c32 += c16 >>> 16 + c16 &= 0xffff + c32 += a32 + b32 + c48 += c32 >>> 16 + c32 &= 0xffff + c48 += a48 + b48 + c48 &= 0xffff + return Integer.fromBits((c16 << 16) | c00, (c48 << 16) | c32) + } + + /** + * Returns the difference of this and the specified Integer. + * @param {!Integer|number|string} subtrahend Subtrahend + * @returns {!Integer} Difference + * @expose + */ + subtract (subtrahend: Integerable): Integer { + const theSubtrahend = Integer.fromValue(subtrahend) + return this.add(theSubtrahend.negate()) + } + + /** + * Returns the product of this and the specified Integer. + * @param {!Integer|number|string} multiplier Multiplier + * @returns {!Integer} Product + * @expose + */ + multiply (multiplier: Integerable): Integer { + if (this.isZero()) { + return Integer.ZERO + } + + const theMultiplier = Integer.fromValue(multiplier) + + if (theMultiplier.isZero()) { + return Integer.ZERO + } + if (this.equals(Integer.MIN_VALUE)) { + return theMultiplier.isOdd() ? Integer.MIN_VALUE : Integer.ZERO + } + if (theMultiplier.equals(Integer.MIN_VALUE)) { + return this.isOdd() ? Integer.MIN_VALUE : Integer.ZERO + } + + if (this.isNegative()) { + if (theMultiplier.isNegative()) { + return this.negate().multiply(theMultiplier.negate()) + } else { + return this.negate() + .multiply(theMultiplier) + .negate() + } + } else if (theMultiplier.isNegative()) { + return this.multiply(theMultiplier.negate()).negate() + } + + // If both longs are small, use float multiplication + if (this.lessThan(TWO_PWR_24) && theMultiplier.lessThan(TWO_PWR_24)) { + return Integer.fromNumber(this.toNumber() * theMultiplier.toNumber()) + } + + // Divide each long into 4 chunks of 16 bits, and then add up 4x4 products. + // We can skip products that would overflow. + + const a48 = this.high >>> 16 + const a32 = this.high & 0xffff + const a16 = this.low >>> 16 + const a00 = this.low & 0xffff + + const b48 = theMultiplier.high >>> 16 + const b32 = theMultiplier.high & 0xffff + const b16 = theMultiplier.low >>> 16 + const b00 = theMultiplier.low & 0xffff + + let c48 = 0 + let c32 = 0 + let c16 = 0 + let c00 = 0 + c00 += a00 * b00 + c16 += c00 >>> 16 + c00 &= 0xffff + c16 += a16 * b00 + c32 += c16 >>> 16 + c16 &= 0xffff + c16 += a00 * b16 + c32 += c16 >>> 16 + c16 &= 0xffff + c32 += a32 * b00 + c48 += c32 >>> 16 + c32 &= 0xffff + c32 += a16 * b16 + c48 += c32 >>> 16 + c32 &= 0xffff + c32 += a00 * b32 + c48 += c32 >>> 16 + c32 &= 0xffff + c48 += a48 * b00 + a32 * b16 + a16 * b32 + a00 * b48 + c48 &= 0xffff + return Integer.fromBits((c16 << 16) | c00, (c48 << 16) | c32) + } + + /** + * Returns this Integer divided by the specified. + * @param {!Integer|number|string} divisor Divisor + * @returns {!Integer} Quotient + * @expose + */ + div (divisor: Integerable): Integer { + const theDivisor = Integer.fromValue(divisor) + + if (theDivisor.isZero()) { + throw newError('division by zero') + } + if (this.isZero()) { + return Integer.ZERO + } + let approx, rem, res + if (this.equals(Integer.MIN_VALUE)) { + if ( + theDivisor.equals(Integer.ONE) || + theDivisor.equals(Integer.NEG_ONE) + ) { + return Integer.MIN_VALUE + } + if (theDivisor.equals(Integer.MIN_VALUE)) { + return Integer.ONE + } else { + // At this point, we have |other| >= 2, so |this/other| < |MIN_VALUE|. + const halfThis = this.shiftRight(1) + approx = halfThis.div(theDivisor).shiftLeft(1) + if (approx.equals(Integer.ZERO)) { + return theDivisor.isNegative() ? Integer.ONE : Integer.NEG_ONE + } else { + rem = this.subtract(theDivisor.multiply(approx)) + res = approx.add(rem.div(theDivisor)) + return res + } + } + } else if (theDivisor.equals(Integer.MIN_VALUE)) { + return Integer.ZERO + } + if (this.isNegative()) { + if (theDivisor.isNegative()) { + return this.negate().div(theDivisor.negate()) + } + return this.negate() + .div(theDivisor) + .negate() + } else if (theDivisor.isNegative()) { + return this.div(theDivisor.negate()).negate() + } + + // Repeat the following until the remainder is less than other: find a + // floating-point that approximates remainder / other *from below*, add this + // into the result, and subtract it from the remainder. It is critical that + // the approximate value is less than or equal to the real value so that the + // remainder never becomes negative. + res = Integer.ZERO + rem = this + while (rem.greaterThanOrEqual(theDivisor)) { + // Approximate the result of division. This may be a little greater or + // smaller than the actual value. + approx = Math.max(1, Math.floor(rem.toNumber() / theDivisor.toNumber())) + + // We will tweak the approximate result by changing it in the 48-th digit or + // the smallest non-fractional digit, whichever is larger. + const log2 = Math.ceil(Math.log(approx) / Math.LN2) + const delta = log2 <= 48 ? 1 : Math.pow(2, log2 - 48) + + // Decrease the approximation until it is smaller than the remainder. Note + // that if it is too large, the product overflows and is negative. + let approxRes = Integer.fromNumber(approx) + let approxRem = approxRes.multiply(theDivisor) + while (approxRem.isNegative() || approxRem.greaterThan(rem)) { + approx -= delta + approxRes = Integer.fromNumber(approx) + approxRem = approxRes.multiply(theDivisor) + } + + // We know the answer can't be zero... and actually, zero would cause + // infinite recursion since we would make no progress. + if (approxRes.isZero()) { + approxRes = Integer.ONE + } + + res = res.add(approxRes) + rem = rem.subtract(approxRem) + } + return res + } + + /** + * Returns this Integer modulo the specified. + * @param {!Integer|number|string} divisor Divisor + * @returns {!Integer} Remainder + * @expose + */ + modulo (divisor: Integerable): Integer { + const theDivisor = Integer.fromValue(divisor) + return this.subtract(this.div(theDivisor).multiply(theDivisor)) + } + + /** + * Returns the bitwise NOT of this Integer. + * @returns {!Integer} + * @expose + */ + not (): Integer { + return Integer.fromBits(~this.low, ~this.high) + } + + /** + * Returns the bitwise AND of this Integer and the specified. + * @param {!Integer|number|string} other Other Integer + * @returns {!Integer} + * @expose + */ + and (other: Integerable): Integer { + const theOther = Integer.fromValue(other) + return Integer.fromBits(this.low & theOther.low, this.high & theOther.high) + } + + /** + * Returns the bitwise OR of this Integer and the specified. + * @param {!Integer|number|string} other Other Integer + * @returns {!Integer} + * @expose + */ + or (other: Integerable): Integer { + const theOther = Integer.fromValue(other) + return Integer.fromBits(this.low | theOther.low, this.high | theOther.high) + } + + /** + * Returns the bitwise XOR of this Integer and the given one. + * @param {!Integer|number|string} other Other Integer + * @returns {!Integer} + * @expose + */ + xor (other: Integerable): Integer { + const theOther = Integer.fromValue(other) + return Integer.fromBits(this.low ^ theOther.low, this.high ^ theOther.high) + } + + /** + * Returns this Integer with bits shifted to the left by the given amount. + * @param {number|!Integer} numBits Number of bits + * @returns {!Integer} Shifted Integer + * @expose + */ + shiftLeft (numBits: number | Integer): Integer { + let bitsCount = Integer.toNumber(numBits) + if ((bitsCount &= 63) === 0) { + return Integer.ZERO + } else if (bitsCount < 32) { + return Integer.fromBits( + this.low << bitsCount, + (this.high << bitsCount) | (this.low >>> (32 - bitsCount)) + ) + } else { + return Integer.fromBits(0, this.low << (bitsCount - 32)) + } + } + + /** + * Returns this Integer with bits arithmetically shifted to the right by the given amount. + * @param {number|!Integer} numBits Number of bits + * @returns {!Integer} Shifted Integer + * @expose + */ + shiftRight (numBits: number | Integer): Integer { + let bitsCount: number = Integer.toNumber(numBits) + + if ((bitsCount &= 63) === 0) { + return Integer.ZERO + } else if (numBits < 32) { + return Integer.fromBits( + (this.low >>> bitsCount) | (this.high << (32 - bitsCount)), + this.high >> bitsCount + ) + } else { + return Integer.fromBits( + this.high >> (bitsCount - 32), + this.high >= 0 ? 0 : -1 + ) + } + } + + /** + * Signed zero. + * @type {!Integer} + * @expose + */ + static ZERO: Integer = Integer.fromInt(0) + + /** + * Signed one. + * @type {!Integer} + * @expose + */ + static ONE: Integer = Integer.fromInt(1) + + /** + * Signed negative one. + * @type {!Integer} + * @expose + */ + static NEG_ONE: Integer = Integer.fromInt(-1) + + /** + * Maximum signed value. + * @type {!Integer} + * @expose + */ + static MAX_VALUE: Integer = Integer.fromBits(0xffffffff | 0, 0x7fffffff | 0) + + /** + * Minimum signed value. + * @type {!Integer} + * @expose + */ + static MIN_VALUE: Integer = Integer.fromBits(0, 0x80000000 | 0) + + /** + * Minimum safe value. + * @type {!Integer} + * @expose + */ + static MIN_SAFE_VALUE: Integer = Integer.fromBits( + 0x1 | 0, + 0xffffffffffe00000 | 0 + ) + + /** + * Maximum safe value. + * @type {!Integer} + * @expose + */ + static MAX_SAFE_VALUE: Integer = Integer.fromBits( + 0xffffffff | 0, + 0x1fffff | 0 + ) + + /** + * An indicator used to reliably determine if an object is a Integer or not. + * @type {boolean} + * @const + * @expose + * @private + */ + static __isInteger__: boolean = true + + /** + * Tests if the specified object is a Integer. + * @access private + * @param {*} obj Object + * @returns {boolean} + * @expose + */ + static isInteger (obj: any): obj is Integer { + return obj?.__isInteger__ === true + } + + /** + * Returns a Integer representing the given 32 bit integer value. + * @access private + * @param {number} value The 32 bit integer in question + * @returns {!Integer} The corresponding Integer value + * @expose + */ + static fromInt (value: number): Integer { + let cachedObj + value = value | 0 + if (value >= -128 && value < 128) { + cachedObj = INT_CACHE.get(value) + if (cachedObj != null) { + return cachedObj + } + } + const obj = new Integer(value, value < 0 ? -1 : 0) + if (value >= -128 && value < 128) { + INT_CACHE.set(value, obj) + } + return obj + } + + /** + * Returns a Integer representing the 64 bit integer that comes by concatenating the given low and high bits. Each is + * assumed to use 32 bits. + * @access private + * @param {number} lowBits The low 32 bits + * @param {number} highBits The high 32 bits + * @returns {!Integer} The corresponding Integer value + * @expose + */ + static fromBits (lowBits: number, highBits: number): Integer { + return new Integer(lowBits, highBits) + } + + /** + * Returns a Integer representing the given value, provided that it is a finite number. Otherwise, zero is returned. + * @access private + * @param {number} value The number in question + * @returns {!Integer} The corresponding Integer value + * @expose + */ + static fromNumber (value: number): Integer { + if (isNaN(value) || !isFinite(value)) { + return Integer.ZERO + } + if (value <= -TWO_PWR_63_DBL) { + return Integer.MIN_VALUE + } + if (value + 1 >= TWO_PWR_63_DBL) { + return Integer.MAX_VALUE + } + if (value < 0) { + return Integer.fromNumber(-value).negate() + } + return new Integer(value % TWO_PWR_32_DBL | 0, (value / TWO_PWR_32_DBL) | 0) + } + + /** + * Returns a Integer representation of the given string, written using the specified radix. + * @access private + * @param {string} str The textual representation of the Integer + * @param {number=} radix The radix in which the text is written (2-36), defaults to 10 + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @returns {!Integer} The corresponding Integer value + * @expose + */ + static fromString (str: string, radix?: number, { strictStringValidation }: { strictStringValidation?: boolean} = {}): Integer { + if (str.length === 0) { + throw newError('number format error: empty string') + } + if ( + str === 'NaN' || + str === 'Infinity' || + str === '+Infinity' || + str === '-Infinity' + ) { + return Integer.ZERO + } + radix = radix ?? 10 + if (radix < 2 || radix > 36) { + throw newError('radix out of range: ' + radix.toString()) + } + + let p: number + if ((p = str.indexOf('-')) > 0) { + throw newError('number format error: interior "-" character: ' + str) + } else if (p === 0) { + return Integer.fromString(str.substring(1), radix).negate() + } + + // Do several (8) digits each time through the loop, so as to + // minimize the calls to the very expensive emulated div. + const radixToPower = Integer.fromNumber(Math.pow(radix, 8)) + + let result = Integer.ZERO + for (let i = 0; i < str.length; i += 8) { + const size = Math.min(8, str.length - i) + const valueString = str.substring(i, i + size) + const value = parseInt(valueString, radix) + + if (strictStringValidation === true && !_isValidNumberFromString(valueString, value, radix)) { + throw newError(`number format error: "${valueString}" is NaN in radix ${radix}: ${str}`) + } + + if (size < 8) { + const power = Integer.fromNumber(Math.pow(radix, size)) + result = result.multiply(power).add(Integer.fromNumber(value)) + } else { + result = result.multiply(radixToPower) + result = result.add(Integer.fromNumber(value)) + } + } + return result + } + + /** + * Converts the specified value to a Integer. + * @access private + * @param {!Integer|number|string|bigint|!{low: number, high: number}} val Value + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @returns {!Integer} + * @expose + */ + static fromValue (val: Integerable, opts: { strictStringValidation?: boolean} = {}): Integer { + if (val /* is compatible */ instanceof Integer) { + return val + } + if (typeof val === 'number') { + return Integer.fromNumber(val) + } + if (typeof val === 'string') { + return Integer.fromString(val, undefined, opts) + } + if (typeof val === 'bigint') { + return Integer.fromString(val.toString()) + } + // Throws for non-objects, converts non-instanceof Integer: + return new Integer(val.low, val.high) + } + + /** + * Converts the specified value to a number. + * @access private + * @param {!Integer|number|string|!{low: number, high: number}} val Value + * @returns {number} + * @expose + */ + static toNumber (val: Integerable): number { + switch (typeof val) { + case 'number': + return val + case 'bigint': + return Number(val) + default: + return Integer.fromValue(val).toNumber() + } + } + + /** + * Converts the specified value to a string. + * @access private + * @param {!Integer|number|string|!{low: number, high: number}} val Value + * @param {number} radix optional radix for string conversion, defaults to 10 + * @returns {string} + * @expose + */ + static toString (val: Integerable, radix?: number): string { + return Integer.fromValue(val).toString(radix) + } + + /** + * Checks if the given value is in the safe range in order to be converted to a native number + * @access private + * @param {!Integer|number|string|!{low: number, high: number}} val Value + * @param {number} radix optional radix for string conversion, defaults to 10 + * @returns {boolean} + * @expose + */ + static inSafeRange (val: Integerable): boolean { + return Integer.fromValue(val).inSafeRange() + } +} + +/** + * @private + * @param num + * @param radix + * @param minSize + * @returns {string} + */ +function _convertNumberToString (num: number, radix: number, minSize: number): string { + const theNumberString = num.toString(radix) + const paddingLength = Math.max(minSize - theNumberString.length, 0) + const padding = '0'.repeat(paddingLength) + return `${padding}${theNumberString}` +} + +/** + * + * @private + * @param theString + * @param theNumber + * @param radix + * @return {boolean} True if valid + */ +function _isValidNumberFromString (theString: string, theNumber: number, radix: number): boolean { + return !Number.isNaN(theString) && + !Number.isNaN(theNumber) && + _convertNumberToString(theNumber, radix, theString.length) === theString.toLowerCase() +} + +type Integerable = + | number + | string + | Integer + | { low: number, high: number } + | bigint + +Object.defineProperty(Integer.prototype, '__isInteger__', { + value: true, + enumerable: false, + configurable: false +}) + +/** + * @type {number} + * @const + * @inner + * @private + */ +const TWO_PWR_16_DBL = 1 << 16 + +/** + * @type {number} + * @const + * @inner + * @private + */ +const TWO_PWR_24_DBL = 1 << 24 + +/** + * @type {number} + * @const + * @inner + * @private + */ +const TWO_PWR_32_DBL = TWO_PWR_16_DBL * TWO_PWR_16_DBL + +/** + * @type {number} + * @const + * @inner + * @private + */ +const TWO_PWR_64_DBL = TWO_PWR_32_DBL * TWO_PWR_32_DBL + +/** + * @type {number} + * @const + * @inner + * @private + */ +const TWO_PWR_63_DBL = TWO_PWR_64_DBL / 2 + +/** + * @type {!Integer} + * @const + * @inner + * @private + */ +const TWO_PWR_24 = Integer.fromInt(TWO_PWR_24_DBL) + +/** + * Cast value to Integer type. + * @access public + * @param {Mixed} value - The value to use. + * @param {Object} [opts={}] Configuration options + * @param {boolean} [opts.strictStringValidation=false] Enable strict validation generated Integer. + * @return {Integer} - An object of type Integer. + */ +const int = Integer.fromValue + +/** + * Check if a variable is of Integer type. + * @access public + * @param {Mixed} value - The variable to check. + * @return {Boolean} - Is it of the Integer type? + */ +const isInt = Integer.isInteger + +/** + * Check if a variable can be safely converted to a number + * @access public + * @param {Mixed} value - The variable to check + * @return {Boolean} - true if it is safe to call toNumber on variable otherwise false + */ +const inSafeRange = Integer.inSafeRange + +/** + * Converts a variable to a number + * @access public + * @param {Mixed} value - The variable to convert + * @return {number} - the variable as a number + */ +const toNumber = Integer.toNumber + +/** + * Converts the integer to a string representation + * @access public + * @param {Mixed} value - The variable to convert + * @param {number} radix - radix to use in string conversion, defaults to 10 + * @return {string} - returns a string representation of the integer + */ +const toString = Integer.toString + +export { int, isInt, inSafeRange, toNumber, toString } + +export default Integer diff --git a/packages/neo4j-driver-deno/lib/core/internal/bookmarks.ts b/packages/neo4j-driver-deno/lib/core/internal/bookmarks.ts new file mode 100644 index 000000000..3afa070d8 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/bookmarks.ts @@ -0,0 +1,135 @@ +/** + * 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 util from './util.ts' + +const BOOKMARKS_KEY = 'bookmarks' + +export class Bookmarks { + private readonly _values: string[] + + /** + * @constructor + * @param {string|string[]} values single bookmark as string or multiple bookmarks as a string array. + */ + constructor (values?: string | string[] | null) { + this._values = asStringArray(values) + } + + static empty (): Bookmarks { + return EMPTY_BOOKMARK + } + + /** + * Check if the given Bookmarks holder is meaningful and can be send to the database. + * @return {boolean} returns `true` bookmarks has a value, `false` otherwise. + */ + isEmpty (): boolean { + return this._values.length === 0 + } + + /** + * Get all bookmarks values as an array. + * @return {string[]} all values. + */ + values (): string[] { + return this._values + } + + [Symbol.iterator] (): IterableIterator { + return this._values[Symbol.iterator]() + } + + /** + * Get these bookmarks as an object for begin transaction call. + * @return {Object} the value of this bookmarks holder as object. + */ + asBeginTransactionParameters (): { [BOOKMARKS_KEY]?: string[] } { + if (this.isEmpty()) { + return {} + } + + // Driver sends {bookmarks: "max", bookmarks: ["one", "two", "max"]} instead of simple + // {bookmarks: ["one", "two", "max"]} for backwards compatibility reasons. Old servers can only accept single + // bookmarks that is why driver has to parse and compare given list of bookmarks. This functionality will + // eventually be removed. + return { + [BOOKMARKS_KEY]: this._values + } + } +} + +const EMPTY_BOOKMARK = new Bookmarks(null) + +/** + * Converts given value to an array. + * @param {string|string[]|Array} [value=undefined] argument to convert. + * @return {string[]} value converted to an array. + */ +function asStringArray ( + value?: string | string[] | null +): string[] { + if (value == null || value === '') { + return [] + } + + if (util.isString(value)) { + return [value] as string[] + } + + if (Array.isArray(value)) { + const result = new Set() + const flattenedValue = flattenArray(value) + for (let i = 0; i < flattenedValue.length; i++) { + const element = flattenedValue[i] + // if it is undefined or null, ignore it + if (element !== undefined && element !== null) { + if (!util.isString(element)) { + throw new TypeError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Bookmark value should be a string, given: '${element}'` + ) + } + result.add(element) + } + } + return [...result] + } + + throw new TypeError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Bookmarks should either be a string or a string array, given: '${value}'` + ) +} + +/** + * Recursively flattens an array so that the result becomes a single array + * of values, which does not include any sub-arrays + * + * @param {Array} value + */ +function flattenArray (values: any[]): string[] { + return values.reduce( + (dest, value) => + Array.isArray(value) + ? dest.concat(flattenArray(value)) + : dest.concat(value), + [] + ) +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts new file mode 100644 index 000000000..d67044100 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts @@ -0,0 +1,331 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ + +import { newError } from '../error.ts' +import { assertString } from './util.ts' +import Connection from '../connection.ts' +import { ACCESS_MODE_WRITE } from './constants.ts' +import { Bookmarks } from './bookmarks.ts' +import ConnectionProvider from '../connection-provider.ts' + +/** + * @private + */ +interface ConnectionHolderInterface { + /** + * Returns the assigned access mode. + * @returns {string} access mode + */ + mode: () => string | undefined + + /** + * Returns the target database name + * @returns {string} the database name + */ + database: () => string | undefined + + /** + * Returns the bookmarks + */ + bookmarks: () => Bookmarks + + /** + * Make this holder initialize new connection if none exists already. + * @return {boolean} + */ + initializeConnection: () => boolean + + /** + * Get the current connection promise. + * @return {Promise} promise resolved with the current connection. + */ + getConnection: () => Promise + + /** + * Notify this holder that single party does not require current connection any more. + * @return {Promise} promise resolved with the current connection, never a rejected promise. + */ + releaseConnection: () => Promise + + /** + * Closes this holder and releases current connection (if any) despite any existing users. + * @return {Promise} promise resolved when current connection is released to the pool. + */ + close: () => Promise +} + +/** + * Utility to lazily initialize connections and return them back to the pool when unused. + * @private + */ +class ConnectionHolder implements ConnectionHolderInterface { + private readonly _mode: string + private _database?: string + private readonly _bookmarks: Bookmarks + private readonly _connectionProvider?: ConnectionProvider + private _referenceCount: number + private _connectionPromise: Promise + private readonly _impersonatedUser?: string + private readonly _getConnectionAcquistionBookmarks: () => Promise + private readonly _onDatabaseNameResolved?: (databaseName?: string) => void + + /** + * @constructor + * @param {object} params + * @property {string} params.mode - the access mode for new connection holder. + * @property {string} params.database - the target database name. + * @property {Bookmarks} params.bookmarks - initial bookmarks + * @property {ConnectionProvider} params.connectionProvider - the connection provider to acquire connections from. + * @property {string?} params.impersonatedUser - the user which will be impersonated + * @property {function(databaseName:string)} params.onDatabaseNameResolved - callback called when the database name is resolved + * @property {function():Promise} params.getConnectionAcquistionBookmarks - called for getting Bookmarks for acquiring connections + */ + constructor ({ + mode = ACCESS_MODE_WRITE, + database = '', + bookmarks, + connectionProvider, + impersonatedUser, + onDatabaseNameResolved, + getConnectionAcquistionBookmarks + }: { + mode?: string + database?: string + bookmarks?: Bookmarks + connectionProvider?: ConnectionProvider + impersonatedUser?: string + onDatabaseNameResolved?: (databaseName?: string) => void + getConnectionAcquistionBookmarks?: () => Promise + } = {}) { + this._mode = mode + this._database = database != null ? assertString(database, 'database') : '' + this._bookmarks = bookmarks ?? Bookmarks.empty() + this._connectionProvider = connectionProvider + this._impersonatedUser = impersonatedUser + this._referenceCount = 0 + this._connectionPromise = Promise.resolve(null) + this._onDatabaseNameResolved = onDatabaseNameResolved + this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) + } + + mode (): string | undefined { + return this._mode + } + + database (): string | undefined { + return this._database + } + + setDatabase (database?: string): void { + this._database = database + } + + bookmarks (): Bookmarks { + return this._bookmarks + } + + connectionProvider (): ConnectionProvider | undefined { + return this._connectionProvider + } + + referenceCount (): number { + return this._referenceCount + } + + initializeConnection (): boolean { + if (this._referenceCount === 0 && (this._connectionProvider != null)) { + this._connectionPromise = this._createConnectionPromise(this._connectionProvider) + } else { + this._referenceCount++ + return false + } + this._referenceCount++ + return true + } + + private async _createConnectionPromise (connectionProvider: ConnectionProvider): Promise { + return await connectionProvider.acquireConnection({ + accessMode: this._mode, + database: this._database, + bookmarks: await this._getBookmarks(), + impersonatedUser: this._impersonatedUser, + onDatabaseNameResolved: this._onDatabaseNameResolved + }) + } + + private async _getBookmarks (): Promise { + return await this._getConnectionAcquistionBookmarks() + } + + getConnection (): Promise { + return this._connectionPromise + } + + releaseConnection (): Promise { + if (this._referenceCount === 0) { + return this._connectionPromise + } + + this._referenceCount-- + + if (this._referenceCount === 0) { + return this._releaseConnection() + } + return this._connectionPromise + } + + close (hasTx?: boolean): Promise { + if (this._referenceCount === 0) { + return this._connectionPromise + } + this._referenceCount = 0 + return this._releaseConnection(hasTx) + } + + /** + * Return the current pooled connection instance to the connection pool. + * We don't pool Session instances, to avoid users using the Session after they've called close. + * The `Session` object is just a thin wrapper around Connection anyway, so it makes little difference. + * @return {Promise} - promise resolved then connection is returned to the pool. + * @private + */ + private _releaseConnection (hasTx?: boolean): Promise { + this._connectionPromise = this._connectionPromise + .then((connection?: Connection|null) => { + if (connection != null) { + if (connection.isOpen() && (connection.hasOngoingObservableRequests() || hasTx === true)) { + return connection + .resetAndFlush() + .catch(ignoreError) + .then(() => connection._release().then(() => null)) + } + return connection._release().then(() => null) + } else { + return Promise.resolve(null) + } + }) + .catch(ignoreError) + + return this._connectionPromise + } +} + +/** + * Provides a interaction with a ConnectionHolder without change it state by + * releasing or initilizing + */ +export default class ReadOnlyConnectionHolder extends ConnectionHolder { + private readonly _connectionHolder: ConnectionHolder + + /** + * Contructor + * @param {ConnectionHolder} connectionHolder the connection holder which will treat the requests + */ + constructor (connectionHolder: ConnectionHolder) { + super({ + mode: connectionHolder.mode(), + database: connectionHolder.database(), + bookmarks: connectionHolder.bookmarks(), + // @ts-expect-error + getConnectionAcquistionBookmarks: connectionHolder._getConnectionAcquistionBookmarks, + connectionProvider: connectionHolder.connectionProvider() + }) + this._connectionHolder = connectionHolder + } + + /** + * Return the true if the connection is suppose to be initilized with the command. + * + * @return {boolean} + */ + initializeConnection (): boolean { + if (this._connectionHolder.referenceCount() === 0) { + return false + } + return true + } + + /** + * Get the current connection promise. + * @return {Promise} promise resolved with the current connection. + */ + getConnection (): Promise { + return this._connectionHolder.getConnection() + } + + /** + * Get the current connection promise, doesn't performs the release + * @return {Promise} promise with the resolved current connection + */ + releaseConnection (): Promise { + return this._connectionHolder.getConnection().catch(() => Promise.resolve(null)) + } + + /** + * Get the current connection promise, doesn't performs the connection close + * @return {Promise} promise with the resolved current connection + */ + close (): Promise { + return this._connectionHolder.getConnection().catch(() => Promise.resolve(null)) + } +} + +class EmptyConnectionHolder extends ConnectionHolder { + mode (): undefined { + return undefined + } + + database (): undefined { + return undefined + } + + initializeConnection (): boolean { + // nothing to initialize + return true + } + + async getConnection (): Promise { + return await Promise.reject( + newError('This connection holder does not serve connections') + ) + } + + async releaseConnection (): Promise { + return await Promise.resolve(null) + } + + async close (): Promise { + return await Promise.resolve(null) + } +} + +/** + * Connection holder that does not manage any connections. + * @type {ConnectionHolder} + * @private + */ +const EMPTY_CONNECTION_HOLDER: EmptyConnectionHolder = new EmptyConnectionHolder() + +// eslint-disable-next-line node/handle-callback-err +function ignoreError (error: Error): null { + return null +} + +export { ConnectionHolder, ReadOnlyConnectionHolder, EMPTY_CONNECTION_HOLDER } diff --git a/packages/neo4j-driver-deno/lib/core/internal/constants.ts b/packages/neo4j-driver-deno/lib/core/internal/constants.ts new file mode 100644 index 000000000..39e790f2f --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/constants.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const FETCH_ALL = -1 +const DEFAULT_POOL_ACQUISITION_TIMEOUT = 60 * 1000 // 60 seconds +const DEFAULT_POOL_MAX_SIZE = 100 +const DEFAULT_CONNECTION_TIMEOUT_MILLIS = 30000 // 30 seconds by default + +const ACCESS_MODE_READ: 'READ' = 'READ' +const ACCESS_MODE_WRITE: 'WRITE' = 'WRITE' + +const BOLT_PROTOCOL_V1: number = 1 +const BOLT_PROTOCOL_V2: number = 2 +const BOLT_PROTOCOL_V3: number = 3 +const BOLT_PROTOCOL_V4_0: number = 4.0 +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, + ACCESS_MODE_READ, + ACCESS_MODE_WRITE, + DEFAULT_CONNECTION_TIMEOUT_MILLIS, + DEFAULT_POOL_ACQUISITION_TIMEOUT, + DEFAULT_POOL_MAX_SIZE, + BOLT_PROTOCOL_V1, + BOLT_PROTOCOL_V2, + BOLT_PROTOCOL_V3, + BOLT_PROTOCOL_V4_0, + BOLT_PROTOCOL_V4_1, + BOLT_PROTOCOL_V4_2, + BOLT_PROTOCOL_V4_3, + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_0 +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/index.ts b/packages/neo4j-driver-deno/lib/core/internal/index.ts new file mode 100644 index 000000000..a618369ad --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/index.ts @@ -0,0 +1,48 @@ +/** + * 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 util from './util.ts' +import * as temporalUtil from './temporal-util.ts' +import * as observer from './observers.ts' +import * as bookmarks from './bookmarks.ts' +import * as constants from './constants.ts' +import * as connectionHolder from './connection-holder.ts' +import * as txConfig from './tx-config.ts' +import * as transactionExecutor from './transaction-executor.ts' +import * as logger from './logger.ts' +import * as urlUtil from './url-util.ts' +import * as serverAddress from './server-address.ts' +import * as resolver from './resolver/index.ts' +import * as objectUtil from './object-util.ts' + +export { + util, + temporalUtil, + observer, + bookmarks, + constants, + connectionHolder, + txConfig, + transactionExecutor, + logger, + urlUtil, + serverAddress, + resolver, + objectUtil +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/logger.ts b/packages/neo4j-driver-deno/lib/core/internal/logger.ts new file mode 100644 index 000000000..fee29714b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/logger.ts @@ -0,0 +1,224 @@ +/** + * 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 { newError } from '../error.ts' +import { LogLevel, LoggerFunction, LoggingConfig } from '../types.ts' + +const ERROR: 'error' = 'error' +const WARN: 'warn' = 'warn' +const INFO: 'info' = 'info' +const DEBUG: 'debug' = 'debug' + +const DEFAULT_LEVEL = INFO + +const levels = { + [ERROR]: 0, + [WARN]: 1, + [INFO]: 2, + [DEBUG]: 3 +} + +/** + * Logger used by the driver to notify about various internal events. Single logger should be used per driver. + */ +export class Logger { + private readonly _level: LogLevel + private readonly _loggerFunction: LoggerFunction + /** + * @constructor + * @param {string} level the enabled logging level. + * @param {function(level: string, message: string)} loggerFunction the function to write the log level and message. + */ + constructor (level: LogLevel, loggerFunction: LoggerFunction) { + this._level = level + this._loggerFunction = loggerFunction + } + + /** + * Create a new logger based on the given driver configuration. + * @param {Object} driverConfig the driver configuration as supplied by the user. + * @return {Logger} a new logger instance or a no-op logger when not configured. + */ + static create (driverConfig: { logging?: LoggingConfig }): Logger { + if (driverConfig?.logging != null) { + const loggingConfig = driverConfig.logging + const level = extractConfiguredLevel(loggingConfig) + const loggerFunction = extractConfiguredLogger(loggingConfig) + return new Logger(level, loggerFunction) + } + return this.noOp() + } + + /** + * Create a no-op logger implementation. + * @return {Logger} the no-op logger implementation. + */ + static noOp (): Logger { + return noOpLogger + } + + /** + * Check if error logging is enabled, i.e. it is not a no-op implementation. + * @return {boolean} `true` when enabled, `false` otherwise. + */ + isErrorEnabled (): boolean { + return isLevelEnabled(this._level, ERROR) + } + + /** + * Log an error message. + * @param {string} message the message to log. + */ + error (message: string): void { + if (this.isErrorEnabled()) { + this._loggerFunction(ERROR, message) + } + } + + /** + * Check if warn logging is enabled, i.e. it is not a no-op implementation. + * @return {boolean} `true` when enabled, `false` otherwise. + */ + isWarnEnabled (): boolean { + return isLevelEnabled(this._level, WARN) + } + + /** + * Log an warning message. + * @param {string} message the message to log. + */ + warn (message: string): void { + if (this.isWarnEnabled()) { + this._loggerFunction(WARN, message) + } + } + + /** + * Check if info logging is enabled, i.e. it is not a no-op implementation. + * @return {boolean} `true` when enabled, `false` otherwise. + */ + isInfoEnabled (): boolean { + return isLevelEnabled(this._level, INFO) + } + + /** + * Log an info message. + * @param {string} message the message to log. + */ + info (message: string): void { + if (this.isInfoEnabled()) { + this._loggerFunction(INFO, message) + } + } + + /** + * Check if debug logging is enabled, i.e. it is not a no-op implementation. + * @return {boolean} `true` when enabled, `false` otherwise. + */ + isDebugEnabled (): boolean { + return isLevelEnabled(this._level, DEBUG) + } + + /** + * Log a debug message. + * @param {string} message the message to log. + */ + debug (message: string): void { + if (this.isDebugEnabled()) { + this._loggerFunction(DEBUG, message) + } + } +} + +class NoOpLogger extends Logger { + constructor () { + super(INFO, (level: LogLevel, message: string) => {}) + } + + isErrorEnabled (): boolean { + return false + } + + error (message: string): void {} + + isWarnEnabled (): boolean { + return false + } + + warn (message: string): void {} + + isInfoEnabled (): boolean { + return false + } + + info (message: string): void {} + + isDebugEnabled (): boolean { + return false + } + + debug (message: string): void {} +} + +const noOpLogger = new NoOpLogger() + +/** + * Check if the given logging level is enabled. + * @param {string} configuredLevel the configured level. + * @param {string} targetLevel the level to check. + * @return {boolean} value of `true` when enabled, `false` otherwise. + */ +function isLevelEnabled (configuredLevel: LogLevel, targetLevel: LogLevel): boolean { + return levels[configuredLevel] >= levels[targetLevel] +} + +/** + * Extract the configured logging level from the driver's logging configuration. + * @param {Object} loggingConfig the logging configuration. + * @return {string} the configured log level or default when none configured. + */ +function extractConfiguredLevel (loggingConfig: LoggingConfig): LogLevel { + if (loggingConfig?.level != null) { + const configuredLevel = loggingConfig.level + const value = levels[configuredLevel] + if (value == null && value !== 0) { + throw newError( + `Illegal logging level: ${configuredLevel}. Supported levels are: ${Object.keys( + levels + ).toString()}` + ) + } + return configuredLevel + } + return DEFAULT_LEVEL +} + +/** + * Extract the configured logger function from the driver's logging configuration. + * @param {Object} loggingConfig the logging configuration. + * @return {function(level: string, message: string)} the configured logging function. + */ +function extractConfiguredLogger (loggingConfig: LoggingConfig): LoggerFunction { + if (loggingConfig?.logger != null) { + const configuredLogger = loggingConfig.logger + if (configuredLogger != null && typeof configuredLogger === 'function') { + return configuredLogger + } + } + throw newError(`Illegal logger function: ${loggingConfig?.logger?.toString() ?? 'undefined'}`) +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/object-util.ts b/packages/neo4j-driver-deno/lib/core/internal/object-util.ts new file mode 100644 index 000000000..77016038c --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/object-util.ts @@ -0,0 +1,87 @@ +/** + * 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. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +const __isBrokenObject__ = '__isBrokenObject__' +// eslint-disable-next-line @typescript-eslint/naming-convention +const __reason__ = '__reason__' + +/** + * Creates a object which all method call will throw the given error + * + * @param {Error} error The error + * @param {any} object The object. Default: {} + * @returns {any} A broken object + */ +function createBrokenObject (error: Error, object: any = {}): T { + const fail: () => T = () => { + throw error + } + + return new Proxy(object, { + get: (_: T, p: string | Symbol): any => { + if (p === __isBrokenObject__) { + return true + } else if (p === __reason__) { + return error + } else if (p === 'toJSON') { + return undefined + } + fail() + }, + set: fail, + apply: fail, + construct: fail, + defineProperty: fail, + deleteProperty: fail, + getOwnPropertyDescriptor: fail, + getPrototypeOf: fail, + has: fail, + isExtensible: fail, + ownKeys: fail, + preventExtensions: fail, + setPrototypeOf: fail + }) +} + +/** + * Verifies if it is a Broken Object + * @param {any} object The object + * @returns {boolean} If it was created with createBrokenObject + */ +function isBrokenObject (object: any): boolean { + return object !== null && typeof object === 'object' && object[__isBrokenObject__] === true +} + +/** + * Returns if the reason the object is broken. + * + * This method should only be called with instances create with {@link createBrokenObject} + * + * @param {any} object The object + * @returns {Error} The reason the object is broken + */ +function getBrokenObjectReason (object: any): Error { + return object[__reason__] +} + +export { + createBrokenObject, + isBrokenObject, + getBrokenObjectReason +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/observers.ts b/packages/neo4j-driver-deno/lib/core/internal/observers.ts new file mode 100644 index 000000000..1b6aea7a5 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/observers.ts @@ -0,0 +1,210 @@ +/** + * 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 Record from '../record.ts' +import ResultSummary from '../result-summary.ts' + +interface StreamObserver { + /** + * Will be called on every record that comes in and transform a raw record + * to a Object. If user-provided observer is present, pass transformed record + * to it's onNext method, otherwise, push to record que. + * @param {Array} rawRecord - An array with the raw record + */ + onNext?: (rawRecord: any[]) => void + /** + * Will be called on errors. + * If user-provided observer is present, pass the error + * to it's onError method, otherwise set instance variable _error. + * @param {Object} error - An error object + */ + onError: (error: Error) => void + onCompleted?: (meta: any) => void +} + +/** + * Interface to observe updates on the Result which is being produced. + * + */ +interface ResultObserver { + /** + * Receive the keys present on the record whenever this information is available + * + * @param {string[]} keys The keys present on the {@link Record} + */ + onKeys?: (keys: string[]) => void + + /** + * Receive the each record present on the {@link @Result} + * @param {Record} record The {@link Record} produced + */ + onNext?: (record: Record) => void + + /** + * Called when the result is fully received + * @param {ResultSummary| any} summary The result summary + */ + onCompleted?: (summary: ResultSummary | any) => void + + /** + * Called when some error occurs during the result proccess or query execution + * @param {Error} error The error ocurred + */ + onError?: (error: Error) => void +} + +/** + * Raw observer for the stream + */ +export interface ResultStreamObserver extends StreamObserver { + /** + * Cancel pending record stream + */ + cancel: () => void + + /** + * Pause the record consuming + * + * This function will supend the record consuming. It will not cancel the stream and the already + * requested records will be sent to the subscriber. + */ + pause: () => void + + /** + * Resume the record consuming + * + * This function will resume the record consuming fetching more records from the server. + */ + resume: () => void + + /** + * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. + * Response for RUN initializes query keys. Response for PULL_ALL / DISCARD_ALL exposes the result stream. + * + * However, some operations can be represented as a single message which receives full metadata in a single response. + * For example, operations to begin, commit and rollback an explicit transaction use two messages in Bolt V1 but a single message in Bolt V3. + * Messages are `RUN "BEGIN" {}` + `PULL_ALL` in Bolt V1 and `BEGIN` in Bolt V3. + * + * This function prepares the observer to only handle a single response message. + */ + prepareToHandleSingleResponse: () => void + + /** + * Mark this observer as if it has completed with no metadata. + */ + markCompleted: () => void + + /** + * Subscribe to events with provided observer. + * @param {Object} observer - Observer object + * @param {function(keys: String[])} observer.onKeys - Handle stream header, field keys. + * @param {function(record: Object)} observer.onNext - Handle records, one by one. + * @param {function(metadata: Object)} observer.onCompleted - Handle stream tail, the metadata. + * @param {function(error: Object)} observer.onError - Handle errors, should always be provided. + */ + subscribe: (observer: ResultObserver) => void +} + +export class CompletedObserver implements ResultStreamObserver { + subscribe (observer: ResultObserver): void { + apply(observer, observer.onKeys, []) + apply(observer, observer.onCompleted, {}) + } + + cancel (): void { + // do nothing + } + + pause (): void { + // do nothing + } + + resume (): void { + // do nothing + } + + prepareToHandleSingleResponse (): void { + // do nothing + } + + markCompleted (): void { + // do nothing + } + + // eslint-disable-next-line node/handle-callback-err + onError (error: Error): void { + // nothing to do, already finished + throw Error('CompletedObserver not supposed to call onError') + } +} + +export class FailedObserver implements ResultStreamObserver { + private readonly _error: Error + private readonly _beforeError?: (error: Error) => void + private readonly _observers: ResultObserver[] + + constructor ({ + error, + onError + }: { + error: Error + onError?: (error: Error) => void | Promise + }) { + this._error = error + this._beforeError = onError + this._observers = [] + this.onError(error) + } + + subscribe (observer: ResultObserver): void { + apply(observer, observer.onError, this._error) + this._observers.push(observer) + } + + onError (error: Error): void { + apply(this, this._beforeError, error) + this._observers.forEach(o => apply(o, o.onError, error)) + } + + cancel (): void { + // do nothing + } + + pause (): void { + // do nothing + } + + resume (): void { + // do nothing + } + + markCompleted (): void { + // do nothing + } + + prepareToHandleSingleResponse (): void { + // do nothing + } +} + +function apply (thisArg: any, func?: (param: T) => void, param?: T): void { + if (func != null) { + func.bind(thisArg)(param as any) + } +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/resolver/base-host-name-resolver.ts b/packages/neo4j-driver-deno/lib/core/internal/resolver/base-host-name-resolver.ts new file mode 100644 index 000000000..4dcdf5ea9 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/resolver/base-host-name-resolver.ts @@ -0,0 +1,34 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ + +import { ServerAddress } from '../server-address.ts' + +export default class BaseHostNameResolver { + resolve (): Promise { + throw new Error('Abstract function') + } + + /** + * @protected + */ + _resolveToItself (address: ServerAddress): Promise { + return Promise.resolve([address]) + } +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/resolver/configured-custom-resolver.ts b/packages/neo4j-driver-deno/lib/core/internal/resolver/configured-custom-resolver.ts new file mode 100644 index 000000000..878096143 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/resolver/configured-custom-resolver.ts @@ -0,0 +1,47 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ +import { ServerAddress } from '../server-address.ts' + +function resolveToSelf (address: ServerAddress): Promise { + return Promise.resolve([address]) +} + +export default class ConfiguredCustomResolver { + private readonly _resolverFunction: (address: string) => string + + constructor (resolverFunction: (address: string) => string) { + this._resolverFunction = resolverFunction ?? resolveToSelf + } + + resolve (seedRouter: ServerAddress): Promise { + return new Promise(resolve => + resolve(this._resolverFunction(seedRouter.asHostPort())) + ).then(resolved => { + if (!Array.isArray(resolved)) { + throw new TypeError( + 'Configured resolver function should either return an array of addresses or a Promise resolved with an array of addresses.' + + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Each address is ':'. Got: ${resolved}` + ) + } + return resolved.map(r => ServerAddress.fromUrl(r)) + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/resolver/index.ts b/packages/neo4j-driver-deno/lib/core/internal/resolver/index.ts new file mode 100644 index 000000000..e60c5ea5b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/resolver/index.ts @@ -0,0 +1,22 @@ +/** + * 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 BaseHostNameResolver from './base-host-name-resolver.ts' +import ConfiguredCustomResolver from './configured-custom-resolver.ts' + +export { BaseHostNameResolver, ConfiguredCustomResolver } diff --git a/packages/neo4j-driver-deno/lib/core/internal/server-address.ts b/packages/neo4j-driver-deno/lib/core/internal/server-address.ts new file mode 100644 index 000000000..89efa84d9 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/server-address.ts @@ -0,0 +1,79 @@ +/** + * 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 { assertNumber, assertString } from './util.ts' +import * as urlUtil from './url-util.ts' + +export class ServerAddress { + private readonly _host: string + private readonly _resolved: string | null + private readonly _port: number + private readonly _hostPort: string + private readonly _stringValue: string + + constructor ( + host: string, + resolved: string | null | undefined, + port: number, + hostPort: string + ) { + this._host = assertString(host, 'host') + this._resolved = resolved != null ? assertString(resolved, 'resolved') : null + this._port = assertNumber(port, 'port') + this._hostPort = hostPort + this._stringValue = resolved != null ? `${hostPort}(${resolved})` : `${hostPort}` + } + + host (): string { + return this._host + } + + resolvedHost (): string { + return this._resolved != null ? this._resolved : this._host + } + + port (): number { + return this._port + } + + resolveWith (resolved: string): ServerAddress { + return new ServerAddress(this._host, resolved, this._port, this._hostPort) + } + + asHostPort (): string { + return this._hostPort + } + + asKey (): string { + return this._hostPort + } + + toString (): string { + return this._stringValue + } + + static fromUrl (url: string): ServerAddress { + const urlParsed = urlUtil.parseDatabaseUrl(url) + return new ServerAddress( + urlParsed.host, + null, + urlParsed.port, + urlParsed.hostAndPort + ) + } +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/temporal-util.ts b/packages/neo4j-driver-deno/lib/core/internal/temporal-util.ts new file mode 100644 index 000000000..8ea58b204 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/temporal-util.ts @@ -0,0 +1,645 @@ +/** + * 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 Integer, { int, isInt } from '../integer.ts' +import { newError } from '../error.ts' +import { assertNumberOrInteger } from './util.ts' +import { NumberOrInteger } from '../graph-types.ts' + +/* + Code in this util should be compatible with code in the database that uses JSR-310 java.time APIs. + + It is based on a library called ThreeTen (https://github.com/ThreeTen/threetenbp) which was derived + from JSR-310 reference implementation previously hosted on GitHub. Code uses `Integer` type everywhere + to correctly handle large integer values that are greater than `Number.MAX_SAFE_INTEGER`. + + Please consult either ThreeTen or js-joda (https://github.com/js-joda/js-joda) when working with the + conversion functions. + */ +class ValueRange { + _minNumber: number + _maxNumber: number + _minInteger: Integer + _maxInteger: Integer + + constructor (min: number, max: number) { + this._minNumber = min + this._maxNumber = max + this._minInteger = int(min) + this._maxInteger = int(max) + } + + contains (value: number | Integer | bigint): boolean { + if (isInt(value) && value instanceof Integer) { + return ( + value.greaterThanOrEqual(this._minInteger) && + value.lessThanOrEqual(this._maxInteger) + ) + } else if (typeof value === 'bigint') { + const intValue = int(value) + return ( + intValue.greaterThanOrEqual(this._minInteger) && + intValue.lessThanOrEqual(this._maxInteger) + ) + } else { + return value >= this._minNumber && value <= this._maxNumber + } + } + + toString (): string { + return `[${this._minNumber}, ${this._maxNumber}]` + } +} + +export const YEAR_RANGE = new ValueRange(-999999999, 999999999) +export const MONTH_OF_YEAR_RANGE = new ValueRange(1, 12) +export const DAY_OF_MONTH_RANGE = new ValueRange(1, 31) +export const HOUR_OF_DAY_RANGE = new ValueRange(0, 23) +export const MINUTE_OF_HOUR_RANGE = new ValueRange(0, 59) +export const SECOND_OF_MINUTE_RANGE = new ValueRange(0, 59) +export const NANOSECOND_OF_SECOND_RANGE = new ValueRange(0, 999999999) + +export const MINUTES_PER_HOUR = 60 +export const SECONDS_PER_MINUTE = 60 +export const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR +export const NANOS_PER_SECOND = 1000000000 +export const NANOS_PER_MILLISECOND = 1000000 +export const NANOS_PER_MINUTE = NANOS_PER_SECOND * SECONDS_PER_MINUTE +export const NANOS_PER_HOUR = NANOS_PER_MINUTE * MINUTES_PER_HOUR +export const DAYS_0000_TO_1970 = 719528 +export const DAYS_PER_400_YEAR_CYCLE = 146097 +export const SECONDS_PER_DAY = 86400 + +export function normalizeSecondsForDuration ( + seconds: number | Integer | bigint, + nanoseconds: number | Integer | bigint +): Integer { + return int(seconds).add(floorDiv(nanoseconds, NANOS_PER_SECOND)) +} + +export function normalizeNanosecondsForDuration ( + nanoseconds: number | Integer | bigint +): Integer { + return floorMod(nanoseconds, NANOS_PER_SECOND) +} + +/** + * Converts given local time into a single integer representing this same time in nanoseconds of the day. + * @param {Integer|number|string} hour the hour of the local time to convert. + * @param {Integer|number|string} minute the minute of the local time to convert. + * @param {Integer|number|string} second the second of the local time to convert. + * @param {Integer|number|string} nanosecond the nanosecond of the local time to convert. + * @return {Integer} nanoseconds representing the given local time. + */ +export function localTimeToNanoOfDay ( + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string +): Integer { + hour = int(hour) + minute = int(minute) + second = int(second) + nanosecond = int(nanosecond) + + let totalNanos = hour.multiply(NANOS_PER_HOUR) + totalNanos = totalNanos.add(minute.multiply(NANOS_PER_MINUTE)) + totalNanos = totalNanos.add(second.multiply(NANOS_PER_SECOND)) + return totalNanos.add(nanosecond) +} + +/** + * Converts given local date time into a single integer representing this same time in epoch seconds UTC. + * @param {Integer|number|string} year the year of the local date-time to convert. + * @param {Integer|number|string} month the month of the local date-time to convert. + * @param {Integer|number|string} day the day of the local date-time to convert. + * @param {Integer|number|string} hour the hour of the local date-time to convert. + * @param {Integer|number|string} minute the minute of the local date-time to convert. + * @param {Integer|number|string} second the second of the local date-time to convert. + * @param {Integer|number|string} nanosecond the nanosecond of the local date-time to convert. + * @return {Integer} epoch second in UTC representing the given local date time. + */ +export function localDateTimeToEpochSecond ( + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string, + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string +): Integer { + const epochDay = dateToEpochDay(year, month, day) + const localTimeSeconds = localTimeToSecondOfDay(hour, minute, second) + return epochDay.multiply(SECONDS_PER_DAY).add(localTimeSeconds) +} + +/** + * Converts given local date into a single integer representing it's epoch day. + * @param {Integer|number|string} year the year of the local date to convert. + * @param {Integer|number|string} month the month of the local date to convert. + * @param {Integer|number|string} day the day of the local date to convert. + * @return {Integer} epoch day representing the given date. + */ +export function dateToEpochDay ( + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string +): Integer { + year = int(year) + month = int(month) + day = int(day) + + let epochDay = year.multiply(365) + + if (year.greaterThanOrEqual(0)) { + epochDay = epochDay.add( + year + .add(3) + .div(4) + .subtract(year.add(99).div(100)) + .add(year.add(399).div(400)) + ) + } else { + epochDay = epochDay.subtract( + year + .div(-4) + .subtract(year.div(-100)) + .add(year.div(-400)) + ) + } + + epochDay = epochDay.add( + month + .multiply(367) + .subtract(362) + .div(12) + ) + epochDay = epochDay.add(day.subtract(1)) + if (month.greaterThan(2)) { + epochDay = epochDay.subtract(1) + if (!isLeapYear(year)) { + epochDay = epochDay.subtract(1) + } + } + return epochDay.subtract(DAYS_0000_TO_1970) +} + +/** + * Format given duration to an ISO 8601 string. + * @param {Integer|number|string} months the number of months. + * @param {Integer|number|string} days the number of days. + * @param {Integer|number|string} seconds the number of seconds. + * @param {Integer|number|string} nanoseconds the number of nanoseconds. + * @return {string} ISO string that represents given duration. + */ +export function durationToIsoString ( + months: NumberOrInteger | string, + days: NumberOrInteger | string, + seconds: NumberOrInteger | string, + nanoseconds: NumberOrInteger | string +): string { + const monthsString = formatNumber(months) + const daysString = formatNumber(days) + const secondsAndNanosecondsString = formatSecondsAndNanosecondsForDuration( + seconds, + nanoseconds + ) + return `P${monthsString}M${daysString}DT${secondsAndNanosecondsString}S` +} + +/** + * Formats given time to an ISO 8601 string. + * @param {Integer|number|string} hour the hour value. + * @param {Integer|number|string} minute the minute value. + * @param {Integer|number|string} second the second value. + * @param {Integer|number|string} nanosecond the nanosecond value. + * @return {string} ISO string that represents given time. + */ +export function timeToIsoString ( + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string, + nanosecond: NumberOrInteger | string +): string { + const hourString = formatNumber(hour, 2) + const minuteString = formatNumber(minute, 2) + const secondString = formatNumber(second, 2) + const nanosecondString = formatNanosecond(nanosecond) + return `${hourString}:${minuteString}:${secondString}${nanosecondString}` +} + +/** + * Formats given time zone offset in seconds to string representation like '±HH:MM', '±HH:MM:SS' or 'Z' for UTC. + * @param {Integer|number|string} offsetSeconds the offset in seconds. + * @return {string} ISO string that represents given offset. + */ +export function timeZoneOffsetToIsoString ( + offsetSeconds: NumberOrInteger | string +): string { + offsetSeconds = int(offsetSeconds) + if (offsetSeconds.equals(0)) { + return 'Z' + } + + const isNegative = offsetSeconds.isNegative() + if (isNegative) { + offsetSeconds = offsetSeconds.multiply(-1) + } + const signPrefix = isNegative ? '-' : '+' + + const hours = formatNumber(offsetSeconds.div(SECONDS_PER_HOUR), 2) + const minutes = formatNumber( + offsetSeconds.div(SECONDS_PER_MINUTE).modulo(MINUTES_PER_HOUR), + 2 + ) + const secondsValue = offsetSeconds.modulo(SECONDS_PER_MINUTE) + const seconds = secondsValue.equals(0) ? null : formatNumber(secondsValue, 2) + + return seconds != null + ? `${signPrefix}${hours}:${minutes}:${seconds}` + : `${signPrefix}${hours}:${minutes}` +} + +/** + * Formats given date to an ISO 8601 string. + * @param {Integer|number|string} year the date year. + * @param {Integer|number|string} month the date month. + * @param {Integer|number|string} day the date day. + * @return {string} ISO string that represents given date. + */ +export function dateToIsoString ( + year: NumberOrInteger | string, + month: NumberOrInteger | string, + day: NumberOrInteger | string +): string { + const yearString = formatYear(year) + const monthString = formatNumber(month, 2) + const dayString = formatNumber(day, 2) + return `${yearString}-${monthString}-${dayString}` +} + +/** + * Convert the given iso date string to a JavaScript Date object + * + * @param {string} isoString The iso date string + * @returns {Date} the date + */ +export function isoStringToStandardDate (isoString: string): Date { + return new Date(isoString) +} + +/** + * Convert the given utc timestamp to a JavaScript Date object + * + * @param {number} utc Timestamp in UTC + * @returns {Date} the date + */ +export function toStandardDate (utc: number): Date { + return new Date(utc) +} + +/** + * Shortcut for creating a new StandardDate + * @param date + * @returns {Date} the standard date + */ +export function newDate (date: string | number | Date): Date { + return new Date(date) +} + +/** + * Get the total number of nanoseconds from the milliseconds of the given standard JavaScript date and optional nanosecond part. + * @param {global.Date} standardDate the standard JavaScript date. + * @param {Integer|number|bigint|undefined} nanoseconds the optional number of nanoseconds. + * @return {Integer|number|bigint} the total amount of nanoseconds. + */ +export function totalNanoseconds ( + standardDate: Date, + nanoseconds?: NumberOrInteger +): NumberOrInteger { + nanoseconds = nanoseconds ?? 0 + const nanosFromMillis = standardDate.getMilliseconds() * NANOS_PER_MILLISECOND + return add(nanoseconds, nanosFromMillis) +} + +/** + * Get the time zone offset in seconds from the given standard JavaScript date. + * + * Implementation note: + * Time zone offset returned by the standard JavaScript date is the difference, in minutes, from local time to UTC. + * So positive value means offset is behind UTC and negative value means it is ahead. + * For Neo4j temporal types, like `Time` or `DateTime` offset is in seconds and represents difference from UTC to local time. + * This is different from standard JavaScript dates and that's why implementation negates the returned value. + * + * @param {global.Date} standardDate the standard JavaScript date. + * @return {number} the time zone offset in seconds. + */ +export function timeZoneOffsetInSeconds (standardDate: Date): number { + const secondsPortion = standardDate.getSeconds() >= standardDate.getUTCSeconds() + ? standardDate.getSeconds() - standardDate.getUTCSeconds() + : standardDate.getSeconds() - standardDate.getUTCSeconds() + 60 + const offsetInMinutes = standardDate.getTimezoneOffset() + if (offsetInMinutes === 0) { + return 0 + secondsPortion + } + return -1 * offsetInMinutes * SECONDS_PER_MINUTE + secondsPortion +} + +/** + * Assert that the year value is valid. + * @param {Integer|number} year the value to check. + * @return {Integer|number} the value of the year if it is valid. Exception is thrown otherwise. + */ +export function assertValidYear (year: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(year, YEAR_RANGE, 'Year') +} + +/** + * Assert that the month value is valid. + * @param {Integer|number} month the value to check. + * @return {Integer|number} the value of the month if it is valid. Exception is thrown otherwise. + */ +export function assertValidMonth (month: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(month, MONTH_OF_YEAR_RANGE, 'Month') +} + +/** + * Assert that the day value is valid. + * @param {Integer|number} day the value to check. + * @return {Integer|number} the value of the day if it is valid. Exception is thrown otherwise. + */ +export function assertValidDay (day: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(day, DAY_OF_MONTH_RANGE, 'Day') +} + +/** + * Assert that the hour value is valid. + * @param {Integer|number} hour the value to check. + * @return {Integer|number} the value of the hour if it is valid. Exception is thrown otherwise. + */ +export function assertValidHour (hour: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(hour, HOUR_OF_DAY_RANGE, 'Hour') +} + +/** + * Assert that the minute value is valid. + * @param {Integer|number} minute the value to check. + * @return {Integer|number} the value of the minute if it is valid. Exception is thrown otherwise. + */ +export function assertValidMinute (minute: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(minute, MINUTE_OF_HOUR_RANGE, 'Minute') +} + +/** + * Assert that the second value is valid. + * @param {Integer|number} second the value to check. + * @return {Integer|number} the value of the second if it is valid. Exception is thrown otherwise. + */ +export function assertValidSecond (second: NumberOrInteger): NumberOrInteger { + return assertValidTemporalValue(second, SECOND_OF_MINUTE_RANGE, 'Second') +} + +/** + * Assert that the nanosecond value is valid. + * @param {Integer|number} nanosecond the value to check. + * @return {Integer|number} the value of the nanosecond if it is valid. Exception is thrown otherwise. + */ +export function assertValidNanosecond ( + nanosecond: NumberOrInteger +): NumberOrInteger { + return assertValidTemporalValue( + nanosecond, + NANOSECOND_OF_SECOND_RANGE, + 'Nanosecond' + ) +} + +export function assertValidZoneId (fieldName: string, zoneId: string): void { + try { + Intl.DateTimeFormat(undefined, { timeZone: zoneId }) + } catch (e) { + throw newError( + `${fieldName} is expected to be a valid ZoneId but was: "${zoneId}"` + ) + } +} + +/** + * Check if the given value is of expected type and is in the expected range. + * @param {Integer|number} value the value to check. + * @param {ValueRange} range the range. + * @param {string} name the name of the value. + * @return {Integer|number} the value if valid. Exception is thrown otherwise. + */ +function assertValidTemporalValue ( + value: NumberOrInteger, + range: ValueRange, + name: string +): NumberOrInteger { + assertNumberOrInteger(value, name) + if (!range.contains(value)) { + throw newError( + `${name} is expected to be in range ${range.toString()} but was: ${value.toString()}` + ) + } + return value +} + +/** + * Converts given local time into a single integer representing this same time in seconds of the day. Nanoseconds are skipped. + * @param {Integer|number|string} hour the hour of the local time. + * @param {Integer|number|string} minute the minute of the local time. + * @param {Integer|number|string} second the second of the local time. + * @return {Integer} seconds representing the given local time. + */ +function localTimeToSecondOfDay ( + hour: NumberOrInteger | string, + minute: NumberOrInteger | string, + second: NumberOrInteger | string +): Integer { + hour = int(hour) + minute = int(minute) + second = int(second) + + let totalSeconds = hour.multiply(SECONDS_PER_HOUR) + totalSeconds = totalSeconds.add(minute.multiply(SECONDS_PER_MINUTE)) + return totalSeconds.add(second) +} + +/** + * Check if given year is a leap year. Uses algorithm described here {@link https://en.wikipedia.org/wiki/Leap_year#Algorithm}. + * @param {Integer|number|string} year the year to check. Will be converted to {@link Integer} for all calculations. + * @return {boolean} `true` if given year is a leap year, `false` otherwise. + */ +function isLeapYear (year: NumberOrInteger | string): boolean { + year = int(year) + + if (!year.modulo(4).equals(0)) { + return false + } else if (!year.modulo(100).equals(0)) { + return true + } else if (!year.modulo(400).equals(0)) { + return false + } else { + return true + } +} + +/** + * @param {Integer|number|string} x the divident. + * @param {Integer|number|string} y the divisor. + * @return {Integer} the result. + */ +export function floorDiv ( + x: NumberOrInteger | string, + y: NumberOrInteger | string +): Integer { + x = int(x) + y = int(y) + + let result = x.div(y) + if (x.isPositive() !== y.isPositive() && result.multiply(y).notEquals(x)) { + result = result.subtract(1) + } + return result +} + +/** + * @param {Integer|number|string} x the divident. + * @param {Integer|number|string} y the divisor. + * @return {Integer} the result. + */ +export function floorMod ( + x: NumberOrInteger | string, + y: NumberOrInteger | string +): Integer { + x = int(x) + y = int(y) + + return x.subtract(floorDiv(x, y).multiply(y)) +} + +/** + * @param {Integer|number|string} seconds the number of seconds to format. + * @param {Integer|number|string} nanoseconds the number of nanoseconds to format. + * @return {string} formatted value. + */ +function formatSecondsAndNanosecondsForDuration ( + seconds: NumberOrInteger | string, + nanoseconds: NumberOrInteger | string +): string { + seconds = int(seconds) + nanoseconds = int(nanoseconds) + + let secondsString + let nanosecondsString + + const secondsNegative = seconds.isNegative() + const nanosecondsGreaterThanZero = nanoseconds.greaterThan(0) + if (secondsNegative && nanosecondsGreaterThanZero) { + if (seconds.equals(-1)) { + secondsString = '-0' + } else { + secondsString = seconds.add(1).toString() + } + } else { + secondsString = seconds.toString() + } + + if (nanosecondsGreaterThanZero) { + if (secondsNegative) { + nanosecondsString = formatNanosecond( + nanoseconds + .negate() + .add(2 * NANOS_PER_SECOND) + .modulo(NANOS_PER_SECOND) + ) + } else { + nanosecondsString = formatNanosecond( + nanoseconds.add(NANOS_PER_SECOND).modulo(NANOS_PER_SECOND) + ) + } + } + + return nanosecondsString != null ? secondsString + nanosecondsString : secondsString +} + +/** + * @param {Integer|number|string} value the number of nanoseconds to format. + * @return {string} formatted and possibly left-padded nanoseconds part as string. + */ +function formatNanosecond (value: NumberOrInteger | string): string { + value = int(value) + return value.equals(0) ? '' : '.' + formatNumber(value, 9) +} + +/** + * + * @param {Integer|number|string} year The year to be formatted + * @return {string} formatted year + */ +function formatYear (year: NumberOrInteger | string): string { + const yearInteger = int(year) + if (yearInteger.isNegative() || yearInteger.greaterThan(9999)) { + return formatNumber(yearInteger, 6, { usePositiveSign: true }) + } + return formatNumber(yearInteger, 4) +} + +/** + * @param {Integer|number|string} num the number to format. + * @param {number} [stringLength=undefined] the string length to left-pad to. + * @return {string} formatted and possibly left-padded number as string. + */ +function formatNumber ( + num: NumberOrInteger | string, + stringLength?: number, + params?: { + usePositiveSign?: boolean + } +): string { + num = int(num) + const isNegative = num.isNegative() + if (isNegative) { + num = num.negate() + } + + let numString = num.toString() + if (stringLength != null) { + // left pad the string with zeroes + while (numString.length < stringLength) { + numString = '0' + numString + } + } + if (isNegative) { + return '-' + numString + } else if (params?.usePositiveSign === true) { + return '+' + numString + } + return numString +} + +function add (x: NumberOrInteger, y: number): NumberOrInteger { + if (x instanceof Integer) { + return x.add(y) + } else if (typeof x === 'bigint') { + return x + BigInt(y) + } + return x + y +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts b/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts new file mode 100644 index 000000000..df0f131b3 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/transaction-executor.ts @@ -0,0 +1,268 @@ +/** + * 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. + */ +/* eslint-disable @typescript-eslint/promise-function-async */ + +import { newError, isRetriableError } from '../error.ts' +import Transaction from '../transaction.ts' +import TransactionPromise from '../transaction-promise.ts' + +const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds +const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds +const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0 +const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2 + +type TransactionCreator = () => TransactionPromise +type TransactionWork = (tx: Tx) => T | Promise +type Resolve = (value: T | PromiseLike) => void +type Reject = (value: any) => void +type Timeout = ReturnType + +export class TransactionExecutor { + private readonly _maxRetryTimeMs: number + private readonly _initialRetryDelayMs: number + private readonly _multiplier: number + private readonly _jitterFactor: number + private _inFlightTimeoutIds: Timeout[] + + constructor ( + maxRetryTimeMs?: number | null, + initialRetryDelayMs?: number, + multiplier?: number, + jitterFactor?: number + ) { + this._maxRetryTimeMs = _valueOrDefault( + maxRetryTimeMs, + DEFAULT_MAX_RETRY_TIME_MS + ) + this._initialRetryDelayMs = _valueOrDefault( + initialRetryDelayMs, + DEFAULT_INITIAL_RETRY_DELAY_MS + ) + this._multiplier = _valueOrDefault( + multiplier, + DEFAULT_RETRY_DELAY_MULTIPLIER + ) + this._jitterFactor = _valueOrDefault( + jitterFactor, + DEFAULT_RETRY_DELAY_JITTER_FACTOR + ) + + this._inFlightTimeoutIds = [] + + this._verifyAfterConstruction() + } + + execute( + transactionCreator: TransactionCreator, + transactionWork: TransactionWork, + transactionWrapper?: (tx: Transaction) => Tx + ): Promise { + return new Promise((resolve, reject) => { + this._executeTransactionInsidePromise( + transactionCreator, + transactionWork, + resolve, + reject, + transactionWrapper + ).catch(reject) + }).catch(error => { + const retryStartTimeMs = Date.now() + const retryDelayMs = this._initialRetryDelayMs + return this._retryTransactionPromise( + transactionCreator, + transactionWork, + error, + retryStartTimeMs, + retryDelayMs, + transactionWrapper + ) + }) + } + + close (): void { + // cancel all existing timeouts to prevent further retries + this._inFlightTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId)) + this._inFlightTimeoutIds = [] + } + + _retryTransactionPromise( + transactionCreator: TransactionCreator, + transactionWork: TransactionWork, + error: Error, + retryStartTime: number, + retryDelayMs: number, + transactionWrapper?: (tx: Transaction) => Tx + ): Promise { + const elapsedTimeMs = Date.now() - retryStartTime + + if (elapsedTimeMs > this._maxRetryTimeMs || !isRetriableError(error)) { + return Promise.reject(error) + } + + return new Promise((resolve, reject) => { + const nextRetryTime = this._computeDelayWithJitter(retryDelayMs) + const timeoutId = setTimeout(() => { + // filter out this timeoutId when time has come and function is being executed + this._inFlightTimeoutIds = this._inFlightTimeoutIds.filter( + id => id !== timeoutId + ) + this._executeTransactionInsidePromise( + transactionCreator, + transactionWork, + resolve, + reject, + transactionWrapper + ).catch(reject) + }, nextRetryTime) + // add newly created timeoutId to the list of all in-flight timeouts + this._inFlightTimeoutIds.push(timeoutId) + }).catch(error => { + const nextRetryDelayMs = retryDelayMs * this._multiplier + return this._retryTransactionPromise( + transactionCreator, + transactionWork, + error, + retryStartTime, + nextRetryDelayMs, + transactionWrapper + ) + }) + } + + async _executeTransactionInsidePromise( + transactionCreator: TransactionCreator, + transactionWork: TransactionWork, + resolve: Resolve, + reject: Reject, + transactionWrapper?: (tx: Transaction) => Tx + ): Promise { + let tx: Transaction + try { + tx = await transactionCreator() + } catch (error) { + // failed to create a transaction + reject(error) + return + } + + // The conversion from `tx` as `unknown` then to `Tx` is necessary + // because it is not possible to be sure that `Tx` is a subtype of `Transaction` + // in using static type checking. + const wrap = transactionWrapper ?? ((tx: Transaction) => tx as unknown as Tx) + const wrappedTx = wrap(tx) + const resultPromise = this._safeExecuteTransactionWork(wrappedTx, transactionWork) + + resultPromise + .then(result => + this._handleTransactionWorkSuccess(result, tx, resolve, reject) + ) + .catch(error => this._handleTransactionWorkFailure(error, tx, reject)) + } + + _safeExecuteTransactionWork( + tx: Tx, + transactionWork: TransactionWork + ): Promise { + try { + const result = transactionWork(tx) + // user defined callback is supposed to return a promise, but it might not; so to protect against an + // incorrect API usage we wrap the returned value with a resolved promise; this is effectively a + // validation step without type checks + return Promise.resolve(result) + } catch (error) { + return Promise.reject(error) + } + } + + _handleTransactionWorkSuccess( + result: T, + tx: Transaction, + resolve: Resolve, + reject: Reject + ): void { + if (tx.isOpen()) { + // transaction work returned resolved promise and transaction has not been committed/rolled back + // try to commit the transaction + tx.commit() + .then(() => { + // transaction was committed, return result to the user + resolve(result) + }) + .catch(error => { + // transaction failed to commit, propagate the failure + reject(error) + }) + } else { + // transaction work returned resolved promise and transaction is already committed/rolled back + // return the result returned by given transaction work + resolve(result) + } + } + + _handleTransactionWorkFailure (error: any, tx: Transaction, reject: Reject): void { + if (tx.isOpen()) { + // transaction work failed and the transaction is still open, roll it back and propagate the failure + tx.rollback() + .catch(ignore => { + // ignore the rollback error + }) + .then(() => reject(error)) // propagate the original error we got from the transaction work + .catch(reject) + } else { + // transaction is already rolled back, propagate the error + reject(error) + } + } + + _computeDelayWithJitter (delayMs: number): number { + const jitter = delayMs * this._jitterFactor + const min = delayMs - jitter + const max = delayMs + jitter + return Math.random() * (max - min) + min + } + + _verifyAfterConstruction (): void { + if (this._maxRetryTimeMs < 0) { + throw newError('Max retry time should be >= 0: ' + this._maxRetryTimeMs.toString()) + } + if (this._initialRetryDelayMs < 0) { + throw newError( + 'Initial retry delay should >= 0: ' + this._initialRetryDelayMs.toString() + ) + } + if (this._multiplier < 1.0) { + throw newError('Multiplier should be >= 1.0: ' + this._multiplier.toString()) + } + if (this._jitterFactor < 0 || this._jitterFactor > 1) { + throw newError( + 'Jitter factor should be in [0.0, 1.0]: ' + this._jitterFactor.toFixed() + ) + } + } +} + +function _valueOrDefault ( + value: number | undefined | null, + defaultValue: number +): number { + if (value != null) { + return value + } + return defaultValue +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts b/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts new file mode 100644 index 000000000..825af19cf --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/tx-config.ts @@ -0,0 +1,97 @@ +/** + * 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 util from './util.ts' +import { newError } from '../error.ts' +import Integer, { int } from '../integer.ts' + +/** + * Internal holder of the transaction configuration. + * It performs input validation and value conversion for further serialization by the Bolt protocol layer. + * Users of the driver provide transaction configuration as regular objects `{timeout: 10, metadata: {key: 'value'}}`. + * Driver converts such objects to {@link TxConfig} immediately and uses converted values everywhere. + */ +export class TxConfig { + readonly timeout: Integer | null + readonly metadata: any + + /** + * @constructor + * @param {Object} config the raw configuration object. + */ + constructor (config: any) { + assertValidConfig(config) + this.timeout = extractTimeout(config) + this.metadata = extractMetadata(config) + } + + /** + * Get an empty config object. + * @return {TxConfig} an empty config. + */ + static empty (): TxConfig { + return EMPTY_CONFIG + } + + /** + * Check if this config object is empty. I.e. has no configuration values specified. + * @return {boolean} `true` if this object is empty, `false` otherwise. + */ + isEmpty (): boolean { + return Object.values(this).every(value => value == null) + } +} + +const EMPTY_CONFIG = new TxConfig({}) + +/** + * @return {Integer|null} + */ +function extractTimeout (config: any): Integer | null { + if (util.isObject(config) && config.timeout != null) { + util.assertNumberOrInteger(config.timeout, 'Transaction timeout') + const timeout = int(config.timeout) + if (timeout.isNegative()) { + throw newError('Transaction timeout should not be negative') + } + return timeout + } + return null +} + +/** + * @return {object|null} + */ +function extractMetadata (config: any): any { + if (util.isObject(config) && config.metadata != null) { + const metadata = config.metadata + util.assertObject(metadata, 'config.metadata') + if (Object.keys(metadata).length !== 0) { + // not an empty object + return metadata + } + } + return null +} + +function assertValidConfig (config: any): void { + if (config != null) { + util.assertObject(config, 'Transaction config') + } +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/url-util.ts b/packages/neo4j-driver-deno/lib/core/internal/url-util.ts new file mode 100644 index 000000000..1f1913d0a --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/url-util.ts @@ -0,0 +1,343 @@ +/** + * 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 { assertString } from './util.ts' + +const DEFAULT_BOLT_PORT = 7687 +const DEFAULT_HTTP_PORT = 7474 +const DEFAULT_HTTPS_PORT = 7473 + +class Url { + readonly scheme: string | null + readonly host: string + readonly port: number + readonly hostAndPort: string + readonly query: Object + + constructor ( + scheme: string | null, + host: string, + port: number, + hostAndPort: string, + query: Object + ) { + /** + * Nullable scheme (protocol) of the URL. + * Example: 'bolt', 'neo4j', 'http', 'https', etc. + * @type {string} + */ + this.scheme = scheme + + /** + * Nonnull host name or IP address. IPv6 not wrapped in square brackets. + * Example: 'neo4j.com', 'localhost', '127.0.0.1', '192.168.10.15', '::1', '2001:4860:4860::8844', etc. + * @type {string} + */ + this.host = host + + /** + * Nonnull number representing port. Default port for the given scheme is used if given URL string + * does not contain port. Example: 7687 for bolt, 7474 for HTTP and 7473 for HTTPS. + * @type {number} + */ + this.port = port + + /** + * Nonnull host name or IP address plus port, separated by ':'. IPv6 wrapped in square brackets. + * Example: 'neo4j.com', 'neo4j.com:7687', '127.0.0.1', '127.0.0.1:8080', '[2001:4860:4860::8844]', + * '[2001:4860:4860::8844]:9090', etc. + * @type {string} + */ + this.hostAndPort = hostAndPort + + /** + * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. + * Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc. + * @type {Object} + */ + this.query = query + } +} + +interface ParsedUri { + scheme?: string + host?: string + port?: number | string + query?: string + fragment?: string + userInfo?: string + authority?: string + path?: string +} + +function parseDatabaseUrl (url: string): Url { + assertString(url, 'URL') + + const sanitized = sanitizeUrl(url) + const parsedUrl = uriJsParse(sanitized.url) + + const scheme = sanitized.schemeMissing + ? null + : extractScheme(parsedUrl.scheme) + const host = extractHost(parsedUrl.host) // no square brackets for IPv6 + const formattedHost = formatHost(host) // has square brackets for IPv6 + const port = extractPort(parsedUrl.port, scheme) + const hostAndPort = `${formattedHost}:${port}` + const query = extractQuery( + // @ts-expect-error + parsedUrl.query ?? extractResourceQueryString(parsedUrl.resourceName), + url + ) + + return new Url(scheme, host, port, hostAndPort, query) +} + +function extractResourceQueryString (resource?: string): string | null { + if (typeof resource !== 'string') { + return null + } + const [, query] = resource.split('?') + return query +} + +function sanitizeUrl (url: string): { schemeMissing: boolean, url: string } { + url = url.trim() + + if (!url.includes('://')) { + // url does not contain scheme, add dummy 'none://' to make parser work correctly + return { schemeMissing: true, url: `none://${url}` } + } + + return { schemeMissing: false, url: url } +} + +function extractScheme (scheme?: string): string | null { + if (scheme != null) { + scheme = scheme.trim() + if (scheme.charAt(scheme.length - 1) === ':') { + scheme = scheme.substring(0, scheme.length - 1) + } + return scheme + } + return null +} + +function extractHost (host?: string, url?: string): string { + if (host == null) { + throw new Error('Unable to extract host from null or undefined URL') + } + return host.trim() +} + +function extractPort ( + portString: string | number | undefined, + scheme: string | null +): number { + const port = + typeof portString === 'string' ? parseInt(portString, 10) : portString + return port != null && !isNaN(port) ? port : defaultPortForScheme(scheme) +} + +function extractQuery ( + queryString: string | undefined | null, + url: string +): Object { + const query = queryString != null ? trimAndSanitizeQuery(queryString) : null + const context: any = {} + + if (query != null) { + query.split('&').forEach((pair: string) => { + const keyValue = pair.split('=') + if (keyValue.length !== 2) { + throw new Error(`Invalid parameters: '${keyValue.toString()}' in URL '${url}'.`) + } + + const key = trimAndVerifyQueryElement(keyValue[0], 'key', url) + const value = trimAndVerifyQueryElement(keyValue[1], 'value', url) + + if (context[key] !== undefined) { + throw new Error( + `Duplicated query parameters with key '${key}' in URL '${url}'` + ) + } + + context[key] = value + }) + } + + return context +} + +function trimAndSanitizeQuery (query: string): string { + query = (query ?? '').trim() + if (query?.charAt(0) === '?') { + query = query.substring(1, query.length) + } + return query +} + +function trimAndVerifyQueryElement (element: string, name: string, url: string): string { + element = (element ?? '').trim() + if (element === '') { + throw new Error(`Illegal empty ${name} in URL query '${url}'`) + } + return element +} + +function escapeIPv6Address (address: string): string { + const startsWithSquareBracket = address.charAt(0) === '[' + const endsWithSquareBracket = address.charAt(address.length - 1) === ']' + + if (!startsWithSquareBracket && !endsWithSquareBracket) { + return `[${address}]` + } else if (startsWithSquareBracket && endsWithSquareBracket) { + return address + } else { + throw new Error(`Illegal IPv6 address ${address}`) + } +} + +function formatHost (host: string): string { + if (host === '' || host == null) { + throw new Error(`Illegal host ${host}`) + } + const isIPv6Address = host.includes(':') + return isIPv6Address ? escapeIPv6Address(host) : host +} + +function formatIPv4Address (address: string, port: number): string { + return `${address}:${port}` +} + +function formatIPv6Address (address: string, port: number): string { + const escapedAddress = escapeIPv6Address(address) + return `${escapedAddress}:${port}` +} + +function defaultPortForScheme (scheme: string | null): number { + if (scheme === 'http') { + return DEFAULT_HTTP_PORT + } else if (scheme === 'https') { + return DEFAULT_HTTPS_PORT + } else { + return DEFAULT_BOLT_PORT + } +} + +function uriJsParse (value: string): ParsedUri { + // JS version of Python partition function + function partition (s: string, delimiter: string): [string, string, string] { + const i = s.indexOf(delimiter) + if (i >= 0) return [s.substring(0, i), s[i], s.substring(i + 1)] + else return [s, '', ''] + } + + // JS version of Python rpartition function + function rpartition (s: string, delimiter: string): [string, string, string] { + const i = s.lastIndexOf(delimiter) + if (i >= 0) return [s.substring(0, i), s[i], s.substring(i + 1)] + else return ['', '', s] + } + + function between ( + s: string, + ldelimiter: string, + rdelimiter: string + ): [string, string] { + const lpartition = partition(s, ldelimiter) + const rpartition = partition(lpartition[2], rdelimiter) + return [rpartition[0], rpartition[2]] + } + + // Parse an authority string into an object + // with the following keys: + // - userInfo (optional, might contain both user name and password) + // - host + // - port (optional, included only as a string) + function parseAuthority (value: string): ParsedUri { + const parsed: ParsedUri = {} + let parts: [string, string, string] + + // Parse user info + parts = rpartition(value, '@') + if (parts[1] === '@') { + parsed.userInfo = decodeURIComponent(parts[0]) + value = parts[2] + } + + // Parse host and port + const [ipv6Host, rest] = between(value, '[', ']') + if (ipv6Host !== '') { + parsed.host = ipv6Host + parts = partition(rest, ':') + } else { + parts = partition(value, ':') + parsed.host = parts[0] + } + + if (parts[1] === ':') { + parsed.port = parts[2] + } + + return parsed + } + + let parsed: ParsedUri = {} + let parts: string[] + + // Parse scheme + parts = partition(value, ':') + if (parts[1] === ':') { + parsed.scheme = decodeURIComponent(parts[0]) + value = parts[2] + } + + // Parse fragment + parts = partition(value, '#') + if (parts[1] === '#') { + parsed.fragment = decodeURIComponent(parts[2]) + value = parts[0] + } + + // Parse query + parts = partition(value, '?') + if (parts[1] === '?') { + parsed.query = parts[2] + value = parts[0] + } + + // Parse authority and path + if (value.startsWith('//')) { + parts = partition(value.substr(2), '/') + parsed = { ...parsed, ...parseAuthority(parts[0]) } + parsed.path = parts[1] + parts[2] + } else { + parsed.path = value + } + + return parsed +} + +export { + parseDatabaseUrl, + defaultPortForScheme, + formatIPv4Address, + formatIPv6Address, + Url +} diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts new file mode 100644 index 000000000..3a181fdae --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -0,0 +1,238 @@ +/** + * 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. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import Integer, { isInt } from '../integer.ts' +import { NumberOrInteger } from '../graph-types.ts' +import { EncryptionLevel } from '../types.ts' +import { stringify } from '../json.ts' + +const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON' +const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF' +/** + * Verifies if the object is null or empty + * @param obj The subject object + * @returns {boolean} True if it's empty object or null + */ +function isEmptyObjectOrNull (obj?: any): boolean { + if (obj === null) { + return true + } + + if (!isObject(obj)) { + return false + } + + for (const prop in obj) { + if (obj[prop] !== undefined) { + return false + } + } + + return true +} + +/** + * Verify if it's an object + * @param obj The subject + * @returns {boolean} True if it's an object + */ +function isObject (obj: any): boolean { + return typeof obj === 'object' && !Array.isArray(obj) && obj !== null +} + +/** + * Check and normalize given query and parameters. + * @param {string|{text: string, parameters: Object}} query the query to check. + * @param {Object} parameters + * @return {{validatedQuery: string|{text: string, parameters: Object}, params: Object}} the normalized query with parameters. + * @throws TypeError when either given query or parameters are invalid. + */ +function validateQueryAndParameters ( + query: string | String | { text: string, parameters?: any }, + parameters?: any, + opt?: { skipAsserts: boolean } +): { + validatedQuery: string + params: any + } { + let validatedQuery: string = '' + let params = parameters ?? {} + const skipAsserts: boolean = opt?.skipAsserts ?? false + + if (typeof query === 'string') { + validatedQuery = query + } else if (query instanceof String) { + validatedQuery = query.toString() + } else if (typeof query === 'object' && query.text != null) { + validatedQuery = query.text + params = query.parameters ?? {} + } + + if (!skipAsserts) { + assertCypherQuery(validatedQuery) + assertQueryParameters(params) + } + + return { validatedQuery, params } +} + +/** + * Assert it's a object + * @param {any} obj The subject + * @param {string} objName The object name + * @returns {object} The subject object + * @throws {TypeError} when the supplied param is not an object + */ +function assertObject (obj: any, objName: string): Object { + if (!isObject(obj)) { + throw new TypeError( + objName + ' expected to be an object but was: ' + stringify(obj) + ) + } + return obj +} + +/** + * Assert it's a string + * @param {any} obj The subject + * @param {string} objName The object name + * @returns {string} The subject string + * @throws {TypeError} when the supplied param is not a string + */ +function assertString (obj: any, objName: Object): string { + if (!isString(obj)) { + throw new TypeError( + stringify(objName) + ' expected to be string but was: ' + stringify(obj) + ) + } + return obj +} + +/** + * Assert it's a number + * @param {any} obj The subject + * @param {string} objName The object name + * @returns {number} The number + * @throws {TypeError} when the supplied param is not a number + */ +function assertNumber (obj: any, objName: string): number { + if (typeof obj !== 'number') { + throw new TypeError( + objName + ' expected to be a number but was: ' + stringify(obj) + ) + } + return obj +} + +/** + * Assert it's a number or integer + * @param {any} obj The subject + * @param {string} objName The object name + * @returns {number|Integer} The subject object + * @throws {TypeError} when the supplied param is not a number or integer + */ +function assertNumberOrInteger (obj: any, objName: string): NumberOrInteger { + if (typeof obj !== 'number' && typeof obj !== 'bigint' && !isInt(obj)) { + throw new TypeError( + objName + + ' expected to be either a number or an Integer object but was: ' + + stringify(obj) + ) + } + return obj +} + +/** + * Assert it's a valid datae + * @param {any} obj The subject + * @param {string} objName The object name + * @returns {Date} The valida date + * @throws {TypeError} when the supplied param is not a valid date + */ +function assertValidDate (obj: any, objName: string): Date { + if (Object.prototype.toString.call(obj) !== '[object Date]') { + throw new TypeError( + objName + + ' expected to be a standard JavaScript Date but was: ' + + stringify(obj) + ) + } + if (Number.isNaN(obj.getTime())) { + throw new TypeError( + objName + + ' expected to be valid JavaScript Date but its time was NaN: ' + + stringify(obj) + ) + } + return obj +} + +/** + * Validates a cypher query string + * @param {any} obj The query + * @returns {void} + * @throws {TypeError} if the query is not valid + */ +function assertCypherQuery (obj: any): void { + assertString(obj, 'Cypher query') + if (obj.trim().length === 0) { + throw new TypeError('Cypher query is expected to be a non-empty string.') + } +} + +/** + * Validates if the query parameters is an object + * @param {any} obj The parameters + * @returns {void} + * @throws {TypeError} if the parameters is not valid + */ +function assertQueryParameters (obj: any): void { + if (!isObject(obj)) { + // objects created with `Object.create(null)` do not have a constructor property + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + const constructor = obj.constructor != null ? ' ' + obj.constructor.name : '' + throw new TypeError( + `Query parameters are expected to either be undefined/null or an object, given:${constructor} ${JSON.stringify(obj)}` + ) + } +} + +/** + * Verify if the supplied object is a string + * + * @param str The string + * @returns {boolean} True if the supplied object is an string + */ +function isString (str: any): str is string { + return Object.prototype.toString.call(str) === '[object String]' +} + +export { + isEmptyObjectOrNull, + isObject, + isString, + assertObject, + assertString, + assertNumber, + assertNumberOrInteger, + assertValidDate, + validateQueryAndParameters, + ENCRYPTION_ON, + ENCRYPTION_OFF +} diff --git a/packages/neo4j-driver-deno/lib/core/json.ts b/packages/neo4j-driver-deno/lib/core/json.ts new file mode 100644 index 000000000..9800682b8 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/json.ts @@ -0,0 +1,41 @@ +/** + * 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 { isBrokenObject, getBrokenObjectReason } from './internal/object-util.ts' + +/** + * Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt. + * @private + * @param val A JavaScript value, usually an object or array, to be converted. + * @returns A JSON string representing the given value. + */ +export function stringify (val: any): string { + return JSON.stringify(val, (_, value) => { + if (isBrokenObject(value)) { + return { + __isBrokenObject__: true, + __reason__: getBrokenObjectReason(value) + } + } + if (typeof value === 'bigint') { + return `${value}n` + } + return value + }) +} diff --git a/packages/neo4j-driver-deno/lib/core/record.ts b/packages/neo4j-driver-deno/lib/core/record.ts new file mode 100644 index 000000000..759cbf821 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/record.ts @@ -0,0 +1,245 @@ +/** + * 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 { newError } from './error.ts' + +type Dict = { + [K in Key]: Value +} + +type Visitor< + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries +> = MapVisitor + +type MapVisitor< + ReturnType, + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries +> = (value: Entries[Key], key: Key, record: Record) => ReturnType + +function generateFieldLookup< + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries, + FieldLookup extends Dict = Dict +> (keys: Key[]): FieldLookup { + const lookup: Dict = {} + keys.forEach((name, idx) => { + lookup[name as string] = idx + }) + return lookup as FieldLookup +} + +/** + * Records make up the contents of the {@link Result}, and is how you access + * the output of a query. A simple query might yield a result stream + * with a single record, for instance: + * + * MATCH (u:User) RETURN u.name, u.age + * + * This returns a stream of records with two fields, named `u.name` and `u.age`, + * each record represents one user found by the query above. You can access + * the values of each field either by name: + * + * record.get("u.name") + * + * Or by it's position: + * + * record.get(0) + * + * @access public + */ +class Record< + Entries extends Dict = Dict, + Key extends keyof Entries = keyof Entries, + FieldLookup extends Dict = Dict +> { + keys: Key[] + length: number + private readonly _fields: any[] + private readonly _fieldLookup: FieldLookup + + /** + * Create a new record object. + * @constructor + * @protected + * @param {string[]} keys An array of field keys, in the order the fields appear in the record + * @param {Array} fields An array of field values + * @param {Object} fieldLookup An object of fieldName -> value index, used to map + * field names to values. If this is null, one will be + * generated. + */ + constructor (keys: Key[], fields: any[], fieldLookup?: FieldLookup) { + /** + * Field keys, in the order the fields appear in the record. + * @type {string[]} + */ + this.keys = keys + /** + * Number of fields + * @type {Number} + */ + this.length = keys.length + this._fields = fields + this._fieldLookup = fieldLookup ?? generateFieldLookup(keys) + } + + /** + * Run the given function for each field in this record. The function + * will get three arguments - the value, the key and this record, in that + * order. + * + * @param {function(value: Object, key: string, record: Record)} visitor the function to apply to each field. + * @returns {void} Nothing + */ + forEach (visitor: Visitor): void { + for (const [key, value] of this.entries()) { + visitor(value, key as Key, this) + } + } + + /** + * Run the given function for each field in this record. The function + * will get three arguments - the value, the key and this record, in that + * order. + * + * @param {function(value: Object, key: string, record: Record)} visitor the function to apply on each field + * and return a value that is saved to the returned Array. + * + * @returns {Array} + */ + map(visitor: MapVisitor): Value[] { + const resultArray = [] + + for (const [key, value] of this.entries()) { + resultArray.push(visitor(value, key as Key, this)) + } + + return resultArray + } + + /** + * Iterate over results. Each iteration will yield an array + * of exactly two items - the key, and the value (in order). + * + * @generator + * @returns {IterableIterator} + */ + * entries (): IterableIterator<[string, any]> { + for (let i = 0; i < this.keys.length; i++) { + yield [this.keys[i] as string, this._fields[i]] + } + } + + /** + * Iterate over values. + * + * @generator + * @returns {IterableIterator} + */ + * values (): IterableIterator { + for (let i = 0; i < this.keys.length; i++) { + yield this._fields[i] + } + } + + /** + * Iterate over values. Delegates to {@link Record#values} + * + * @generator + * @returns {IterableIterator} + */ + * [Symbol.iterator] (): IterableIterator { + for (let i = 0; i < this.keys.length; i++) { + yield this._fields[i] + } + } + + /** + * Generates an object out of the current Record + * + * @returns {Object} + */ + toObject (): Entries { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const obj: Entries = {} as Entries + + for (const [key, value] of this.entries()) { + obj[key as Key] = value + } + + return obj + } + + get(key: K): Entries[K] + get (key: keyof FieldLookup | number): any + + /** + * Get a value from this record, either by index or by field key. + * + * @param {string|Number} key Field key, or the index of the field. + * @returns {*} + */ + get (key: string | number): any { + let index + if (!(typeof key === 'number')) { + index = this._fieldLookup[key] + if (index === undefined) { + throw newError( + "This record has no field with key '" + + key + + "', available key are: [" + + this.keys.toString() + + '].' + ) + } + } else { + index = key + } + + if (index > this._fields.length - 1 || index < 0) { + throw newError( + "This record has no field with index '" + + index.toString() + + "'. Remember that indexes start at `0`, " + + 'and make sure your query returns records in the shape you meant it to.' + ) + } + + return this._fields[index] + } + + /** + * Check if a value from this record, either by index or by field key, exists. + * + * @param {string|Number} key Field key, or the index of the field. + * @returns {boolean} + */ + has (key: Key | string | number): boolean { + // if key is a number, we check if it is in the _fields array + if (typeof key === 'number') { + return key >= 0 && key < this._fields.length + } + + // if it's not a number, we check _fieldLookup dictionary directly + return this._fieldLookup[key as string] !== undefined + } +} + +export default Record diff --git a/packages/neo4j-driver-deno/lib/core/result-summary.ts b/packages/neo4j-driver-deno/lib/core/result-summary.ts new file mode 100644 index 000000000..771075922 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/result-summary.ts @@ -0,0 +1,549 @@ +/** + * 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 Integer, { int } from './integer.ts' +import { NumberOrInteger } from './graph-types.ts' + +/** + * A ResultSummary instance contains structured metadata for a {@link Result}. + * @access public + */ +class ResultSummary { + query: { text: string, parameters: { [key: string]: any } } + queryType: string + counters: QueryStatistics + updateStatistics: QueryStatistics + plan: Plan | false + profile: ProfiledPlan | false + notifications: Notification[] + server: ServerInfo + resultConsumedAfter: T + resultAvailableAfter: T + database: { name: string | undefined | null } + /** + * @constructor + * @param {string} query - The query this summary is for + * @param {Object} parameters - Parameters for the query + * @param {Object} metadata - Query metadata + * @param {number|undefined} protocolVersion - Bolt Protocol Version + */ + constructor ( + query: string, + parameters: { [key: string]: any }, + metadata: any, + protocolVersion?: number + ) { + /** + * The query and parameters this summary is for. + * @type {{text: string, parameters: Object}} + * @public + */ + this.query = { text: query, parameters } + + /** + * The type of query executed. Can be "r" for read-only query, "rw" for read-write query, + * "w" for write-only query and "s" for schema-write query. + * String constants are available in {@link queryType} object. + * @type {string} + * @public + */ + this.queryType = metadata.type + + /** + * Counters for operations the query triggered. + * @type {QueryStatistics} + * @public + */ + this.counters = new QueryStatistics(metadata.stats ?? {}) + // for backwards compatibility, remove in future version + /** + * Use {@link ResultSummary.counters} instead. + * @type {QueryStatistics} + * @deprecated + */ + this.updateStatistics = this.counters + + /** + * This describes how the database will execute the query. + * Query plan for the executed query if available, otherwise undefined. + * Will only be populated for queries that start with "EXPLAIN". + * @type {Plan|false} + * @public + */ + this.plan = + metadata.plan != null || metadata.profile != null + ? new Plan(metadata.plan ?? metadata.profile) + : false + + /** + * This describes how the database did execute your query. This will contain detailed information about what + * each step of the plan did. Profiled query plan for the executed query if available, otherwise undefined. + * Will only be populated for queries that start with "PROFILE". + * @type {ProfiledPlan} + * @public + */ + this.profile = metadata.profile != null ? new ProfiledPlan(metadata.profile) : false + + /** + * An array of notifications that might arise when executing the query. Notifications can be warnings about + * problematic queries or other valuable information that can be presented in a client. Unlike failures + * or errors, notifications do not affect the execution of a query. + * @type {Array} + * @public + */ + this.notifications = this._buildNotifications(metadata.notifications) + + /** + * The basic information of the server where the result is obtained from. + * @type {ServerInfo} + * @public + */ + this.server = new ServerInfo(metadata.server, protocolVersion) + + /** + * The time it took the server to consume the result. + * @type {number} + * @public + */ + this.resultConsumedAfter = metadata.result_consumed_after + + /** + * The time it took the server to make the result available for consumption in milliseconds. + * @type {number} + * @public + */ + this.resultAvailableAfter = metadata.result_available_after + + /** + * The database name where this summary is obtained from. + * @type {{name: string}} + * @public + */ + this.database = { name: metadata.db ?? null } + } + + _buildNotifications (notifications: any[]): Notification[] { + if (notifications == null) { + return [] + } + return notifications.map(function (n: any): Notification { + return new Notification(n) + }) + } + + /** + * Check if the result summary has a plan + * @return {boolean} + */ + hasPlan (): boolean { + return this.plan instanceof Plan + } + + /** + * Check if the result summary has a profile + * @return {boolean} + */ + hasProfile (): boolean { + return this.profile instanceof ProfiledPlan + } +} + +/** + * Class for execution plan received by prepending Cypher with EXPLAIN. + * @access public + */ +class Plan { + operatorType: string + identifiers: string[] + arguments: { [key: string]: string } + children: Plan[] + + /** + * Create a Plan instance + * @constructor + * @param {Object} plan - Object with plan data + */ + constructor (plan: any) { + this.operatorType = plan.operatorType + this.identifiers = plan.identifiers + this.arguments = plan.args + this.children = plan.children != null + ? plan.children.map((child: any) => new Plan(child)) + : [] + } +} + +/** + * Class for execution plan received by prepending Cypher with PROFILE. + * @access public + */ +class ProfiledPlan { + operatorType: string + identifiers: string[] + arguments: { [key: string]: string } + dbHits: number + rows: number + pageCacheMisses: number + pageCacheHits: number + pageCacheHitRatio: number + time: number + children: ProfiledPlan[] + + /** + * Create a ProfiledPlan instance + * @constructor + * @param {Object} profile - Object with profile data + */ + constructor (profile: any) { + this.operatorType = profile.operatorType + this.identifiers = profile.identifiers + this.arguments = profile.args + this.dbHits = valueOrDefault('dbHits', profile) + this.rows = valueOrDefault('rows', profile) + this.pageCacheMisses = valueOrDefault('pageCacheMisses', profile) + this.pageCacheHits = valueOrDefault('pageCacheHits', profile) + this.pageCacheHitRatio = valueOrDefault('pageCacheHitRatio', profile) + this.time = valueOrDefault('time', profile) + this.children = profile.children != null + ? profile.children.map((child: any) => new ProfiledPlan(child)) + : [] + } + + hasPageCacheStats (): boolean { + return ( + this.pageCacheMisses > 0 || + this.pageCacheHits > 0 || + this.pageCacheHitRatio > 0 + ) + } +} + +/** + * Stats Query statistics dictionary for a {@link QueryStatistics} + * @public + */ +class Stats { + nodesCreated: number + nodesDeleted: number + relationshipsCreated: number + relationshipsDeleted: number + propertiesSet: number + labelsAdded: number + labelsRemoved: number + indexesAdded: number + indexesRemoved: number + constraintsAdded: number + constraintsRemoved: number; + [key: string]: number + + /** + * @constructor + * @private + */ + constructor () { + /** + * nodes created + * @type {number} + * @public + */ + this.nodesCreated = 0 + /** + * nodes deleted + * @type {number} + * @public + */ + this.nodesDeleted = 0 + /** + * relationships created + * @type {number} + * @public + */ + this.relationshipsCreated = 0 + /** + * relationships deleted + * @type {number} + * @public + */ + this.relationshipsDeleted = 0 + /** + * properties set + * @type {number} + * @public + */ + this.propertiesSet = 0 + /** + * labels added + * @type {number} + * @public + */ + this.labelsAdded = 0 + /** + * labels removed + * @type {number} + * @public + */ + this.labelsRemoved = 0 + /** + * indexes added + * @type {number} + * @public + */ + this.indexesAdded = 0 + /** + * indexes removed + * @type {number} + * @public + */ + this.indexesRemoved = 0 + /** + * constraints added + * @type {number} + * @public + */ + this.constraintsAdded = 0 + /** + * constraints removed + * @type {number} + * @public + */ + this.constraintsRemoved = 0 + } +} + +/** + * Get statistical information for a {@link Result}. + * @access public + */ +class QueryStatistics { + private _stats: Stats + private _systemUpdates: number + private _containsSystemUpdates?: boolean + private _containsUpdates?: boolean + + /** + * Structurize the statistics + * @constructor + * @param {Object} statistics - Result statistics + */ + constructor (statistics: any) { + this._stats = { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + } + this._systemUpdates = 0 + Object.keys(statistics).forEach(index => { + // To camelCase + const camelCaseIndex = index.replace(/(-\w)/g, m => m[1].toUpperCase()) + if (camelCaseIndex in this._stats) { + this._stats[camelCaseIndex] = intValue(statistics[index]) + } else if (camelCaseIndex === 'systemUpdates') { + this._systemUpdates = intValue(statistics[index]) + } else if (camelCaseIndex === 'containsSystemUpdates') { + this._containsSystemUpdates = statistics[index] + } else if (camelCaseIndex === 'containsUpdates') { + this._containsUpdates = statistics[index] + } + }) + + this._stats = Object.freeze(this._stats) + } + + /** + * Did the database get updated? + * @return {boolean} + */ + containsUpdates (): boolean { + return this._containsUpdates !== undefined + ? this._containsUpdates + : ( + Object.keys(this._stats).reduce((last, current) => { + return last + this._stats[current] + }, 0) > 0 + ) + } + + /** + * Returns the query statistics updates in a dictionary. + * @returns {Stats} + */ + updates (): Stats { + return this._stats + } + + /** + * Return true if the system database get updated, otherwise false + * @returns {boolean} - If the system database get updated or not. + */ + containsSystemUpdates (): boolean { + return this._containsSystemUpdates !== undefined + ? this._containsSystemUpdates + : this._systemUpdates > 0 + } + + /** + * @returns {number} - Number of system updates + */ + systemUpdates (): number { + return this._systemUpdates + } +} + +interface NotificationPosition { + offset?: number + line?: number + column?: number +} + +/** + * Class for Cypher notifications + * @access public + */ +class Notification { + code: string + title: string + description: string + severity: string + position: NotificationPosition | {} + + /** + * Create a Notification instance + * @constructor + * @param {Object} notification - Object with notification data + */ + constructor (notification: any) { + this.code = notification.code + this.title = notification.title + this.description = notification.description + this.severity = notification.severity + this.position = Notification._constructPosition(notification.position) + } + + static _constructPosition (pos: NotificationPosition): NotificationPosition { + if (pos == null) { + return {} + } + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + return { + offset: intValue(pos.offset!), + line: intValue(pos.line!), + column: intValue(pos.column!) + } + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + } +} + +/** + * Class for exposing server info from a result. + * @access public + */ +class ServerInfo { + address?: string + protocolVersion?: number + agent?: string + + /** + * Create a ServerInfo instance + * @constructor + * @param {Object} serverMeta - Object with serverMeta data + * @param {Object} connectionInfo - Bolt connection info + * @param {number} protocolVersion - Bolt Protocol Version + */ + constructor (serverMeta?: any, protocolVersion?: number) { + if (serverMeta != null) { + /** + * The server adress + * @type {string} + * @public + */ + this.address = serverMeta.address + + /** + * The server user agent string + * @type {string} + * @public + */ + this.agent = serverMeta.version + } + + /** + * The protocol version used by the connection + * @type {number} + * @public + */ + this.protocolVersion = protocolVersion + } +} + +function intValue (value: NumberOrInteger): number { + if (value instanceof Integer) { + return value.toInt() + } else if (typeof value === 'bigint') { + return int(value).toInt() + } else { + return value + } +} + +function valueOrDefault ( + key: string, + values: { [key: string]: NumberOrInteger } | false, + defaultValue: number = 0 +): number { + if (values !== false && key in values) { + const value = values[key] + return intValue(value) + } else { + return defaultValue + } +} + +/** + * The constants for query types + * @type {{SCHEMA_WRITE: string, WRITE_ONLY: string, READ_ONLY: string, READ_WRITE: string}} + */ +const queryType = { + READ_ONLY: 'r', + READ_WRITE: 'rw', + WRITE_ONLY: 'w', + SCHEMA_WRITE: 's' +} + +export { + queryType, + ServerInfo, + Notification, + Plan, + ProfiledPlan, + QueryStatistics, + Stats +} +export type { + NotificationPosition +} + +export default ResultSummary diff --git a/packages/neo4j-driver-deno/lib/core/result.ts b/packages/neo4j-driver-deno/lib/core/result.ts new file mode 100644 index 000000000..e2e380aaa --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/result.ts @@ -0,0 +1,656 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/promise-function-async */ + +import ResultSummary from './result-summary.ts' +import Record from './record.ts' +import { Query, PeekableAsyncIterator } from './types.ts' +import { observer, util, connectionHolder } from './internal/index.ts' +import { newError, PROTOCOL_ERROR } from './error.ts' +import { NumberOrInteger } from './graph-types.ts' + +const { EMPTY_CONNECTION_HOLDER } = connectionHolder + +/** + * @private + * @param {Error} error The error + * @returns {void} + */ +const DEFAULT_ON_ERROR = (error: Error): void => { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string + console.log('Uncaught error when processing result: ' + error) +} + +/** + * @private + * @param {ResultSummary} summary + * @returns {void} + */ +const DEFAULT_ON_COMPLETED = (summary: ResultSummary): void => {} + +/** + * @private + * @param {string[]} keys List of keys of the record in the result + * @return {void} + */ +const DEFAULT_ON_KEYS = (keys: string[]): void => {} + +/** + * The query result is the combination of the {@link ResultSummary} and + * the array {@link Record[]} produced by the query + */ +interface QueryResult { + records: Record[] + summary: ResultSummary +} + +/** + * Interface to observe updates on the Result which is being produced. + * + */ +interface ResultObserver { + /** + * Receive the keys present on the record whenever this information is available + * + * @param {string[]} keys The keys present on the {@link Record} + */ + onKeys?: (keys: string[]) => void + + /** + * Receive the each record present on the {@link @Result} + * @param {Record} record The {@link Record} produced + */ + onNext?: (record: Record) => void + + /** + * Called when the result is fully received + * @param {ResultSummary} summary The result summary + */ + onCompleted?: (summary: ResultSummary) => void + + /** + * Called when some error occurs during the result proccess or query execution + * @param {Error} error The error ocurred + */ + onError?: (error: Error) => void +} + +/** + * Defines a ResultObserver interface which can be used to enqueue records and dequeue + * them until the result is fully received. + * @access private + */ +interface QueuedResultObserver extends ResultObserver { + dequeue: () => Promise> + dequeueUntilDone: () => Promise> + head: () => Promise> + size: number +} + +/** + * A stream of {@link Record} representing the result of a query. + * Can be consumed eagerly as {@link Promise} resolved with array of records and {@link ResultSummary} + * summary, or rejected with error that contains {@link string} code and {@link string} message. + * Alternatively can be consumed lazily using {@link Result#subscribe} function. + * @access public + */ +class Result implements Promise { + private readonly _stack: string | null + private readonly _streamObserverPromise: Promise + private _p: Promise | null + private readonly _query: Query + private readonly _parameters: any + private readonly _connectionHolder: connectionHolder.ConnectionHolder + private _keys: string[] | null + private _summary: ResultSummary | null + private _error: Error | null + private readonly _watermarks: { high: number, low: number } + + /** + * Inject the observer to be used. + * @constructor + * @access private + * @param {Promise} streamObserverPromise + * @param {mixed} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @param {ConnectionHolder} connectionHolder - to be notified when result is either fully consumed or error happened. + */ + constructor ( + streamObserverPromise: Promise, + query: Query, + parameters?: any, + connectionHolder?: connectionHolder.ConnectionHolder, + watermarks: { high: number, low: number } = { high: Number.MAX_VALUE, low: Number.MAX_VALUE } + ) { + this._stack = captureStacktrace() + this._streamObserverPromise = streamObserverPromise + this._p = null + this._query = query + this._parameters = parameters ?? {} + this._connectionHolder = connectionHolder ?? EMPTY_CONNECTION_HOLDER + this._keys = null + this._summary = null + this._error = null + this._watermarks = watermarks + } + + /** + * Returns a promise for the field keys. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @public + * @returns {Promise} - Field keys, in the order they will appear in records. + } + */ + keys (): Promise { + if (this._keys !== null) { + return Promise.resolve(this._keys) + } else if (this._error !== null) { + return Promise.reject(this._error) + } + return new Promise((resolve, reject) => { + this._streamObserverPromise + .then(observer => + observer.subscribe(this._decorateObserver({ + onKeys: keys => resolve(keys), + onError: err => reject(err) + })) + ) + .catch(reject) + }) + } + + /** + * Returns a promise for the result summary. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @public + * @returns {Promise} - Result summary. + * + */ + summary (): Promise { + if (this._summary !== null) { + return Promise.resolve(this._summary) + } else if (this._error !== null) { + return Promise.reject(this._error) + } + return new Promise((resolve, reject) => { + this._streamObserverPromise + .then(o => { + o.cancel() + o.subscribe(this._decorateObserver({ + onCompleted: summary => resolve(summary), + onError: err => reject(err) + })) + }) + .catch(reject) + }) + } + + /** + * Create and return new Promise + * + * @private + * @return {Promise} new Promise. + */ + private _getOrCreatePromise (): Promise { + if (this._p == null) { + this._p = new Promise((resolve, reject) => { + const records: Record[] = [] + const observer = { + onNext: (record: Record) => { + records.push(record) + }, + onCompleted: (summary: ResultSummary) => { + resolve({ records: records, summary: summary }) + }, + onError: (error: Error) => { + reject(error) + } + } + this.subscribe(observer) + }) + } + + return this._p + } + + /** + * Provides a async iterator over the records in the result. + * + * *Should not be combined with {@link Result#subscribe} or ${@link Result#then} functions.* + * + * @public + * @returns {PeekableAsyncIterator} The async iterator for the Results + */ + [Symbol.asyncIterator] (): PeekableAsyncIterator { + if (!this.isOpen()) { + const error = newError('Result is already consumed') + return { + next: () => Promise.reject(error), + peek: () => Promise.reject(error) + } + } + const state: { + paused: boolean + firstRun: boolean + finished: boolean + queuedObserver?: QueuedResultObserver + streaming?: observer.ResultStreamObserver + summary?: ResultSummary + } = { paused: true, firstRun: true, finished: false } + + const controlFlow = (): void => { + if (state.streaming == null) { + return + } + + const size = state.queuedObserver?.size ?? 0 + const queueSizeIsOverHighOrEqualWatermark = size >= this._watermarks.high + const queueSizeIsBellowOrEqualLowWatermark = size <= this._watermarks.low + + if (queueSizeIsOverHighOrEqualWatermark && !state.paused) { + state.paused = true + state.streaming.pause() + } else if ((queueSizeIsBellowOrEqualLowWatermark && state.paused) || (state.firstRun && !queueSizeIsOverHighOrEqualWatermark)) { + state.firstRun = false + state.paused = false + state.streaming.resume() + } + } + + const initializeObserver = async (): Promise => { + if (state.queuedObserver === undefined) { + state.queuedObserver = this._createQueuedResultObserver(controlFlow) + state.streaming = await this._subscribe(state.queuedObserver, true).catch(() => undefined) + controlFlow() + } + return state.queuedObserver + } + + const assertSummary = (summary: ResultSummary | undefined): summary is ResultSummary => { + if (summary === undefined) { + throw newError('InvalidState: Result stream finished without Summary', PROTOCOL_ERROR) + } + return true + } + + return { + next: async () => { + if (state.finished) { + if (assertSummary(state.summary)) { + return { done: true, value: state.summary } + } + } + const queuedObserver = await initializeObserver() + const next = await queuedObserver.dequeue() + if (next.done === true) { + state.finished = next.done + state.summary = next.value + } + return next + }, + return: async (value?: ResultSummary) => { + if (state.finished) { + if (assertSummary(state.summary)) { + return { done: true, value: value ?? state.summary } + } + } + state.streaming?.cancel() + const queuedObserver = await initializeObserver() + const last = await queuedObserver.dequeueUntilDone() + state.finished = true + last.value = value ?? last.value + state.summary = last.value as ResultSummary + return last + }, + peek: async () => { + if (state.finished) { + if (assertSummary(state.summary)) { + return { done: true, value: state.summary } + } + } + const queuedObserver = await initializeObserver() + return await queuedObserver.head() + } + } + } + + /** + * Waits for all results and calls the passed in function with the results. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @param {function(result: {records:Array, summary: ResultSummary})} onFulfilled - function to be called + * when finished. + * @param {function(error: {message:string, code:string})} onRejected - function to be called upon errors. + * @return {Promise} promise. + */ + then( + onFulfilled?: + | ((value: QueryResult) => TResult1 | PromiseLike) + | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null + ): Promise { + return this._getOrCreatePromise().then(onFulfilled, onRejected) + } + + /** + * Catch errors when using promises. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @param {function(error: Neo4jError)} onRejected - Function to be called upon errors. + * @return {Promise} promise. + */ + catch ( + onRejected?: ((reason: any) => TResult | PromiseLike) | null + ): Promise { + return this._getOrCreatePromise().catch(onRejected) + } + + /** + * Called when finally the result is done + * + * *Should not be combined with {@link Result#subscribe} function.* + * @param {function()|null} onfinally - function when the promise finished + * @return {Promise} promise. + */ + [Symbol.toStringTag]: string + finally (onfinally?: (() => void) | null): Promise { + return this._getOrCreatePromise().finally(onfinally) + } + + /** + * Stream records to observer as they come in, this is a more efficient method + * of handling the results, and allows you to handle arbitrarily large results. + * + * @param {Object} observer - Observer object + * @param {function(keys: string[])} observer.onKeys - handle stream head, the field keys. + * @param {function(record: Record)} observer.onNext - handle records, one by one. + * @param {function(summary: ResultSummary)} observer.onCompleted - handle stream tail, the result summary. + * @param {function(error: {message:string, code:string})} observer.onError - handle errors. + * @return {void} + */ + subscribe (observer: ResultObserver): void { + this._subscribe(observer) + .catch(() => {}) + } + + /** + * Check if this result is active, i.e., neither a summary nor an error has been received by the result. + * @return {boolean} `true` when neither a summary or nor an error has been received by the result. + */ + isOpen (): boolean { + return this._summary === null && this._error === null + } + + /** + * Stream records to observer as they come in, this is a more efficient method + * of handling the results, and allows you to handle arbitrarily large results. + * + * @access private + * @param {ResultObserver} observer The observer to send records to. + * @param {boolean} paused The flag to indicate if the stream should be started paused + * @returns {Promise} The result stream observer. + */ + _subscribe (observer: ResultObserver, paused: boolean = false): Promise { + const _observer = this._decorateObserver(observer) + + return this._streamObserverPromise + .then(o => { + if (paused) { + o.pause() + } + o.subscribe(_observer) + return o + }) + .catch(error => { + if (_observer.onError != null) { + _observer.onError(error) + } + return Promise.reject(error) + }) + } + + /** + * Decorates the ResultObserver with the necessary methods. + * + * @access private + * @param {ResultObserver} observer The ResultObserver to decorate. + * @returns The decorated result observer + */ + _decorateObserver (observer: ResultObserver): ResultObserver { + const onCompletedOriginal = observer.onCompleted ?? DEFAULT_ON_COMPLETED + const onErrorOriginal = observer.onError ?? DEFAULT_ON_ERROR + const onKeysOriginal = observer.onKeys ?? DEFAULT_ON_KEYS + + const onCompletedWrapper = (metadata: any): void => { + this._releaseConnectionAndGetSummary(metadata).then(summary => { + if (this._summary !== null) { + return onCompletedOriginal.call(observer, this._summary) + } + this._summary = summary + return onCompletedOriginal.call(observer, summary) + }).catch(onErrorOriginal) + } + + const onErrorWrapper = (error: Error): void => { + // notify connection holder that the used connection is not needed any more because error happened + // and result can't bee consumed any further; call the original onError callback after that + this._connectionHolder.releaseConnection().then(() => { + replaceStacktrace(error, this._stack) + this._error = error + onErrorOriginal.call(observer, error) + }).catch(onErrorOriginal) + } + + const onKeysWrapper = (keys: string[]): void => { + this._keys = keys + return onKeysOriginal.call(observer, keys) + } + + return { + onNext: (observer.onNext != null) ? observer.onNext.bind(observer) : undefined, + onKeys: onKeysWrapper, + onCompleted: onCompletedWrapper, + onError: onErrorWrapper + } + } + + /** + * Signals the stream observer that the future records should be discarded on the server. + * + * @protected + * @since 4.0.0 + * @returns {void} + */ + _cancel (): void { + if (this._summary === null && this._error === null) { + this._streamObserverPromise.then(o => o.cancel()) + .catch(() => {}) + } + } + + /** + * @access private + * @param metadata + * @returns + */ + private _releaseConnectionAndGetSummary (metadata: any): Promise { + const { + validatedQuery: query, + params: parameters + } = util.validateQueryAndParameters(this._query, this._parameters, { + skipAsserts: true + }) + const connectionHolder = this._connectionHolder + + return connectionHolder + .getConnection() + .then( + // onFulfilled: + connection => + connectionHolder + .releaseConnection() + .then(() => + connection?.protocol()?.version + ), + // onRejected: + _ => undefined + ) + .then( + protocolVersion => + new ResultSummary(query, parameters, metadata, protocolVersion) + ) + } + + /** + * @access private + */ + private _createQueuedResultObserver (onQueueSizeChanged: () => void): QueuedResultObserver { + interface ResolvablePromise { + promise: Promise + resolve: (arg: T) => any | undefined + reject: (arg: Error) => any | undefined + } + + function createResolvablePromise (): ResolvablePromise> { + const resolvablePromise: any = {} + resolvablePromise.promise = new Promise((resolve, reject) => { + resolvablePromise.resolve = resolve + resolvablePromise.reject = reject + }) + return resolvablePromise + } + + type QueuedResultElementOrError = IteratorResult | Error + + function isError (elementOrError: QueuedResultElementOrError): elementOrError is Error { + return elementOrError instanceof Error + } + + async function dequeue (): Promise> { + if (buffer.length > 0) { + const element = buffer.shift() ?? newError('Unexpected empty buffer', PROTOCOL_ERROR) + onQueueSizeChanged() + if (isError(element)) { + throw element + } + return element + } + promiseHolder.resolvable = createResolvablePromise() + return await promiseHolder.resolvable.promise + } + + const buffer: QueuedResultElementOrError[] = [] + const promiseHolder: { + resolvable: ResolvablePromise> | null + } = { resolvable: null } + + const observer = { + onNext: (record: Record) => { + observer._push({ done: false, value: record }) + }, + onCompleted: (summary: ResultSummary) => { + observer._push({ done: true, value: summary }) + }, + onError: (error: Error) => { + observer._push(error) + }, + _push (element: QueuedResultElementOrError) { + if (promiseHolder.resolvable !== null) { + const resolvable = promiseHolder.resolvable + promiseHolder.resolvable = null + if (isError(element)) { + resolvable.reject(element) + } else { + resolvable.resolve(element) + } + } else { + buffer.push(element) + onQueueSizeChanged() + } + }, + dequeue: dequeue, + dequeueUntilDone: async () => { + while (true) { + const element = await dequeue() + if (element.done === true) { + return element + } + } + }, + head: async () => { + if (buffer.length > 0) { + const element = buffer[0] + if (isError(element)) { + throw element + } + return element + } + promiseHolder.resolvable = createResolvablePromise() + try { + const element = await promiseHolder.resolvable.promise + buffer.unshift(element) + return element + } catch (error) { + buffer.unshift(error) + throw error + } finally { + onQueueSizeChanged() + } + }, + get size (): number { + return buffer.length + } + } + + return observer + } +} + +function captureStacktrace (): string | null { + const error = new Error('') + if (error.stack != null) { + return error.stack.replace(/^Error(\n\r)*/, '') // we don't need the 'Error\n' part, if only it exists + } + return null +} + +/** + * @private + * @param {Error} error The error + * @param {string| null} newStack The newStack + * @returns {void} + */ +function replaceStacktrace (error: Error, newStack?: string | null): void { + if (newStack != null) { + // Error.prototype.toString() concatenates error.name and error.message nicely + // then we add the rest of the stack trace + // eslint-disable-next-line @typescript-eslint/no-base-to-string + error.stack = error.toString() + '\n' + newStack + } +} + +export default Result +export type { QueryResult, ResultObserver } diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts new file mode 100644 index 000000000..6a419e201 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -0,0 +1,594 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/promise-function-async */ + +import { FailedObserver } from './internal/observers.ts' +import { validateQueryAndParameters } from './internal/util.ts' +import { FETCH_ALL, ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants.ts' +import { newError } from './error.ts' +import Result from './result.ts' +import Transaction from './transaction.ts' +import { ConnectionHolder } from './internal/connection-holder.ts' +import { TransactionExecutor } from './internal/transaction-executor.ts' +import { Bookmarks } from './internal/bookmarks.ts' +import { TxConfig } from './internal/tx-config.ts' +import ConnectionProvider from './connection-provider.ts' +import { Query, SessionMode } from './types.ts' +import Connection from './connection.ts' +import { NumberOrInteger } from './graph-types.ts' +import TransactionPromise from './transaction-promise.ts' +import ManagedTransaction from './transaction-managed.ts' +import BookmarkManager from './bookmark-manager.ts' + +type ConnectionConsumer = (connection: Connection | null) => any | undefined | Promise | Promise +type TransactionWork = (tx: Transaction) => Promise | T +type ManagedTransactionWork = (tx: ManagedTransaction) => Promise | T + +interface TransactionConfig { + timeout?: NumberOrInteger + metadata?: object +} + +/** + * A Session instance is used for handling the connection and + * sending queries through the connection. + * In a single session, multiple queries will be executed serially. + * In order to execute parallel queries, multiple sessions are required. + * @access public + */ +class Session { + private readonly _mode: SessionMode + private _database: string + private readonly _reactive: boolean + private readonly _fetchSize: number + private readonly _readConnectionHolder: ConnectionHolder + private readonly _writeConnectionHolder: ConnectionHolder + private _open: boolean + private _hasTx: boolean + private _lastBookmarks: Bookmarks + private _configuredBookmarks: Bookmarks + private readonly _transactionExecutor: TransactionExecutor + private readonly _impersonatedUser?: string + private _databaseNameResolved: boolean + private readonly _lowRecordWatermark: number + private readonly _highRecordWatermark: number + private readonly _results: Result[] + private readonly _bookmarkManager?: BookmarkManager + /** + * @constructor + * @protected + * @param {Object} args + * @param {string} args.mode the default access mode for this session. + * @param {ConnectionProvider} args.connectionProvider - The connection provider to acquire connections from. + * @param {Bookmarks} args.bookmarks - The initial bookmarks for this session. + * @param {string} args.database the database name + * @param {Object} args.config={} - This driver configuration. + * @param {boolean} args.reactive - Whether this session should create reactive streams + * @param {number} args.fetchSize - Defines how many records is pulled in each pulling batch + * @param {string} args.impersonatedUser - The username which the user wants to impersonate for the duration of the session. + */ + constructor ({ + mode, + connectionProvider, + bookmarks, + database, + config, + reactive, + fetchSize, + impersonatedUser, + bookmarkManager + }: { + mode: SessionMode + connectionProvider: ConnectionProvider + bookmarks?: Bookmarks + database: string + config: any + reactive: boolean + fetchSize: number + impersonatedUser?: string + bookmarkManager?: BookmarkManager + }) { + this._mode = mode + this._database = database + this._reactive = reactive + this._fetchSize = fetchSize + this._onDatabaseNameResolved = this._onDatabaseNameResolved.bind(this) + this._getConnectionAcquistionBookmarks = this._getConnectionAcquistionBookmarks.bind(this) + this._readConnectionHolder = new ConnectionHolder({ + mode: ACCESS_MODE_READ, + database, + bookmarks, + connectionProvider, + impersonatedUser, + onDatabaseNameResolved: this._onDatabaseNameResolved, + getConnectionAcquistionBookmarks: this._getConnectionAcquistionBookmarks + }) + this._writeConnectionHolder = new ConnectionHolder({ + mode: ACCESS_MODE_WRITE, + database, + bookmarks, + connectionProvider, + impersonatedUser, + onDatabaseNameResolved: this._onDatabaseNameResolved, + getConnectionAcquistionBookmarks: this._getConnectionAcquistionBookmarks + }) + this._open = true + this._hasTx = false + this._impersonatedUser = impersonatedUser + this._lastBookmarks = bookmarks ?? Bookmarks.empty() + this._configuredBookmarks = this._lastBookmarks + this._transactionExecutor = _createTransactionExecutor(config) + this._databaseNameResolved = this._database !== '' + const calculatedWatermaks = this._calculateWatermaks() + this._lowRecordWatermark = calculatedWatermaks.low + this._highRecordWatermark = calculatedWatermaks.high + this._results = [] + this._bookmarkManager = bookmarkManager + } + + /** + * Run Cypher query + * Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}` + * or with the query and parameters as separate arguments. + * + * @public + * @param {mixed} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @param {TransactionConfig} [transactionConfig] - Configuration for the new auto-commit transaction. + * @return {Result} New Result. + */ + run ( + query: Query, + parameters?: any, + transactionConfig?: TransactionConfig + ): Result { + const { validatedQuery, params } = validateQueryAndParameters( + query, + parameters + ) + const autoCommitTxConfig = (transactionConfig != null) + ? new TxConfig(transactionConfig) + : TxConfig.empty() + + const result = this._run(validatedQuery, params, async connection => { + const bookmarks = await this._bookmarks() + this._assertSessionIsOpen() + return (connection as Connection).protocol().run(validatedQuery, params, { + bookmarks, + txConfig: autoCommitTxConfig, + mode: this._mode, + database: this._database, + impersonatedUser: this._impersonatedUser, + afterComplete: (meta: any) => this._onCompleteCallback(meta, bookmarks), + reactive: this._reactive, + fetchSize: this._fetchSize, + lowRecordWatermark: this._lowRecordWatermark, + highRecordWatermark: this._highRecordWatermark + }) + }) + this._results.push(result) + return result + } + + _run ( + query: Query, + parameters: any, + customRunner: ConnectionConsumer + ): Result { + const connectionHolder = this._connectionHolderWithMode(this._mode) + + let observerPromise + if (!this._open) { + observerPromise = Promise.resolve( + new FailedObserver({ + error: newError('Cannot run query in a closed session.') + }) + ) + } else if (!this._hasTx && connectionHolder.initializeConnection()) { + observerPromise = connectionHolder + .getConnection() + .then(connection => customRunner(connection)) + .catch(error => Promise.resolve(new FailedObserver({ error }))) + } else { + observerPromise = Promise.resolve( + new FailedObserver({ + error: newError( + 'Queries cannot be run directly on a ' + + 'session with an open transaction; either run from within the ' + + 'transaction or use a different session.' + ) + }) + ) + } + const watermarks = { high: this._highRecordWatermark, low: this._lowRecordWatermark } + return new Result(observerPromise, query, parameters, connectionHolder, watermarks) + } + + _acquireConnection (connectionConsumer: ConnectionConsumer): Promise { + let promise + const connectionHolder = this._connectionHolderWithMode(this._mode) + if (!this._open) { + promise = Promise.reject( + newError('Cannot run query in a closed session.') + ) + } else if (!this._hasTx && connectionHolder.initializeConnection()) { + promise = connectionHolder + .getConnection() + .then(connection => connectionConsumer(connection)) + .then(async result => { + await connectionHolder.releaseConnection() + return result + }) + } else { + promise = Promise.reject( + newError( + 'Queries cannot be run directly on a ' + + 'session with an open transaction; either run from within the ' + + 'transaction or use a different session.' + ) + ) + } + + return promise + } + + /** + * Begin a new transaction in this session. A session can have at most one transaction running at a time, if you + * want to run multiple concurrent transactions, you should use multiple concurrent sessions. + * + * While a transaction is open the session cannot be used to run queries outside the transaction. + * + * @param {TransactionConfig} [transactionConfig] - Configuration for the new auto-commit transaction. + * @returns {TransactionPromise} New Transaction. + */ + beginTransaction (transactionConfig?: TransactionConfig): TransactionPromise { + // this function needs to support bookmarks parameter for backwards compatibility + // parameter was of type {string|string[]} and represented either a single or multiple bookmarks + // that's why we need to check parameter type and decide how to interpret the value + const arg = transactionConfig + + let txConfig = TxConfig.empty() + if (arg != null) { + txConfig = new TxConfig(arg) + } + + return this._beginTransaction(this._mode, txConfig) + } + + _beginTransaction (accessMode: SessionMode, txConfig: TxConfig): TransactionPromise { + if (!this._open) { + throw newError('Cannot begin a transaction on a closed session.') + } + if (this._hasTx) { + throw newError( + 'You cannot begin a transaction on a session with an open transaction; ' + + 'either run from within the transaction or use a different session.' + ) + } + + const mode = Session._validateSessionMode(accessMode) + const connectionHolder = this._connectionHolderWithMode(mode) + connectionHolder.initializeConnection() + this._hasTx = true + + const tx = new TransactionPromise({ + connectionHolder, + impersonatedUser: this._impersonatedUser, + onClose: this._transactionClosed.bind(this), + onBookmarks: (newBm, oldBm, db) => this._updateBookmarks(newBm, oldBm, db), + onConnection: this._assertSessionIsOpen.bind(this), + reactive: this._reactive, + fetchSize: this._fetchSize, + lowRecordWatermark: this._lowRecordWatermark, + highRecordWatermark: this._highRecordWatermark + }) + tx._begin(() => this._bookmarks(), txConfig) + return tx + } + + /** + * @private + * @returns {void} + */ + _assertSessionIsOpen (): void { + if (!this._open) { + throw newError('You cannot run more transactions on a closed session.') + } + } + + /** + * @private + * @returns {void} + */ + _transactionClosed (): void { + this._hasTx = false + } + + /** + * Return the bookmarks received following the last completed {@link Transaction}. + * + * @deprecated This method will be removed in version 6.0. Please, use {@link Session#lastBookmarks} instead. + * + * @return {string[]} A reference to a previous transaction. + */ + lastBookmark (): string[] { + return this.lastBookmarks() + } + + /** + * Return the bookmarks received following the last completed {@link Transaction}. + * + * @return {string[]} A reference to a previous transaction. + */ + lastBookmarks (): string[] { + return this._lastBookmarks.values() + } + + private async _bookmarks (): Promise { + const bookmarks = await this._bookmarkManager?.getAllBookmarks() + if (bookmarks === undefined) { + return this._lastBookmarks + } + return new Bookmarks([...bookmarks, ...this._configuredBookmarks]) + } + + /** + * Execute given unit of work in a {@link READ} transaction. + * + * Transaction will automatically be committed unless the given function throws or returns a rejected promise. + * Some failures of the given function or the commit itself will be retried with exponential backoff with initial + * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's + * `maxTransactionRetryTime` property in milliseconds. + * + * @deprecated This method will be removed in version 6.0. Please, use {@link Session#executeRead} instead. + * + * @param {function(tx: Transaction): Promise} transactionWork - Callback that executes operations against + * a given {@link Transaction}. + * @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work. + * @return {Promise} Resolved promise as returned by the given function or rejected promise when given + * function or commit fails. + */ + readTransaction( + transactionWork: TransactionWork, + transactionConfig?: TransactionConfig + ): Promise { + const config = new TxConfig(transactionConfig) + return this._runTransaction(ACCESS_MODE_READ, config, transactionWork) + } + + /** + * Execute given unit of work in a {@link WRITE} transaction. + * + * Transaction will automatically be committed unless the given function throws or returns a rejected promise. + * Some failures of the given function or the commit itself will be retried with exponential backoff with initial + * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's + * `maxTransactionRetryTime` property in milliseconds. + * + * @deprecated This method will be removed in version 6.0. Please, use {@link Session#executeWrite} instead. + * + * @param {function(tx: Transaction): Promise} transactionWork - Callback that executes operations against + * a given {@link Transaction}. + * @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work. + * @return {Promise} Resolved promise as returned by the given function or rejected promise when given + * function or commit fails. + */ + writeTransaction( + transactionWork: TransactionWork, + transactionConfig?: TransactionConfig + ): Promise { + const config = new TxConfig(transactionConfig) + return this._runTransaction(ACCESS_MODE_WRITE, config, transactionWork) + } + + _runTransaction( + accessMode: SessionMode, + transactionConfig: TxConfig, + transactionWork: TransactionWork + ): Promise { + return this._transactionExecutor.execute( + () => this._beginTransaction(accessMode, transactionConfig), + transactionWork + ) + } + + /** + * Execute given unit of work in a {@link READ} transaction. + * + * Transaction will automatically be committed unless the given function throws or returns a rejected promise. + * Some failures of the given function or the commit itself will be retried with exponential backoff with initial + * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's + * `maxTransactionRetryTime` property in milliseconds. + * + * @param {function(tx: ManagedTransaction): Promise} transactionWork - Callback that executes operations against + * a given {@link Transaction}. + * @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work. + * @return {Promise} Resolved promise as returned by the given function or rejected promise when given + * function or commit fails. + */ + executeRead( + transactionWork: ManagedTransactionWork, + transactionConfig?: TransactionConfig + ): Promise { + const config = new TxConfig(transactionConfig) + return this._executeInTransaction(ACCESS_MODE_READ, config, transactionWork) + } + + /** + * Execute given unit of work in a {@link WRITE} transaction. + * + * Transaction will automatically be committed unless the given function throws or returns a rejected promise. + * Some failures of the given function or the commit itself will be retried with exponential backoff with initial + * delay of 1 second and maximum retry time of 30 seconds. Maximum retry time is configurable via driver config's + * `maxTransactionRetryTime` property in milliseconds. + * + * @param {function(tx: ManagedTransaction): Promise} transactionWork - Callback that executes operations against + * a given {@link Transaction}. + * @param {TransactionConfig} [transactionConfig] - Configuration for all transactions started to execute the unit of work. + * @return {Promise} Resolved promise as returned by the given function or rejected promise when given + * function or commit fails. + */ + executeWrite( + transactionWork: ManagedTransactionWork, + transactionConfig?: TransactionConfig + ): Promise { + const config = new TxConfig(transactionConfig) + return this._executeInTransaction(ACCESS_MODE_WRITE, config, transactionWork) + } + + /** + * @private + * @param {SessionMode} accessMode + * @param {TxConfig} transactionConfig + * @param {ManagedTransactionWork} transactionWork + * @returns {Promise} + */ + private _executeInTransaction( + accessMode: SessionMode, + transactionConfig: TxConfig, + transactionWork: ManagedTransactionWork + ): Promise { + return this._transactionExecutor.execute( + () => this._beginTransaction(accessMode, transactionConfig), + transactionWork, + ManagedTransaction.fromTransaction + ) + } + + /** + * Sets the resolved database name in the session context. + * @private + * @param {string|undefined} database The resolved database name + * @returns {void} + */ + _onDatabaseNameResolved (database?: string): void { + if (!this._databaseNameResolved) { + const normalizedDatabase = database ?? '' + this._database = normalizedDatabase + this._readConnectionHolder.setDatabase(normalizedDatabase) + this._writeConnectionHolder.setDatabase(normalizedDatabase) + this._databaseNameResolved = true + } + } + + private async _getConnectionAcquistionBookmarks (): Promise { + const bookmarks = await this._bookmarkManager?.getBookmarks('system') + if (bookmarks === undefined) { + return this._lastBookmarks + } + return new Bookmarks([...this._configuredBookmarks, ...bookmarks]) + } + + /** + * Update value of the last bookmarks. + * @private + * @param {Bookmarks} newBookmarks - The new bookmarks. + * @returns {void} + */ + _updateBookmarks (newBookmarks?: Bookmarks, previousBookmarks?: Bookmarks, database?: string): void { + if ((newBookmarks != null) && !newBookmarks.isEmpty()) { + this._bookmarkManager?.updateBookmarks( + database ?? this._database, + previousBookmarks?.values() ?? [], + newBookmarks?.values() ?? [] + ) + this._lastBookmarks = newBookmarks + this._configuredBookmarks = Bookmarks.empty() + } + } + + /** + * Close this session. + * @return {Promise} + */ + async close (): Promise { + if (this._open) { + this._open = false + + this._results.forEach(result => result._cancel()) + + this._transactionExecutor.close() + + await this._readConnectionHolder.close(this._hasTx) + await this._writeConnectionHolder.close(this._hasTx) + } + } + + _connectionHolderWithMode (mode: SessionMode): ConnectionHolder { + if (mode === ACCESS_MODE_READ) { + return this._readConnectionHolder + } else if (mode === ACCESS_MODE_WRITE) { + return this._writeConnectionHolder + } else { + throw newError('Unknown access mode: ' + (mode as string)) + } + } + + /** + * @private + * @param {Object} meta Connection metadatada + * @returns {void} + */ + _onCompleteCallback (meta: { bookmark: string | string[], db?: string }, previousBookmarks?: Bookmarks): void { + this._updateBookmarks(new Bookmarks(meta.bookmark), previousBookmarks, meta.db) + } + + /** + * @private + * @returns {void} + */ + private _calculateWatermaks (): { low: number, high: number } { + if (this._fetchSize === FETCH_ALL) { + return { + low: Number.MAX_VALUE, // we shall always lower than this number to enable auto pull + high: Number.MAX_VALUE // we shall never reach this number to disable auto pull + } + } + return { + low: 0.3 * this._fetchSize, + high: 0.7 * this._fetchSize + } + } + + /** + * @protected + */ + static _validateSessionMode (rawMode?: SessionMode): SessionMode { + const mode: string = rawMode ?? ACCESS_MODE_WRITE + if (mode !== ACCESS_MODE_READ && mode !== ACCESS_MODE_WRITE) { + throw newError('Illegal session mode ' + mode) + } + return mode as SessionMode + } +} + +/** + * @private + * @param {object} config + * @returns {TransactionExecutor} The transaction executor + */ +function _createTransactionExecutor (config?: { + maxTransactionRetryTime: number | null +}): TransactionExecutor { + const maxRetryTimeMs = config?.maxTransactionRetryTime ?? null + return new TransactionExecutor(maxRetryTimeMs) +} + +export default Session +export type { TransactionConfig } diff --git a/packages/neo4j-driver-deno/lib/core/spatial-types.ts b/packages/neo4j-driver-deno/lib/core/spatial-types.ts new file mode 100644 index 000000000..f0911dafc --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/spatial-types.ts @@ -0,0 +1,98 @@ +/** + * 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 { assertNumber, assertNumberOrInteger } from './internal/util.ts' +import { NumberOrInteger } from './graph-types.ts' +import Integer from './integer.ts' + +const POINT_IDENTIFIER_PROPERTY = '__isPoint__' + +/** + * Represents a single two or three-dimensional point in a particular coordinate reference system. + * Created `Point` objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class Point { + readonly srid: T + readonly x: number + readonly y: number + readonly z: number | undefined + + /** + * @constructor + * @param {T} srid - The coordinate reference system identifier. + * @param {number} x - The `x` coordinate of the point. + * @param {number} y - The `y` coordinate of the point. + * @param {number} [z=undefined] - The `z` coordinate of the point or `undefined` if point has 2 dimensions. + */ + constructor (srid: T, x: number, y: number, z?: number) { + /** + * The coordinate reference system identifier. + * @type {T} + */ + this.srid = assertNumberOrInteger(srid, 'SRID') as T + /** + * The `x` coordinate of the point. + * @type {number} + */ + this.x = assertNumber(x, 'X coordinate') + /** + * The `y` coordinate of the point. + * @type {number} + */ + this.y = assertNumber(y, 'Y coordinate') + /** + * The `z` coordinate of the point or `undefined` if point is 2-dimensional. + * @type {number} + */ + this.z = z === null || z === undefined ? z : assertNumber(z, 'Z coordinate') + Object.freeze(this) + } + + /** + * @ignore + */ + toString (): string { + return this.z != null && !isNaN(this.z) + ? `Point{srid=${formatAsFloat(this.srid)}, x=${formatAsFloat( + this.x + )}, y=${formatAsFloat(this.y)}, z=${formatAsFloat(this.z)}}` + : `Point{srid=${formatAsFloat(this.srid)}, x=${formatAsFloat( + this.x + )}, y=${formatAsFloat(this.y)}}` + } +} + +function formatAsFloat (number: NumberOrInteger): string { + return Number.isInteger(number) ? number.toString() + '.0' : number.toString() +} + +Object.defineProperty(Point.prototype, POINT_IDENTIFIER_PROPERTY, { + value: true, + enumerable: false, + configurable: false, + writable: false +}) + +/** + * Test if given object is an instance of {@link Point} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Point}, `false` otherwise. + */ +export function isPoint (obj?: any): obj is Point { + return obj != null && obj[POINT_IDENTIFIER_PROPERTY] === true +} diff --git a/packages/neo4j-driver-deno/lib/core/temporal-types.ts b/packages/neo4j-driver-deno/lib/core/temporal-types.ts new file mode 100644 index 000000000..7e79de0a2 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/temporal-types.ts @@ -0,0 +1,809 @@ +/** + * 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 util from './internal/temporal-util.ts' +import { NumberOrInteger, StandardDate } from './graph-types.ts' +import { + assertNumberOrInteger, + assertString, + assertValidDate +} from './internal/util.ts' +import { newError } from './error.ts' +import Integer, { int, toNumber } from './integer.ts' + +const IDENTIFIER_PROPERTY_ATTRIBUTES = { + value: true, + enumerable: false, + configurable: false, + writable: false +} + +const DURATION_IDENTIFIER_PROPERTY: string = '__isDuration__' +const LOCAL_TIME_IDENTIFIER_PROPERTY: string = '__isLocalTime__' +const TIME_IDENTIFIER_PROPERTY: string = '__isTime__' +const DATE_IDENTIFIER_PROPERTY: string = '__isDate__' +const LOCAL_DATE_TIME_IDENTIFIER_PROPERTY: string = '__isLocalDateTime__' +const DATE_TIME_IDENTIFIER_PROPERTY: string = '__isDateTime__' + +/** + * Represents an ISO 8601 duration. Contains both date-based values (years, months, days) and time-based values (seconds, nanoseconds). + * Created `Duration` objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class Duration { + readonly months: T + readonly days: T + readonly seconds: T + readonly nanoseconds: T + + /** + * @constructor + * @param {NumberOrInteger} months - The number of months for the new duration. + * @param {NumberOrInteger} days - The number of days for the new duration. + * @param {NumberOrInteger} seconds - The number of seconds for the new duration. + * @param {NumberOrInteger} nanoseconds - The number of nanoseconds for the new duration. + */ + constructor (months: T, days: T, seconds: T, nanoseconds: T) { + /** + * The number of months. + * @type {NumberOrInteger} + */ + this.months = assertNumberOrInteger(months, 'Months') as T + /** + * The number of days. + * @type {NumberOrInteger} + */ + this.days = assertNumberOrInteger(days, 'Days') as T + assertNumberOrInteger(seconds, 'Seconds') + assertNumberOrInteger(nanoseconds, 'Nanoseconds') + /** + * The number of seconds. + * @type {NumberOrInteger} + */ + this.seconds = util.normalizeSecondsForDuration(seconds, nanoseconds) as T + /** + * The number of nanoseconds. + * @type {NumberOrInteger} + */ + this.nanoseconds = util.normalizeNanosecondsForDuration(nanoseconds) as T + Object.freeze(this) + } + + /** + * @ignore + */ + toString (): string { + return util.durationToIsoString( + this.months, + this.days, + this.seconds, + this.nanoseconds + ) + } +} + +Object.defineProperty( + Duration.prototype, + DURATION_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Duration} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Duration}, `false` otherwise. + */ +export function isDuration (obj: object): obj is Duration { + return hasIdentifierProperty(obj, DURATION_IDENTIFIER_PROPERTY) +} + +/** + * Represents an instant capturing the time of day, but not the date, nor the timezone. + * Created {@link LocalTime} objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class LocalTime { + readonly hour: T + readonly minute: T + readonly second: T + readonly nanosecond: T + /** + * @constructor + * @param {NumberOrInteger} hour - The hour for the new local time. + * @param {NumberOrInteger} minute - The minute for the new local time. + * @param {NumberOrInteger} second - The second for the new local time. + * @param {NumberOrInteger} nanosecond - The nanosecond for the new local time. + */ + constructor (hour: T, minute: T, second: T, nanosecond: T) { + /** + * The hour. + * @type {NumberOrInteger} + */ + this.hour = util.assertValidHour(hour) as T + /** + * The minute. + * @type {NumberOrInteger} + */ + this.minute = util.assertValidMinute(minute) as T + /** + * The second. + * @type {NumberOrInteger} + */ + this.second = util.assertValidSecond(second) as T + /** + * The nanosecond. + * @type {NumberOrInteger} + */ + this.nanosecond = util.assertValidNanosecond(nanosecond) as T + Object.freeze(this) + } + + /** + * Create a {@link LocalTime} object from the given standard JavaScript `Date` and optional nanoseconds. + * Year, month, day and time zone offset components of the given date are ignored. + * @param {global.Date} standardDate - The standard JavaScript date to convert. + * @param {NumberOrInteger|undefined} nanosecond - The optional amount of nanoseconds. + * @return {LocalTime} New LocalTime. + */ + static fromStandardDate ( + standardDate: StandardDate, + nanosecond?: NumberOrInteger + ): LocalTime { + verifyStandardDateAndNanos(standardDate, nanosecond) + + const totalNanoseconds: number | Integer | bigint = util.totalNanoseconds( + standardDate, + nanosecond + ) + + return new LocalTime( + standardDate.getHours(), + standardDate.getMinutes(), + standardDate.getSeconds(), + totalNanoseconds instanceof Integer + ? totalNanoseconds.toInt() + : typeof totalNanoseconds === 'bigint' + ? int(totalNanoseconds).toInt() + : totalNanoseconds + ) + } + + /** + * @ignore + */ + toString (): string { + return util.timeToIsoString( + this.hour, + this.minute, + this.second, + this.nanosecond + ) + } +} + +Object.defineProperty( + LocalTime.prototype, + LOCAL_TIME_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link LocalTime} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link LocalTime}, `false` otherwise. + */ +export function isLocalTime (obj: object): boolean { + return hasIdentifierProperty(obj, LOCAL_TIME_IDENTIFIER_PROPERTY) +} + +/** + * Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date. + * Created {@link Time} objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class Time { + readonly hour: T + readonly minute: T + readonly second: T + readonly nanosecond: T + readonly timeZoneOffsetSeconds: T + /** + * @constructor + * @param {NumberOrInteger} hour - The hour for the new local time. + * @param {NumberOrInteger} minute - The minute for the new local time. + * @param {NumberOrInteger} second - The second for the new local time. + * @param {NumberOrInteger} nanosecond - The nanosecond for the new local time. + * @param {NumberOrInteger} timeZoneOffsetSeconds - The time zone offset in seconds. Value represents the difference, in seconds, from UTC to local time. + * This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC. + */ + constructor ( + hour: T, + minute: T, + second: T, + nanosecond: T, + timeZoneOffsetSeconds: T + ) { + /** + * The hour. + * @type {NumberOrInteger} + */ + this.hour = util.assertValidHour(hour) as T + /** + * The minute. + * @type {NumberOrInteger} + */ + this.minute = util.assertValidMinute(minute) as T + /** + * The second. + * @type {NumberOrInteger} + */ + this.second = util.assertValidSecond(second) as T + /** + * The nanosecond. + * @type {NumberOrInteger} + */ + this.nanosecond = util.assertValidNanosecond(nanosecond) as T + /** + * The time zone offset in seconds. + * @type {NumberOrInteger} + */ + this.timeZoneOffsetSeconds = assertNumberOrInteger( + timeZoneOffsetSeconds, + 'Time zone offset in seconds' + ) as T + Object.freeze(this) + } + + /** + * Create a {@link Time} object from the given standard JavaScript `Date` and optional nanoseconds. + * Year, month and day components of the given date are ignored. + * @param {global.Date} standardDate - The standard JavaScript date to convert. + * @param {NumberOrInteger|undefined} nanosecond - The optional amount of nanoseconds. + * @return {Time} New Time. + */ + static fromStandardDate ( + standardDate: StandardDate, + nanosecond?: NumberOrInteger + ): Time { + verifyStandardDateAndNanos(standardDate, nanosecond) + + return new Time( + standardDate.getHours(), + standardDate.getMinutes(), + standardDate.getSeconds(), + toNumber(util.totalNanoseconds(standardDate, nanosecond)), + util.timeZoneOffsetInSeconds(standardDate) + ) + } + + /** + * @ignore + */ + toString (): string { + return ( + util.timeToIsoString( + this.hour, + this.minute, + this.second, + this.nanosecond + ) + util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds) + ) + } +} + +Object.defineProperty( + Time.prototype, + TIME_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Time} class. + * @param {Object} obj the object to test. + * @return {boolean} `true` if given object is a {@link Time}, `false` otherwise. + */ +export function isTime (obj: object): obj is Time { + return hasIdentifierProperty(obj, TIME_IDENTIFIER_PROPERTY) +} + +/** + * Represents an instant capturing the date, but not the time, nor the timezone. + * Created {@link Date} objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class Date { + readonly year: T + readonly month: T + readonly day: T + /** + * @constructor + * @param {NumberOrInteger} year - The year for the new local date. + * @param {NumberOrInteger} month - The month for the new local date. + * @param {NumberOrInteger} day - The day for the new local date. + */ + constructor (year: T, month: T, day: T) { + /** + * The year. + * @type {NumberOrInteger} + */ + this.year = util.assertValidYear(year) as T + /** + * The month. + * @type {NumberOrInteger} + */ + this.month = util.assertValidMonth(month) as T + /** + * The day. + * @type {NumberOrInteger} + */ + this.day = util.assertValidDay(day) as T + Object.freeze(this) + } + + /** + * Create a {@link Date} object from the given standard JavaScript `Date`. + * Hour, minute, second, millisecond and time zone offset components of the given date are ignored. + * @param {global.Date} standardDate - The standard JavaScript date to convert. + * @return {Date} New Date. + */ + static fromStandardDate (standardDate: StandardDate): Date { + verifyStandardDateAndNanos(standardDate) + + return new Date( + standardDate.getFullYear(), + standardDate.getMonth() + 1, + standardDate.getDate() + ) + } + + /** + * Convert date to standard JavaScript `Date`. + * + * The time component of the returned `Date` is set to midnight + * and the time zone is set to UTC. + * + * @returns {StandardDate} Standard JavaScript `Date` at `00:00:00.000` UTC. + */ + toStandardDate (): StandardDate { + return util.isoStringToStandardDate(this.toString()) + } + + /** + * @ignore + */ + toString (): string { + return util.dateToIsoString(this.year, this.month, this.day) + } +} + +Object.defineProperty( + Date.prototype, + DATE_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link Date} class. + * @param {Object} obj - The object to test. + * @return {boolean} `true` if given object is a {@link Date}, `false` otherwise. + */ +export function isDate (obj: object): boolean { + return hasIdentifierProperty(obj, DATE_IDENTIFIER_PROPERTY) +} + +/** + * Represents an instant capturing the date and the time, but not the timezone. + * Created {@link LocalDateTime} objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class LocalDateTime { + readonly year: T + readonly month: T + readonly day: T + readonly hour: T + readonly minute: T + readonly second: T + readonly nanosecond: T + /** + * @constructor + * @param {NumberOrInteger} year - The year for the new local date. + * @param {NumberOrInteger} month - The month for the new local date. + * @param {NumberOrInteger} day - The day for the new local date. + * @param {NumberOrInteger} hour - The hour for the new local time. + * @param {NumberOrInteger} minute - The minute for the new local time. + * @param {NumberOrInteger} second - The second for the new local time. + * @param {NumberOrInteger} nanosecond - The nanosecond for the new local time. + */ + constructor ( + year: T, + month: T, + day: T, + hour: T, + minute: T, + second: T, + nanosecond: T + ) { + /** + * The year. + * @type {NumberOrInteger} + */ + this.year = util.assertValidYear(year) as T + /** + * The month. + * @type {NumberOrInteger} + */ + this.month = util.assertValidMonth(month) as T + /** + * The day. + * @type {NumberOrInteger} + */ + this.day = util.assertValidDay(day) as T + /** + * The hour. + * @type {NumberOrInteger} + */ + this.hour = util.assertValidHour(hour) as T + /** + * The minute. + * @type {NumberOrInteger} + */ + this.minute = util.assertValidMinute(minute) as T + /** + * The second. + * @type {NumberOrInteger} + */ + this.second = util.assertValidSecond(second) as T + /** + * The nanosecond. + * @type {NumberOrInteger} + */ + this.nanosecond = util.assertValidNanosecond(nanosecond) as T + Object.freeze(this) + } + + /** + * Create a {@link LocalDateTime} object from the given standard JavaScript `Date` and optional nanoseconds. + * Time zone offset component of the given date is ignored. + * @param {global.Date} standardDate - The standard JavaScript date to convert. + * @param {NumberOrInteger|undefined} nanosecond - The optional amount of nanoseconds. + * @return {LocalDateTime} New LocalDateTime. + */ + static fromStandardDate ( + standardDate: StandardDate, + nanosecond?: NumberOrInteger + ): LocalDateTime { + verifyStandardDateAndNanos(standardDate, nanosecond) + + return new LocalDateTime( + standardDate.getFullYear(), + standardDate.getMonth() + 1, + standardDate.getDate(), + standardDate.getHours(), + standardDate.getMinutes(), + standardDate.getSeconds(), + toNumber(util.totalNanoseconds(standardDate, nanosecond)) + ) + } + + /** + * Convert date to standard JavaScript `Date`. + * + * @returns {StandardDate} Standard JavaScript `Date` at the local timezone + */ + toStandardDate (): StandardDate { + return util.isoStringToStandardDate(this.toString()) + } + + /** + * @ignore + */ + toString (): string { + return localDateTimeToString( + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + this.nanosecond + ) + } +} + +Object.defineProperty( + LocalDateTime.prototype, + LOCAL_DATE_TIME_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link LocalDateTime} class. + * @param {Object} obj - The object to test. + * @return {boolean} `true` if given object is a {@link LocalDateTime}, `false` otherwise. + */ +export function isLocalDateTime (obj: any): obj is LocalDateTime { + return hasIdentifierProperty(obj, LOCAL_DATE_TIME_IDENTIFIER_PROPERTY) +} + +/** + * Represents an instant capturing the date, the time and the timezone identifier. + * Created {@ DateTime} objects are frozen with `Object.freeze()` in constructor and thus immutable. + */ +export class DateTime { + readonly year: T + readonly month: T + readonly day: T + readonly hour: T + readonly minute: T + readonly second: T + readonly nanosecond: T + readonly timeZoneOffsetSeconds?: T + readonly timeZoneId?: string + /** + * @constructor + * @param {NumberOrInteger} year - The year for the new date-time. + * @param {NumberOrInteger} month - The month for the new date-time. + * @param {NumberOrInteger} day - The day for the new date-time. + * @param {NumberOrInteger} hour - The hour for the new date-time. + * @param {NumberOrInteger} minute - The minute for the new date-time. + * @param {NumberOrInteger} second - The second for the new date-time. + * @param {NumberOrInteger} nanosecond - The nanosecond for the new date-time. + * @param {NumberOrInteger} timeZoneOffsetSeconds - The time zone offset in seconds. Either this argument or `timeZoneId` should be defined. + * Value represents the difference, in seconds, from UTC to local time. + * This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC. + * @param {string|null} timeZoneId - The time zone id for the new date-time. Either this argument or `timeZoneOffsetSeconds` should be defined. + */ + constructor ( + year: T, + month: T, + day: T, + hour: T, + minute: T, + second: T, + nanosecond: T, + timeZoneOffsetSeconds?: T, + timeZoneId?: string | null + ) { + /** + * The year. + * @type {NumberOrInteger} + */ + this.year = util.assertValidYear(year) as T + /** + * The month. + * @type {NumberOrInteger} + */ + this.month = util.assertValidMonth(month) as T + /** + * The day. + * @type {NumberOrInteger} + */ + this.day = util.assertValidDay(day) as T + /** + * The hour. + * @type {NumberOrInteger} + */ + this.hour = util.assertValidHour(hour) as T + /** + * The minute. + * @type {NumberOrInteger} + */ + this.minute = util.assertValidMinute(minute) as T + /** + * The second. + * @type {NumberOrInteger} + */ + this.second = util.assertValidSecond(second) as T + /** + * The nanosecond. + * @type {NumberOrInteger} + */ + this.nanosecond = util.assertValidNanosecond(nanosecond) as T + + const [offset, id] = verifyTimeZoneArguments( + timeZoneOffsetSeconds, + timeZoneId + ) + /** + * The time zone offset in seconds. + * + * *Either this or {@link timeZoneId} is defined.* + * + * @type {NumberOrInteger} + */ + this.timeZoneOffsetSeconds = offset as T + /** + * The time zone id. + * + * *Either this or {@link timeZoneOffsetSeconds} is defined.* + * + * @type {string} + */ + this.timeZoneId = id ?? undefined + + Object.freeze(this) + } + + /** + * Create a {@link DateTime} object from the given standard JavaScript `Date` and optional nanoseconds. + * @param {global.Date} standardDate - The standard JavaScript date to convert. + * @param {NumberOrInteger|undefined} nanosecond - The optional amount of nanoseconds. + * @return {DateTime} New DateTime. + */ + static fromStandardDate ( + standardDate: StandardDate, + nanosecond?: NumberOrInteger + ): DateTime { + verifyStandardDateAndNanos(standardDate, nanosecond) + + return new DateTime( + standardDate.getFullYear(), + standardDate.getMonth() + 1, + standardDate.getDate(), + standardDate.getHours(), + standardDate.getMinutes(), + standardDate.getSeconds(), + toNumber(util.totalNanoseconds(standardDate, nanosecond)), + util.timeZoneOffsetInSeconds(standardDate), + null /* no time zone id */ + ) + } + + /** + * Convert date to standard JavaScript `Date`. + * + * @returns {StandardDate} Standard JavaScript `Date` at the defined time zone offset + * @throws {Error} If the time zone offset is not defined in the object. + */ + toStandardDate (): StandardDate { + return util.toStandardDate(this._toUTC()) + } + + /** + * @ignore + */ + toString (): string { + const localDateTimeStr = localDateTimeToString( + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + this.nanosecond + ) + + const timeOffset = this.timeZoneOffsetSeconds != null + ? util.timeZoneOffsetToIsoString(this.timeZoneOffsetSeconds ?? 0) + : '' + + const timeZoneStr = this.timeZoneId != null + ? `[${this.timeZoneId}]` + : '' + + return localDateTimeStr + timeOffset + timeZoneStr + } + + /** + * @private + * @returns {number} + */ + private _toUTC (): number { + if (this.timeZoneOffsetSeconds === undefined) { + throw new Error('Requires DateTime created with time zone offset') + } + const epochSecond = util.localDateTimeToEpochSecond( + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + this.nanosecond + ) + + const utcSecond = epochSecond.subtract(this.timeZoneOffsetSeconds ?? 0) + + return int(utcSecond) + .multiply(1000) + .add(int(this.nanosecond).div(1_000_000)) + .toNumber() + } +} + +Object.defineProperty( + DateTime.prototype, + DATE_TIME_IDENTIFIER_PROPERTY, + IDENTIFIER_PROPERTY_ATTRIBUTES +) + +/** + * Test if given object is an instance of {@link DateTime} class. + * @param {Object} obj - The object to test. + * @return {boolean} `true` if given object is a {@link DateTime}, `false` otherwise. + */ +export function isDateTime (obj: object): boolean { + return hasIdentifierProperty(obj, DATE_TIME_IDENTIFIER_PROPERTY) +} + +function hasIdentifierProperty (obj: any, property: string): boolean { + return obj != null && obj[property] === true +} + +function localDateTimeToString ( + year: NumberOrInteger, + month: NumberOrInteger, + day: NumberOrInteger, + hour: NumberOrInteger, + minute: NumberOrInteger, + second: NumberOrInteger, + nanosecond: NumberOrInteger +): string { + return ( + util.dateToIsoString(year, month, day) + + 'T' + + util.timeToIsoString(hour, minute, second, nanosecond) + ) +} + +/** + * @private + * @param {NumberOrInteger} timeZoneOffsetSeconds + * @param {string | null } timeZoneId + * @returns {Array} + */ +function verifyTimeZoneArguments ( + timeZoneOffsetSeconds?: NumberOrInteger, + timeZoneId?: string | null +): [NumberOrInteger | undefined | null, string | undefined | null] { + const offsetDefined = timeZoneOffsetSeconds !== null && timeZoneOffsetSeconds !== undefined + const idDefined = timeZoneId !== null && timeZoneId !== undefined && timeZoneId !== '' + + 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') + util.assertValidZoneId('Time zone ID', timeZoneId) + result[1] = timeZoneId + } + + return result +} + +/** + * @private + * @param {StandardDate} standardDate + * @param {NumberOrInteger} nanosecond + * @returns {void} + */ +function verifyStandardDateAndNanos ( + standardDate: StandardDate, + nanosecond?: NumberOrInteger +): void { + assertValidDate(standardDate, 'Standard date') + if (nanosecond !== null && nanosecond !== undefined) { + assertNumberOrInteger(nanosecond, 'Nanosecond') + } +} diff --git a/packages/neo4j-driver-deno/lib/core/transaction-managed.ts b/packages/neo4j-driver-deno/lib/core/transaction-managed.ts new file mode 100644 index 000000000..162af33c9 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/transaction-managed.ts @@ -0,0 +1,68 @@ +/** + * 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 Result from './result.ts' +import Transaction from './transaction.ts' +import { Query } from './types.ts' + +type Run = (query: Query, parameters?: any) => Result + +/** + * Represents a transaction that is managed by the transaction executor. + * + * @public + */ +class ManagedTransaction { + private readonly _run: Run + + /** + * @private + */ + private constructor ({ run }: { run: Run }) { + /** + * @private + */ + this._run = run + } + + /** + * @private + * @param {Transaction} tx - Transaction to wrap + * @returns {ManagedTransaction} the ManagedTransaction + */ + static fromTransaction (tx: Transaction): ManagedTransaction { + return new ManagedTransaction({ + run: tx.run.bind(tx) + }) + } + + /** + * Run Cypher query + * Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}` + * or with the query and parameters as separate arguments. + * @param {mixed} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @return {Result} New Result + */ + run (query: Query, parameters?: any): Result { + return this._run(query, parameters) + } +} + +export default ManagedTransaction diff --git a/packages/neo4j-driver-deno/lib/core/transaction-promise.ts b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts new file mode 100644 index 000000000..157588735 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/transaction-promise.ts @@ -0,0 +1,195 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/promise-function-async */ + +import Transaction from './transaction.ts' +import { + ConnectionHolder +} from './internal/connection-holder.ts' + +import { Bookmarks } from './internal/bookmarks.ts' +import { TxConfig } from './internal/tx-config.ts' + +/** + * Represents a {@link Promise} object and a {@link Transaction} object. + * + * Resolving this object promise verifies the result of the transaction begin and returns the {@link Transaction} object in case of success. + * + * The object can still also used as {@link Transaction} for convenience. The result of begin will be checked + * during the next API calls in the object as it is in the transaction. + * + * @access public + */ +class TransactionPromise extends Transaction implements Promise { + [Symbol.toStringTag]: string = 'TransactionPromise' + private _beginError?: Error + private _beginMetadata?: any + private _beginPromise?: Promise + private _reject?: (error: Error) => void + private _resolve?: (value: Transaction | PromiseLike) => void + + /** + * @constructor + * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. + * @param {function()} onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. + * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the connection + * is not yet released. + * @param {boolean} reactive whether this transaction generates reactive streams + * @param {number} fetchSize - the record fetch size in each pulling batch. + * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. + */ + constructor ({ + connectionHolder, + onClose, + onBookmarks, + onConnection, + reactive, + fetchSize, + impersonatedUser, + highRecordWatermark, + lowRecordWatermark + }: { + connectionHolder: ConnectionHolder + onClose: () => void + onBookmarks: (newBookmarks: Bookmarks, previousBookmarks: Bookmarks, database?: string) => void + onConnection: () => void + reactive: boolean + fetchSize: number + impersonatedUser?: string + highRecordWatermark: number + lowRecordWatermark: number + }) { + super({ + connectionHolder, + onClose, + onBookmarks, + onConnection, + reactive, + fetchSize, + impersonatedUser, + highRecordWatermark, + lowRecordWatermark + }) + } + + /** + * Waits for the begin to complete. + * + * @param {function(transaction: Transaction)} onFulfilled - function to be called when finished. + * @param {function(error: {message:string, code:string})} onRejected - function to be called upon errors. + * @return {Promise} promise. + */ + then( + onfulfilled?: + ((value: Transaction) => TResult1 | PromiseLike) + | null, + onrejected?: + ((reason: any) => TResult2 | PromiseLike) + | null + ): Promise { + return this._getOrCreateBeginPromise().then(onfulfilled, onrejected) + } + + /** + * Catch errors when using promises. + * + * @param {function(error: Neo4jError)} onRejected - Function to be called upon errors. + * @return {Promise} promise. + */ + catch (onrejected?: ((reason: any) => TResult | PromiseLike) | null): Promise { + return this._getOrCreateBeginPromise().catch(onrejected) + } + + /** + * Called when finally the begin is done + * + * @param {function()|null} onfinally - function when the promise finished + * @return {Promise} promise. + */ + finally (onfinally?: (() => void) | null): Promise { + return this._getOrCreateBeginPromise().finally(onfinally) + } + + private _getOrCreateBeginPromise (): Promise { + if (this._beginPromise == null) { + this._beginPromise = new Promise((resolve, reject) => { + this._resolve = resolve + this._reject = reject + if (this._beginError != null) { + reject(this._beginError) + } + if (this._beginMetadata != null) { + resolve(this._toTransaction()) + } + }) + } + return this._beginPromise + } + + /** + * @access private + */ + private _toTransaction (): Transaction { + return { + ...this, + run: super.run.bind(this), + commit: super.commit.bind(this), + rollback: super.rollback.bind(this), + close: super.close.bind(this), + isOpen: super.isOpen.bind(this), + _begin: this._begin.bind(this) + } + } + + /** + * @access private + */ + _begin (bookmarks: () => Promise, txConfig: TxConfig): void { + return super._begin(bookmarks, txConfig, { + onError: this._onBeginError.bind(this), + onComplete: this._onBeginMetadata.bind(this) + }) + } + + /** + * @access private + * @returns {void} + */ + private _onBeginError (error: Error): void { + this._beginError = error + if (this._reject != null) { + this._reject(error) + } + } + + /** + * @access private + * @returns {void} + */ + private _onBeginMetadata (metadata: any): void { + this._beginMetadata = metadata ?? {} + if (this._resolve != null) { + this._resolve(this._toTransaction()) + } + } +} + +export default TransactionPromise diff --git a/packages/neo4j-driver-deno/lib/core/transaction.ts b/packages/neo4j-driver-deno/lib/core/transaction.ts new file mode 100644 index 000000000..66c4c22e1 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/transaction.ts @@ -0,0 +1,699 @@ +/** + * 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. + */ + +/* eslint-disable @typescript-eslint/promise-function-async */ +import { validateQueryAndParameters } from './internal/util.ts' +import Connection from './connection.ts' +import { + ConnectionHolder, + ReadOnlyConnectionHolder, + EMPTY_CONNECTION_HOLDER +} from './internal/connection-holder.ts' +import { Bookmarks } from './internal/bookmarks.ts' +import { TxConfig } from './internal/tx-config.ts' + +import { + ResultStreamObserver, + FailedObserver, + CompletedObserver +} from './internal/observers.ts' + +import { newError } from './error.ts' +import Result from './result.ts' +import { Query } from './types.ts' + +/** + * Represents a transaction in the Neo4j database. + * + * @access public + */ +class Transaction { + private readonly _connectionHolder: ConnectionHolder + private readonly _reactive: boolean + private _state: any + private readonly _onClose: () => void + private readonly _onBookmarks: (newBookmarks: Bookmarks, previousBookmarks: Bookmarks, database?: string) => void + private readonly _onConnection: () => void + private readonly _onError: (error: Error) => Promise + private readonly _onComplete: (metadata: any, previousBookmarks?: Bookmarks) => void + private readonly _fetchSize: number + private readonly _results: any[] + private readonly _impersonatedUser?: string + private readonly _lowRecordWatermak: number + private readonly _highRecordWatermark: number + private _bookmarks: Bookmarks + private readonly _activePromise: Promise + private _acceptActive: () => void + + /** + * @constructor + * @param {ConnectionHolder} connectionHolder - the connection holder to get connection from. + * @param {function()} onClose - Function to be called when transaction is committed or rolled back. + * @param {function(bookmarks: Bookmarks)} onBookmarks callback invoked when new bookmark is produced. + * @param {function()} onConnection - Function to be called when a connection is obtained to ensure the conneciton + * is not yet released. + * @param {boolean} reactive whether this transaction generates reactive streams + * @param {number} fetchSize - the record fetch size in each pulling batch. + * @param {string} impersonatedUser - The name of the user which should be impersonated for the duration of the session. + * @param {number} highRecordWatermark - The high watermark for the record buffer. + * @param {number} lowRecordWatermark - The low watermark for the record buffer. + */ + constructor ({ + connectionHolder, + onClose, + onBookmarks, + onConnection, + reactive, + fetchSize, + impersonatedUser, + highRecordWatermark, + lowRecordWatermark + }: { + connectionHolder: ConnectionHolder + onClose: () => void + onBookmarks: (newBookmarks: Bookmarks, previousBookmarks: Bookmarks, database?: string) => void + onConnection: () => void + reactive: boolean + fetchSize: number + impersonatedUser?: string + highRecordWatermark: number + lowRecordWatermark: number + }) { + this._connectionHolder = connectionHolder + this._reactive = reactive + this._state = _states.ACTIVE + this._onClose = onClose + this._onBookmarks = onBookmarks + this._onConnection = onConnection + this._onError = this._onErrorCallback.bind(this) + this._fetchSize = fetchSize + this._onComplete = this._onCompleteCallback.bind(this) + this._results = [] + this._impersonatedUser = impersonatedUser + this._lowRecordWatermak = lowRecordWatermark + this._highRecordWatermark = highRecordWatermark + this._bookmarks = Bookmarks.empty() + this._acceptActive = () => { } // satisfy DenoJS + this._activePromise = new Promise((resolve, reject) => { + this._acceptActive = resolve + }) + } + + /** + * @private + * @param {Bookmarks | string | string []} bookmarks + * @param {TxConfig} txConfig + * @returns {void} + */ + _begin (getBookmarks: () => Promise, txConfig: TxConfig, events?: { + onError: (error: Error) => void + onComplete: (metadata: any) => void + }): void { + this._connectionHolder + .getConnection() + .then(async connection => { + this._onConnection() + if (connection != null) { + this._bookmarks = await getBookmarks() + return connection.protocol().beginTransaction({ + bookmarks: this._bookmarks, + txConfig: txConfig, + mode: this._connectionHolder.mode(), + database: this._connectionHolder.database(), + impersonatedUser: this._impersonatedUser, + beforeError: (error: Error) => { + if (events != null) { + events.onError(error) + } + return this._onError(error) + }, + afterComplete: (metadata: any) => { + if (events != null) { + events.onComplete(metadata) + } + return this._onComplete(metadata) + } + }) + } else { + throw newError('No connection available') + } + }) + .catch(error => { + if (events != null) { + events.onError(error) + } + this._onError(error).catch(() => {}) + }) + // It should make the transaction active anyway + // further errors will be treated by the existing + // observers + .finally(() => this._acceptActive()) + } + + /** + * Run Cypher query + * Could be called with a query object i.e.: `{text: "MATCH ...", parameters: {param: 1}}` + * or with the query and parameters as separate arguments. + * @param {mixed} query - Cypher query to execute + * @param {Object} parameters - Map with parameters to use in query + * @return {Result} New Result + */ + run (query: Query, parameters?: any): Result { + const { validatedQuery, params } = validateQueryAndParameters( + query, + parameters + ) + + const result = this._state.run(validatedQuery, params, { + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete, + onConnection: this._onConnection, + reactive: this._reactive, + fetchSize: this._fetchSize, + highRecordWatermark: this._highRecordWatermark, + lowRecordWatermark: this._lowRecordWatermak, + preparationJob: this._activePromise + }) + this._results.push(result) + return result + } + + /** + * Commits the transaction and returns the result. + * + * After committing the transaction can no longer be used. + * + * @returns {Promise} An empty promise if committed successfully or error if any error happened during commit. + */ + commit (): Promise { + const committed = this._state.commit({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: (meta: any) => this._onCompleteCallback(meta, this._bookmarks), + onConnection: this._onConnection, + pendingResults: this._results, + preparationJob: this._activePromise + }) + this._state = committed.state + // clean up + this._onClose() + return new Promise((resolve, reject) => { + committed.result.subscribe({ + onCompleted: () => resolve(), + onError: (error: any) => reject(error) + }) + }) + } + + /** + * Rollbacks the transaction. + * + * After rolling back, the transaction can no longer be used. + * + * @returns {Promise} An empty promise if rolled back successfully or error if any error happened during + * rollback. + */ + rollback (): Promise { + const rolledback = this._state.rollback({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete, + onConnection: this._onConnection, + pendingResults: this._results, + preparationJob: this._activePromise + }) + this._state = rolledback.state + // clean up + this._onClose() + return new Promise((resolve, reject) => { + rolledback.result.subscribe({ + onCompleted: () => resolve(), + onError: (error: any) => reject(error) + }) + }) + } + + /** + * Check if this transaction is active, which means commit and rollback did not happen. + * @return {boolean} `true` when not committed and not rolled back, `false` otherwise. + */ + isOpen (): boolean { + return this._state === _states.ACTIVE + } + + /** + * Closes the transaction + * + * This method will roll back the transaction if it is not already committed or rolled back. + * + * @returns {Promise} An empty promise if closed successfully or error if any error happened during + */ + async close (): Promise { + if (this.isOpen()) { + await this.rollback() + } + } + + _onErrorCallback (): Promise { + // error will be "acknowledged" by sending a RESET message + // database will then forget about this transaction and cleanup all corresponding resources + // it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it + this._state = _states.FAILED + this._onClose() + + // release connection back to the pool + return this._connectionHolder.releaseConnection() + } + + /** + * @private + * @param {object} meta The meta with bookmarks + * @returns {void} + */ + _onCompleteCallback (meta: { bookmark?: string | string[], db?: string }, previousBookmarks?: Bookmarks): void { + this._onBookmarks(new Bookmarks(meta?.bookmark), previousBookmarks ?? Bookmarks.empty(), meta?.db) + } +} + +/** + * Defines the structure of state transition function + * @private + */ +interface StateTransitionParams { + connectionHolder: ConnectionHolder + onError: (error: Error) => void + onComplete: (metadata: any) => void + onConnection: () => void + pendingResults: any[] + reactive: boolean + fetchSize: number + highRecordWatermark: number + lowRecordWatermark: number + preparationJob?: Promise +} + +const _states = { + // The transaction is running with no explicit success or failure marked + ACTIVE: { + commit: ({ + connectionHolder, + onError, + onComplete, + onConnection, + pendingResults, + preparationJob + }: StateTransitionParams): any => { + return { + result: finishTransaction( + true, + connectionHolder, + onError, + onComplete, + onConnection, + pendingResults, + preparationJob + ), + state: _states.SUCCEEDED + } + }, + rollback: ({ + connectionHolder, + onError, + onComplete, + onConnection, + pendingResults, + preparationJob + }: StateTransitionParams): any => { + return { + result: finishTransaction( + false, + connectionHolder, + onError, + onComplete, + onConnection, + pendingResults, + preparationJob + ), + state: _states.ROLLED_BACK + } + }, + run: ( + query: Query, + parameters: any, + { + connectionHolder, + onError, + onComplete, + onConnection, + reactive, + fetchSize, + highRecordWatermark, + lowRecordWatermark, + preparationJob + }: StateTransitionParams + ): any => { + // RUN in explicit transaction can't contain bookmarks and transaction configuration + // No need to include mode and database name as it shall be included in begin + const requirements = preparationJob ?? Promise.resolve() + + const observerPromise = + connectionHolder.getConnection() + .then(conn => requirements.then(() => conn)) + .then(conn => { + onConnection() + if (conn != null) { + return conn.protocol().run(query, parameters, { + bookmarks: Bookmarks.empty(), + txConfig: TxConfig.empty(), + beforeError: onError, + afterComplete: onComplete, + reactive: reactive, + fetchSize: fetchSize, + highRecordWatermark: highRecordWatermark, + lowRecordWatermark: lowRecordWatermark + }) + } else { + throw newError('No connection available') + } + }) + .catch(error => new FailedObserver({ error, onError })) + + return newCompletedResult( + observerPromise, + query, + parameters, + connectionHolder, + highRecordWatermark, + lowRecordWatermark + ) + } + }, + + // An error has occurred, transaction can no longer be used and no more messages will + // be sent for this transaction. + FAILED: { + commit: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot commit this transaction, because it has been rolled back either because of an error or explicit termination.' + ), + onError + }), + 'COMMIT', + {}, + connectionHolder, + 0, // high watermark + 0 // low watermark + ), + state: _states.FAILED + } + }, + rollback: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new CompletedObserver(), + 'ROLLBACK', + {}, + connectionHolder, + 0, // high watermark + 0 // low watermark + ), + state: _states.FAILED + } + }, + run: ( + query: Query, + parameters: any, + { connectionHolder, onError, onComplete }: StateTransitionParams + ): any => { + return newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot run query in this transaction, because it has been rolled back either because of an error or explicit termination.' + ), + onError + }), + query, + parameters, + connectionHolder, + 0, // high watermark + 0 // low watermark + ) + } + }, + + // This transaction has successfully committed + SUCCEEDED: { + commit: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot commit this transaction, because it has already been committed.' + ), + onError + }), + 'COMMIT', + {}, + EMPTY_CONNECTION_HOLDER, + 0, // high watermark + 0 // low watermark + ), + state: _states.SUCCEEDED, + connectionHolder + } + }, + rollback: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot rollback this transaction, because it has already been committed.' + ), + onError + }), + 'ROLLBACK', + {}, + EMPTY_CONNECTION_HOLDER, + 0, // high watermark + 0 // low watermark + ), + state: _states.SUCCEEDED, + connectionHolder + } + }, + run: ( + query: Query, + parameters: any, + { connectionHolder, onError, onComplete }: StateTransitionParams + ): any => { + return newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot run query in this transaction, because it has already been committed.' + ), + onError + }), + query, + parameters, + connectionHolder, + 0, // high watermark + 0 // low watermark + ) + } + }, + + // This transaction has been rolled back + ROLLED_BACK: { + commit: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot commit this transaction, because it has already been rolled back.' + ), + onError + }), + 'COMMIT', + {}, + connectionHolder, + 0, // high watermark + 0 // low watermark + ), + state: _states.ROLLED_BACK + } + }, + rollback: ({ + connectionHolder, + onError, + onComplete + }: StateTransitionParams): any => { + return { + result: newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot rollback this transaction, because it has already been rolled back.' + ) + }), + 'ROLLBACK', + {}, + connectionHolder, + 0, // high watermark + 0 // low watermark + ), + state: _states.ROLLED_BACK + } + }, + run: ( + query: Query, + parameters: any, + { connectionHolder, onError, onComplete }: StateTransitionParams + ): any => { + return newCompletedResult( + new FailedObserver({ + error: newError( + 'Cannot run query in this transaction, because it has already been rolled back.' + ), + onError + }), + query, + parameters, + connectionHolder, + 0, // high watermark + 0 // low watermark + ) + } + } +} + +/** + * + * @param {boolean} commit + * @param {ConnectionHolder} connectionHolder + * @param {function(err:Error): any} onError + * @param {function(metadata:object): any} onComplete + * @param {function() : any} onConnection + * @param {list>}pendingResults all run results in this transaction + */ +function finishTransaction ( + commit: boolean, + connectionHolder: ConnectionHolder, + onError: (err: Error) => any, + onComplete: (metadata: any) => any, + onConnection: () => any, + pendingResults: Result[], + preparationJob?: Promise +): Result { + const requirements = preparationJob ?? Promise.resolve() + + const observerPromise = + connectionHolder.getConnection() + .then(conn => requirements.then(() => conn)) + .then(connection => { + onConnection() + pendingResults.forEach(r => r._cancel()) + return Promise.all(pendingResults.map(result => result.summary())).then(results => { + if (connection != null) { + if (commit) { + return connection.protocol().commitTransaction({ + beforeError: onError, + afterComplete: onComplete + }) + } else { + return connection.protocol().rollbackTransaction({ + beforeError: onError, + afterComplete: onComplete + }) + } + } else { + throw newError('No connection available') + } + }) + }) + .catch(error => new FailedObserver({ error, onError })) + + // for commit & rollback we need result that uses real connection holder and notifies it when + // connection is not needed and can be safely released to the pool + return new Result( + observerPromise, + commit ? 'COMMIT' : 'ROLLBACK', + {}, + connectionHolder, + { + high: Number.MAX_VALUE, + low: Number.MAX_VALUE + } + ) +} + +/** + * Creates a {@link Result} with empty connection holder. + * For cases when result represents an intermediate or failed action, does not require any metadata and does not + * need to influence real connection holder to release connections. + * @param {ResultStreamObserver} observer - an observer for the created result. + * @param {string} query - the cypher query that produced the result. + * @param {Object} parameters - the parameters for cypher query that produced the result. + * @param {ConnectionHolder} connectionHolder - the connection holder used to get the result + * @return {Result} new result. + * @private + */ +function newCompletedResult ( + observerPromise: ResultStreamObserver | Promise, + query: Query, + parameters: any, + connectionHolder: ConnectionHolder = EMPTY_CONNECTION_HOLDER, + highRecordWatermark: number, + lowRecordWatermark: number +): Result { + return new Result( + Promise.resolve(observerPromise), + query, + parameters, + new ReadOnlyConnectionHolder(connectionHolder ?? EMPTY_CONNECTION_HOLDER), + { + low: lowRecordWatermark, + high: highRecordWatermark + } + ) +} + +export default Transaction diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts new file mode 100644 index 000000000..63140cc16 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -0,0 +1,81 @@ +/** + * 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. + */ + +/** + * @private + */ +export type Query = string | String | { text: string, parameters?: any } + +export type EncryptionLevel = 'ENCRYPTION_ON' | 'ENCRYPTION_OFF' + +export type LogLevel = 'error' | 'warn' | 'info' | 'debug' + +export type LoggerFunction = (level: LogLevel, message: string) => unknown + +export type SessionMode = 'READ' | 'WRITE' + +export interface LoggingConfig { + level?: LogLevel + logger: LoggerFunction +} + +export type TrustStrategy = + | 'TRUST_ALL_CERTIFICATES' + | 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' + | 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + +export interface Parameters { [key: string]: any } +export interface AuthToken { + scheme: string + principal: string + credentials: string + realm?: string + parameters?: Parameters +} +export interface Config { + encrypted?: boolean | EncryptionLevel + trust?: TrustStrategy + trustedCertificates?: string[] + knownHosts?: string + fetchSize?: number + maxConnectionPoolSize?: number + maxTransactionRetryTime?: number + maxConnectionLifetime?: number + connectionAcquisitionTimeout?: number + connectionTimeout?: number + disableLosslessIntegers?: boolean + useBigInt?: boolean + logging?: LoggingConfig + resolver?: (address: string) => string[] | Promise + userAgent?: string +} + +/** + * Extension interface for {@link AsyncIterator} with peek capabilities. + * + * @public + */ +export interface PeekableAsyncIterator extends AsyncIterator { + /** + * Returns the next element in the iteration without advancing the iterator. + * + * @return {IteratorResult} The next element in the iteration. + */ + peek: () => Promise> +} diff --git a/packages/neo4j-driver-deno/lib/logging.ts b/packages/neo4j-driver-deno/lib/logging.ts new file mode 100644 index 000000000..57d81ef71 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/logging.ts @@ -0,0 +1,20 @@ +import { types as coreTypes } from './core/index.ts' + +type LogLevel = coreTypes.LogLevel + +/** + * Object containing predefined logging configurations. These are expected to be used as values of the driver config's `logging` property. + * @property {function(level: ?string): object} console the function to create a logging config that prints all messages to `console.log` with + * timestamp, level and message. It takes an optional `level` parameter which represents the maximum log level to be logged. Default value is 'info'. + */ +export const logging = { + console: (level: LogLevel) => { + return { + level: level, + logger: (level: LogLevel, message: string) => + console.log(`${Date.now()} ${level.toUpperCase()} ${message}`) + // Note: This 'logging' object is in its own file so we can easily access the global Date object here without conflicting + // with the Neo4j Date class, and without relying on 'globalThis' which isn't compatible with Node 10. + } + } +} diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts new file mode 100644 index 000000000..0c221215b --- /dev/null +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -0,0 +1,537 @@ +/** + * 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 VERSION from './version.ts' +import { logging } from './logging.ts' + +import { + Neo4jError, + isRetriableError, + error, + Integer, + inSafeRange, + int, + isInt, + toNumber, + toString, + internal, + isPoint, + Point, + Date, + DateTime, + Duration, + isDate, + isDateTime, + isDuration, + isLocalDateTime, + isLocalTime, + isTime, + LocalDateTime, + LocalTime, + Time, + Node, + Path, + PathSegment, + Relationship, + UnboundRelationship, + Record, + ResultSummary, + Result, + ConnectionProvider, + Driver, + QueryResult, + ResultObserver, + Plan, + ProfiledPlan, + QueryStatistics, + Notification, + NotificationPosition, + Session, + Transaction, + ManagedTransaction, + TransactionPromise, + ServerInfo, + Connection, + driver as coreDriver, + types as coreTypes, + auth, + BookmarkManager, + bookmarkManager, + BookmarkManagerConfig, + SessionConfig +} from './core/index.ts' +// @deno-types=./bolt-connection/types/index.d.ts +import { + DirectConnectionProvider, + RoutingConnectionProvider +} from './bolt-connection/index.js' + +type AuthToken = coreTypes.AuthToken +type Config = coreTypes.Config +type TrustStrategy = coreTypes.TrustStrategy +type EncryptionLevel = coreTypes.EncryptionLevel +type SessionMode = coreTypes.SessionMode +type Logger = internal.logger.Logger +type ConfiguredCustomResolver = internal.resolver.ConfiguredCustomResolver + +const { READ, WRITE } = coreDriver + +const { + util: { ENCRYPTION_ON, assertString, isEmptyObjectOrNull }, + serverAddress: { ServerAddress }, + urlUtil +} = internal + +/** + * Construct a new Neo4j Driver. This is your main entry point for this + * library. + * + * ## Configuration + * + * This function optionally takes a configuration argument. Available configuration + * options are as follows: + * + * { + * // Encryption level: ENCRYPTION_ON or ENCRYPTION_OFF. + * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF + * + * // Trust strategy to use if encryption is enabled. There is no mode to disable + * // trust other than disabling encryption altogether. The reason for + * // this is that if you don't know who you are talking to, it is easy for an + * // attacker to hijack your encrypted connection, rendering encryption pointless. + * // + * // TRUST_SYSTEM_CA_SIGNED_CERTIFICATES is the default choice. For NodeJS environments, this + * // means that you trust whatever certificates are in the default trusted certificate + * // store of the underlying system. For Browser environments, the trusted certificate + * // store is usually managed by the browser. Refer to your system or browser documentation + * // if you want to explicitly add a certificate as trusted. + * // + * // TRUST_CUSTOM_CA_SIGNED_CERTIFICATES is another option for trust verification - + * // whenever we establish an encrypted connection, we ensure the host is using + * // an encryption certificate that is in, or is signed by, a certificate given + * // as trusted through configuration. This option is only available for NodeJS environments. + * // + * // TRUST_ALL_CERTIFICATES means that you trust everything without any verifications + * // steps carried out. This option is only available for NodeJS environments and should not + * // be used on production systems. + * trust: "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES" | "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES" | + * "TRUST_ALL_CERTIFICATES", + * + * // List of one or more paths to trusted encryption certificates. This only + * // works in the NodeJS bundle, and only matters if you use "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES". + * // The certificate files should be in regular X.509 PEM format. + * // For instance, ['./trusted.pem'] + * trustedCertificates: [], + * + * // The maximum total number of connections allowed to be managed by the connection pool, per host. + * // This includes both in-use and idle connections. No maximum connection pool size is imposed + * // by default. + * maxConnectionPoolSize: 100, + * + * // The maximum allowed lifetime for a pooled connection in milliseconds. Pooled connections older than this + * // threshold will be closed and removed from the pool. Such discarding happens during connection acquisition + * // so that new session is never backed by an old connection. Setting this option to a low value will cause + * // a high connection churn and might result in a performance hit. It is recommended to set maximum lifetime + * // to a slightly smaller value than the one configured in network equipment (load balancer, proxy, firewall, + * // etc. can also limit maximum connection lifetime). No maximum lifetime limit is imposed by default. Zero + * // and negative values result in lifetime not being checked. + * maxConnectionLifetime: 60 * 60 * 1000, // 1 hour + * + * // The maximum amount of time to wait to acquire a connection from the pool (to either create a new + * // connection or borrow an existing one. + * connectionAcquisitionTimeout: 60000, // 1 minute + * + * // Specify the maximum time in milliseconds transactions are allowed to retry via + * // `Session#executeRead()` and `Session#executeWrite()` functions. + * // These functions will retry the given unit of work on `ServiceUnavailable`, `SessionExpired` and transient + * // errors with exponential backoff using initial delay of 1 second. + * // Default value is 30000 which is 30 seconds. + * maxTransactionRetryTime: 30000, // 30 seconds + * + * // Specify socket connection timeout in milliseconds. Numeric values are expected. Negative and zero values + * // result in no timeout being applied. Connection establishment will be then bound by the timeout configured + * // on the operating system level. Default value is 30000, which is 30 seconds. + * connectionTimeout: 30000, // 30 seconds + * + * // Make this driver always return native JavaScript numbers for integer values, instead of the + * // dedicated {@link Integer} class. Values that do not fit in native number bit range will be represented as + * // `Number.NEGATIVE_INFINITY` or `Number.POSITIVE_INFINITY`. + * // **Warning:** ResultSummary It is not always safe to enable this setting when JavaScript applications are not the only ones + * // interacting with the database. Stored numbers might in such case be not representable by native + * // {@link Number} type and thus driver will return lossy values. This might also happen when data was + * // initially imported using neo4j import tool and contained numbers larger than + * // `Number.MAX_SAFE_INTEGER`. Driver will then return positive infinity, which is lossy. + * // Default value for this option is `false` because native JavaScript numbers might result + * // in loss of precision in the general case. + * disableLosslessIntegers: false, + * + * // Make this driver always return native Javascript {@link BigInt} for integer values, instead of the dedicated {@link Integer} class or {@link Number}. + * // + * // Default value for this option is `false` for backwards compatibility. + * // + * // **Warning:** `BigInt` doesn't implement the method `toJSON`. In maner of serialize it as `json`, It's needed to add a custom implementation of the `toJSON` on the + * // `BigInt.prototype` {@see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json} + * useBigInt: false, + * + * // Specify the logging configuration for the driver. Object should have two properties `level` and `logger`. + * // + * // Property `level` represents the logging level which should be one of: 'error', 'warn', 'info' or 'debug'. This property is optional and + * // its default value is 'info'. Levels have priorities: 'error': 0, 'warn': 1, 'info': 2, 'debug': 3. Enabling a certain level also enables all + * // levels with lower priority. For example: 'error', 'warn' and 'info' will be logged when 'info' level is configured. + * // + * // Property `logger` represents the logging function which will be invoked for every log call with an acceptable level. The function should + * // take two string arguments `level` and `message`. The function should not execute any blocking or long-running operations + * // because it is often executed on a hot path. + * // + * // No logging is done by default. See `neo4j.logging` object that contains predefined logging implementations. + * logging: { + * level: 'info', + * logger: (level, message) => console.log(level + ' ' + message) + * }, + * + * // Specify a custom server address resolver function used by the routing driver to resolve the initial address used to create the driver. + * // Such resolution happens: + * // * during the very first rediscovery when driver is created + * // * when all the known routers from the current routing table have failed and driver needs to fallback to the initial address + * // + * // In NodeJS environment driver defaults to performing a DNS resolution of the initial address using 'dns' module. + * // In browser environment driver uses the initial address as-is. + * // Value should be a function that takes a single string argument - the initial address. It should return an array of new addresses. + * // Address is a string of shape ':'. Provided function can return either a Promise resolved with an array of addresses + * // or array of addresses directly. + * resolver: function(address) { + * return ['127.0.0.1:8888', 'fallback.db.com:7687']; + * }, + * + * // Optionally override the default user agent name. + * userAgent: USER_AGENT + * } + * + * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" + * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Object} config Configuration object. See the configuration section above for details. + * @returns {Driver} + */ +function driver ( + url: string, + authToken: AuthToken, + config: Config = {} +): Driver { + assertString(url, 'Bolt URL') + const parsedUrl = urlUtil.parseDatabaseUrl(url) + + // Determine entryption/trust options from the URL. + let routing = false + let encrypted = false + let trust: TrustStrategy | undefined + switch (parsedUrl.scheme) { + case 'bolt': + break + case 'bolt+s': + encrypted = true + trust = 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + break + case 'bolt+ssc': + encrypted = true + trust = 'TRUST_ALL_CERTIFICATES' + break + case 'neo4j': + routing = true + break + case 'neo4j+s': + encrypted = true + trust = 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + routing = true + break + case 'neo4j+ssc': + encrypted = true + trust = 'TRUST_ALL_CERTIFICATES' + routing = true + break + default: + throw new Error(`Unknown scheme: ${parsedUrl.scheme ?? 'null'}`) + } + + // Encryption enabled on URL, propagate trust to the config. + if (encrypted) { + // Check for configuration conflict between URL and config. + if ('encrypted' in config || 'trust' in config) { + throw new Error( + 'Encryption/trust can only be configured either through URL or config, not both' + ) + } + config.encrypted = ENCRYPTION_ON + config.trust = trust + } + + // Sanitize authority token. Nicer error from server when a scheme is set. + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + + // Use default user agent or user agent specified by user. + config.userAgent = config.userAgent ?? USER_AGENT + const address = ServerAddress.fromUrl(parsedUrl.hostAndPort) + + const meta = { + address, + typename: routing ? 'Routing' : 'Direct', + routing + } + + return new Driver(meta, config, createConnectionProviderFunction()) + + function createConnectionProviderFunction (): (id: number, config: Config, log: Logger, hostNameResolver: ConfiguredCustomResolver) => ConnectionProvider { + if (routing) { + return ( + id: number, + config: Config, + log: Logger, + hostNameResolver: ConfiguredCustomResolver + ): ConnectionProvider => + new RoutingConnectionProvider({ + id, + config, + log, + hostNameResolver, + authToken, + address, + userAgent: config.userAgent, + routingContext: parsedUrl.query + }) + } else { + if (!isEmptyObjectOrNull(parsedUrl.query)) { + throw new Error( + `Parameters are not supported with none routed scheme. Given URL: '${url}'` + ) + } + + return (id: number, config: Config, log: Logger): ConnectionProvider => + new DirectConnectionProvider({ + id, + config, + log, + authToken, + address, + userAgent: config.userAgent + }) + } + } +} + +/** + * Verifies if the driver can reach a server at the given url. + * + * @experimental + * @since 5.0.0 + * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" + * @param {Pick} config Configuration object. See the {@link driver} + * @returns {true} When the server is reachable + * @throws {Error} When the server is not reachable or the url is invalid + */ +async function hasReachableServer (url: string, config?: Pick): Promise { + const nonLoggedDriver = driver(url, { scheme: 'none', principal: '', credentials: '' }, config) + try { + await nonLoggedDriver.getNegotiatedProtocolVersion() + return true + } finally { + await nonLoggedDriver.close() + } +} + +const USER_AGENT: string = 'neo4j-javascript/' + VERSION + +/** + * Object containing constructors for all neo4j types. + */ +const types = { + Node, + Relationship, + UnboundRelationship, + PathSegment, + Path, + Result, + ResultSummary, + Record, + Point, + Date, + DateTime, + Duration, + LocalDateTime, + LocalTime, + Time, + Integer +} + +/** + * Object containing string constants representing session access modes. + */ +const session = { + READ, + WRITE +} + +/** + * Object containing functions to work with {@link Integer} objects. + */ +const integer = { + toNumber, + toString, + inSafeRange +} + +/** + * Object containing functions to work with spatial types, like {@link Point}. + */ +const spatial = { + isPoint +} + +/** + * Object containing functions to work with temporal types, like {@link Time} or {@link Duration}. + */ +const temporal = { + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTime +} + +/** + * @private + */ +const forExport = { + driver, + hasReachableServer, + int, + isInt, + isPoint, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTime, + integer, + Neo4jError, + isRetriableError, + auth, + logging, + types, + session, + error, + spatial, + temporal, + Driver, + Result, + Record, + ResultSummary, + Node, + Relationship, + UnboundRelationship, + PathSegment, + Path, + Integer, + Plan, + ProfiledPlan, + QueryStatistics, + Notification, + ServerInfo, + Session, + Transaction, + ManagedTransaction, + TransactionPromise, + Point, + Duration, + LocalTime, + Time, + Date, + LocalDateTime, + DateTime, + ConnectionProvider, + Connection, + bookmarkManager +} + +export { + driver, + hasReachableServer, + int, + isInt, + isPoint, + isDuration, + isLocalTime, + isTime, + isDate, + isLocalDateTime, + isDateTime, + integer, + Neo4jError, + isRetriableError, + auth, + logging, + types, + session, + error, + spatial, + temporal, + Driver, + Result, + Record, + ResultSummary, + Node, + Relationship, + UnboundRelationship, + PathSegment, + Path, + Integer, + Plan, + ProfiledPlan, + QueryStatistics, + Notification, + ServerInfo, + Session, + Transaction, + ManagedTransaction, + TransactionPromise, + Point, + Duration, + LocalTime, + Time, + Date, + LocalDateTime, + DateTime, + ConnectionProvider, + Connection, + bookmarkManager +} +export type { + QueryResult, + AuthToken, + Config, + EncryptionLevel, + TrustStrategy, + SessionMode, + ResultObserver, + NotificationPosition, + BookmarkManager, + BookmarkManagerConfig, + SessionConfig +} +export default forExport diff --git a/packages/neo4j-driver-deno/lib/version.ts b/packages/neo4j-driver-deno/lib/version.ts new file mode 100644 index 000000000..b653b98b7 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/version.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ + +export default "5.0.0-dev" // Specified using --version when running generate.ts