From f87463eadfc49df22180ea48180a61f9f4e9d139 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Wed, 31 Jul 2019 12:44:10 +0100 Subject: [PATCH 01/14] Enable code coverage statistics --- .babelrc | 11 +- .gitignore | 6 +- .nycrc | 4 + package-lock.json | 1026 +++++++++++++++++++++++++++++++++++++++++---- package.json | 7 +- 5 files changed, 962 insertions(+), 92 deletions(-) create mode 100644 .nycrc diff --git a/.babelrc b/.babelrc index 15ce28a39..fd7a89fc3 100644 --- a/.babelrc +++ b/.babelrc @@ -5,6 +5,13 @@ ] ], "plugins": [ - "@babel/plugin-transform-runtime" - ] + "@babel/plugin-transform-runtime", + ], + "env": { + "test": { + "plugins": [ + "istanbul" + ] + } + } } diff --git a/.gitignore b/.gitignore index dc27dcc1f..064b8e33c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ node_modules docs/build .npmrc *.iml -/lib \ No newline at end of file +/lib +.nyc_output +coverage +.vscode +*.code-workspace \ No newline at end of file diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000..8cd3fdbe9 --- /dev/null +++ b/.nycrc @@ -0,0 +1,4 @@ +{ + "extends": "@istanbuljs/nyc-config-babel", + "reporter": ["text", "lcov"] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8c431c827..02b5c1dfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1577,6 +1577,12 @@ } } }, + "@istanbuljs/nyc-config-babel": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-babel/-/nyc-config-babel-2.1.1.tgz", + "integrity": "sha512-cWcUCqHOYB+Mpumsv03uaE7rMvtmJn7pZ3llc+9gyqMFC93IVcUuuJ/mknoWsiuajcEjRCqKmhGaiAaXG6kzLA==", + "dev": true + }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -1921,6 +1927,13 @@ "integrity": "sha1-S4Mee1MUFafMUYzUBOc/YZPGNJ0=", "dev": true }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -2006,6 +2019,15 @@ "buffer-equal": "^1.0.0" } }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -2415,6 +2437,63 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + } + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -3070,6 +3149,42 @@ "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", "dev": true }, + "caching-transform": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", + "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", + "dev": true, + "requires": { + "hasha": "^3.0.0", + "make-dir": "^2.0.0", + "package-hash": "^3.0.0", + "write-file-atomic": "^2.4.2" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -3704,6 +3819,43 @@ } } }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -4037,6 +4189,15 @@ } } }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, "default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", @@ -4536,6 +4697,12 @@ "next-tick": "^1.0.0" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -5838,6 +6005,28 @@ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", "dev": true }, + "foreground-child": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", + "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "dev": true, + "requires": { + "cross-spawn": "^4", + "signal-exit": "^3.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7370,6 +7559,26 @@ "glogg": "^1.0.0" } }, + "handlebars": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", + "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -7500,6 +7709,15 @@ "minimalistic-assert": "^1.0.1" } }, + "hasha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", + "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "dev": true, + "requires": { + "is-stream": "^1.0.1" + } + }, "hat": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", @@ -8435,62 +8653,308 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, - "istextorbinary": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", - "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", - "dev": true, - "requires": { - "binaryextensions": "2", - "editions": "^1.3.3", - "textextensions": "2" - } - }, - "jasmine": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", - "integrity": "sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ==", + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { - "glob": "^7.1.3", - "jasmine-core": "~3.4.0" + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } } }, - "jasmine-core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz", - "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", "dev": true }, - "jasmine-spec-reporter": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", - "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", "dev": true, "requires": { - "colors": "1.1.2" - }, - "dependencies": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - } + "append-transform": "^1.0.0" } }, - "jasmine-terminal-reporter": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/jasmine-terminal-reporter/-/jasmine-terminal-reporter-1.0.3.tgz", - "integrity": "sha1-iW8eyP30v2rs3UHFA+2nNH9hUms=", + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", "dev": true, "requires": { - "indent-string": "^2.1.0", - "pluralize": "^1.2.1" + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" } }, - "js-levenshtein": { - "version": "1.1.6", + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", + "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "dev": true, + "requires": { + "handlebars": "^4.1.2" + } + }, + "istextorbinary": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz", + "integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==", + "dev": true, + "requires": { + "binaryextensions": "2", + "editions": "^1.3.3", + "textextensions": "2" + } + }, + "jasmine": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.4.0.tgz", + "integrity": "sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "jasmine-core": "~3.4.0" + } + }, + "jasmine-core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz", + "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + } + } + }, + "jasmine-terminal-reporter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/jasmine-terminal-reporter/-/jasmine-terminal-reporter-1.0.3.tgz", + "integrity": "sha1-iW8eyP30v2rs3UHFA+2nNH9hUms=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "pluralize": "^1.2.1" + } + }, + "js-levenshtein": { + "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", "dev": true @@ -9172,9 +9636,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", "dev": true }, "lodash._basecopy": { @@ -9270,6 +9734,12 @@ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", "dev": true }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", @@ -9318,9 +9788,9 @@ "dev": true }, "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha1-rcJdnLmbk5HFliTzefu6YNcRHVQ=", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "lodash.pick": { @@ -9698,6 +10168,23 @@ "readable-stream": "^2.0.1" } }, + "merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "messageformat": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-1.1.1.tgz", @@ -9859,9 +10346,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -9871,7 +10358,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -10059,6 +10546,12 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -10301,6 +10794,215 @@ "dev": true, "optional": true }, + "nyc": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", + "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "caching-transform": "^3.0.2", + "convert-source-map": "^1.6.0", + "cp-file": "^6.2.0", + "find-cache-dir": "^2.1.0", + "find-up": "^3.0.0", + "foreground-child": "^1.5.6", + "glob": "^7.1.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.4", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "merge-source-map": "^1.1.0", + "resolve-from": "^4.0.0", + "rimraf": "^2.6.3", + "signal-exit": "^3.0.2", + "spawn-wrap": "^1.4.2", + "test-exclude": "^5.2.3", + "uuid": "^3.3.2", + "yargs": "^13.2.2", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -10519,6 +11221,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, "os-locale": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", @@ -10584,6 +11292,18 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "package-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", + "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^3.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", @@ -12234,6 +12954,15 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -12612,10 +13341,9 @@ } }, "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha1-87sP572n+2nerAwW8XtQsLh5BQQ=", - "dev": true, + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", + "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", "requires": { "tslib": "^1.9.0" } @@ -12702,9 +13430,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -13115,6 +13843,20 @@ "integrity": "sha1-AI22XtzmxQ7sDF4ijhlFBh3QQ3w=", "dev": true }, + "spawn-wrap": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", + "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "dev": true, + "requires": { + "foreground-child": "^1.5.6", + "mkdirp": "^0.5.0", + "os-homedir": "^1.0.1", + "rimraf": "^2.6.2", + "signal-exit": "^3.0.2", + "which": "^1.3.0" + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -13662,6 +14404,127 @@ } } }, + "test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "requires": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "requires": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + } + } + }, "text-encoding-utf-8": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", @@ -13882,8 +14745,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY=", - "dev": true + "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY=" }, "tty-browserify": { "version": "0.0.1", @@ -14095,38 +14957,15 @@ "dev": true }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-filename": { @@ -14818,6 +15657,17 @@ "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", diff --git a/package.json b/package.json index 24847c0c2..5581592dc 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,10 @@ "@babel/plugin-transform-runtime": "^7.4.4", "@babel/preset-env": "^7.4.4", "@babel/register": "^7.4.4", + "@istanbuljs/nyc-config-babel": "^2.1.1", "async": "^2.6.2", "babel-eslint": "^10.0.1", + "babel-plugin-istanbul": "^5.2.0", "babelify": "^10.0.0", "browserify": "^16.2.3", "browserify-transform-tools": "^1.7.0", @@ -74,6 +76,7 @@ "gulp-uglify": "^3.0.2", "gulp-watch": "^5.0.1", "husky": "^2.3.0", + "istanbul": "^0.4.5", "jasmine-spec-reporter": "^4.2.1", "karma": "^4.1.0", "karma-browserify": "^6.0.0", @@ -85,10 +88,11 @@ "karma-source-map-support": "^1.4.0", "karma-spec-reporter": "^0.0.32", "lint-staged": "^8.1.6", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "lolex": "^4.0.1", "minimist": "^1.2.0", "mustache": "^3.0.1", + "nyc": "^14.1.1", "prettier-eslint": "^8.8.2", "prettier-eslint-cli": "^4.7.1", "run-sequence": "^2.2.1", @@ -101,6 +105,7 @@ }, "dependencies": { "@babel/runtime": "^7.4.4", + "rxjs": "^6.5.2", "text-encoding-utf-8": "^1.0.2", "uri-js": "^4.2.2" } From ce7e252e833787e218742cada9ec81f4b5fdb175 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Wed, 31 Jul 2019 12:50:05 +0100 Subject: [PATCH 02/14] Pull stream observers into protocol implementations --- src/internal/bolt-protocol-util.js | 4 +- src/internal/bolt-protocol-v1.js | 227 ++++-- src/internal/bolt-protocol-v3.js | 160 ++++- src/internal/bolt-protocol-v4.js | 71 +- src/internal/connection-channel.js | 81 +-- src/internal/connection-holder.js | 10 +- src/internal/connection.js | 5 +- src/internal/connectivity-verifier.js | 7 +- src/internal/packstream-v1.js | 80 +-- src/internal/packstream-v2.js | 123 ++-- src/internal/routing-util.js | 7 +- src/internal/stream-observer.js | 174 ----- src/internal/stream-observers.js | 473 +++++++++++++ src/result.js | 107 +-- src/session.js | 79 +-- src/transaction.js | 314 ++++---- test/examples.test.js | 433 ++++++----- test/internal/bolt-protocol-v1.test.js | 65 +- test/internal/bolt-protocol-v3.test.js | 34 +- test/internal/bolt-protocol-v4.test.js | 7 +- test/internal/connection-channel.test.js | 39 +- test/internal/connection-holder.test.js | 33 +- .../node/direct.driver.boltkit.test.js | 16 +- .../node/routing.driver.boltkit.test.js | 46 +- test/internal/stream-observer.test.js | 24 +- test/session.test.js | 42 +- test/stress.test.js | 6 +- test/temporal-types.test.js | 670 +++++++++--------- test/types/session.test.ts | 4 +- types/session.d.ts | 2 +- 30 files changed, 1890 insertions(+), 1453 deletions(-) delete mode 100644 src/internal/stream-observer.js create mode 100644 src/internal/stream-observers.js diff --git a/src/internal/bolt-protocol-util.js b/src/internal/bolt-protocol-util.js index 07d51cec7..5b8031d5b 100644 --- a/src/internal/bolt-protocol-util.js +++ b/src/internal/bolt-protocol-util.js @@ -17,11 +17,12 @@ * limitations under the License. */ import { newError } from '../error' +import { ResultStreamObserver } from './stream-observers' /** * @param {TxConfig} txConfig the auto-commit transaction configuration. * @param {Connection} connection the connection. - * @param {StreamObserver} observer the response observer. + * @param {ResultStreamObserver} observer the response observer. */ function assertTxConfigIsEmpty (txConfig, connection, observer) { if (txConfig && !txConfig.isEmpty()) { @@ -41,7 +42,6 @@ function assertTxConfigIsEmpty (txConfig, connection, observer) { * Asserts that the passed-in database name is empty. * @param {string} database * @param {Connection} connection - * @param {StreamObserver} observer */ function assertDatabaseIsEmpty (database, connection, observer) { if (database) { diff --git a/src/internal/bolt-protocol-v1.js b/src/internal/bolt-protocol-v1.js index db23b231b..0bff631b1 100644 --- a/src/internal/bolt-protocol-v1.js +++ b/src/internal/bolt-protocol-v1.js @@ -21,10 +21,19 @@ import * as v1 from './packstream-v1' import Bookmark from './bookmark' import TxConfig from './tx-config' import { ACCESS_MODE_WRITE } from './constants' +import Connection from './connection' +import { Chunker } from './chunking' +import { Packer } from './packstream-v1' import { assertDatabaseIsEmpty, assertTxConfigIsEmpty } from './bolt-protocol-util' +import { + ResultStreamObserver, + LoginObserver, + ResetObserver, + StreamObserver +} from './stream-observers' export default class BoltProtocol { /** @@ -66,99 +75,217 @@ export default class BoltProtocol { /** * Perform initialization and authentication of the underlying connection. - * @param {string} clientName the client name. - * @param {object} authToken the authentication token. - * @param {StreamObserver} observer the response observer. + * @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 (clientName, authToken, observer) { - const message = RequestMessage.init(clientName, authToken) - this._connection.write(message, observer, true) + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + connection: this._connection, + afterError: onError, + afterComplete: onComplete + }) + + this._connection.write( + RequestMessage.init(userAgent, authToken), + observer, + true + ) + + return observer } - prepareToClose (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 {StreamObserver} observer the response observer. - * @param {Bookmark} bookmark the bookmark. - * @param {TxConfig} txConfig the configuration. - * @param {string} database the target database name. - * @param {string} mode the access mode. + * @param {object} param + * @param {Bookmark} param.bookmark the bookmark. + * @param {TxConfig} param.txConfig the configuration. + * @param {string} param.database the target database name. + * @param {string} param.mode the access mode. + * @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 (observer, { bookmark, txConfig, database, mode }) { - assertTxConfigIsEmpty(txConfig, this._connection, observer) - assertDatabaseIsEmpty(database, this._connection, observer) - - const runMessage = RequestMessage.run( + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + return this.run( 'BEGIN', - bookmark.asBeginTransactionParameters() + bookmark ? bookmark.asBeginTransactionParameters() : {}, + { + bookmark: bookmark, + txConfig: txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush: false + } ) - const pullAllMessage = RequestMessage.pullAll() - - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, false) } /** * Commit the explicit transaction. - * @param {StreamObserver} observer the response observer. + * @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 (observer) { + 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 - this.run('COMMIT', {}, observer, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: ACCESS_MODE_WRITE - }) + return this.run( + 'COMMIT', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) } /** * Rollback the explicit transaction. - * @param {StreamObserver} observer the response observer. + * @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 (observer) { + 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 - this.run('ROLLBACK', {}, observer, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: ACCESS_MODE_WRITE - }) + return this.run( + 'ROLLBACK', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: ACCESS_MODE_WRITE, + beforeError, + afterError, + beforeComplete, + afterComplete + } + ) } /** * Send a Cypher statement through the underlying connection. * @param {string} statement the cypher statement. * @param {object} parameters the statement parameters. - * @param {StreamObserver} observer the response observer. - * @param {Bookmark} bookmark the bookmark. - * @param {TxConfig} txConfig the auto-commit transaction configuration. - * @param {string} database the target database name. - * @param {string} mode the access mode. + * @param {object} param + * @param {Bookmark} param.bookmark the bookmark. + * @param {TxConfig} param.txConfig the transaction configuration. + * @param {string} param.database the target database name. + * @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 (statement, parameters, observer, { bookmark, txConfig, database, mode }) { + run ( + statement, + parameters, + { + bookmark, + txConfig, + database, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + // bookmark and mode are ignored in this version of the protocol assertTxConfigIsEmpty(txConfig, this._connection, observer) // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._connection, observer) - const runMessage = RequestMessage.run(statement, parameters) - const pullAllMessage = RequestMessage.pullAll() + this._connection.write( + RequestMessage.run(statement, parameters), + observer, + false + ) + this._connection.write(RequestMessage.pullAll(), observer, flush) - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, true) + return observer } /** * Send a RESET through the underlying connection. - * @param {StreamObserver} observer the response observer. + * @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 (observer) { - const message = RequestMessage.reset() - this._connection.write(message, observer, true) + reset ({ onError, onComplete } = {}) { + const observer = new ResetObserver({ + connection: this._connection, + onError, + onComplete + }) + + this._connection.write(RequestMessage.reset(), observer, true) + + return observer } _createPacker (chunker) { diff --git a/src/internal/bolt-protocol-v3.js b/src/internal/bolt-protocol-v3.js index 5d0394f01..118b08350 100644 --- a/src/internal/bolt-protocol-v3.js +++ b/src/internal/bolt-protocol-v3.js @@ -19,6 +19,13 @@ import BoltProtocolV2 from './bolt-protocol-v2' import RequestMessage from './request-message' import { assertDatabaseIsEmpty } from './bolt-protocol-util' +import { + StreamObserver, + LoginObserver, + ResultStreamObserver +} from './stream-observers' + +const noOpObserver = new StreamObserver() export default class BoltProtocol extends BoltProtocolV2 { transformMetadata (metadata) { @@ -37,57 +44,138 @@ export default class BoltProtocol extends BoltProtocolV2 { return metadata } - initialize (userAgent, authToken, observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.hello(userAgent, authToken) - this._connection.write(message, observer, true) + initialize ({ userAgent, authToken, onError, onComplete } = {}) { + const observer = new LoginObserver({ + connection: this._connection, + afterError: onError, + afterComplete: onComplete + }) + + this._connection.write( + RequestMessage.hello(userAgent, authToken), + observer, + true + ) + + return observer } - prepareToClose (observer) { - const message = RequestMessage.goodbye() - this._connection.write(message, observer, true) + prepareToClose () { + this._connection.write(RequestMessage.goodbye(), noOpObserver, true) } - beginTransaction (observer, { bookmark, txConfig, database, mode }) { + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + // passing in a database name on this protocol version throws an error assertDatabaseIsEmpty(database, this._connection, observer) - prepareToHandleSingleResponse(observer) - const message = RequestMessage.begin({ bookmark, txConfig, mode }) - this._connection.write(message, observer, true) - } - commitTransaction (observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.commit() - this._connection.write(message, observer, true) + this._connection.write( + RequestMessage.begin({ bookmark, txConfig, mode }), + observer, + true + ) + + return observer } - rollbackTransaction (observer) { - prepareToHandleSingleResponse(observer) - const message = RequestMessage.rollback() - this._connection.write(message, observer, true) + commitTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write(RequestMessage.commit(), observer, true) + + return observer } - run (statement, parameters, observer, { bookmark, txConfig, database, mode }) { - // passing in a database name on this protocol version throws an error - assertDatabaseIsEmpty(database, this._connection, observer) + rollbackTransaction ({ + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write(RequestMessage.rollback(), observer, true) - const runMessage = RequestMessage.runWithMetadata(statement, parameters, { + return observer + } + + run ( + statement, + parameters, + { bookmark, txConfig, - mode + database, + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete }) - const pullAllMessage = RequestMessage.pullAll() - this._connection.write(runMessage, observer, false) - this._connection.write(pullAllMessage, observer, true) - } -} + // passing in a database name on this protocol version throws an error + assertDatabaseIsEmpty(database, this._connection, observer) -function prepareToHandleSingleResponse (observer) { - if ( - observer && - typeof observer.prepareToHandleSingleResponse === 'function' - ) { - observer.prepareToHandleSingleResponse() + this._connection.write( + RequestMessage.runWithMetadata(statement, parameters, { + bookmark, + txConfig, + mode + }), + observer, + false + ) + this._connection.write(RequestMessage.pullAll(), observer, flush) + + return observer } } diff --git a/src/internal/bolt-protocol-v4.js b/src/internal/bolt-protocol-v4.js index ca75ada8a..766321b23 100644 --- a/src/internal/bolt-protocol-v4.js +++ b/src/internal/bolt-protocol-v4.js @@ -18,23 +18,76 @@ */ import BoltProtocolV3 from './bolt-protocol-v3' import RequestMessage from './request-message' +import { ResultStreamObserver } from './stream-observers' export default class BoltProtocol extends BoltProtocolV3 { - beginTransaction (observer, { bookmark, txConfig, database, mode }) { - const message = RequestMessage.begin({ bookmark, txConfig, database, mode }) - this._connection.write(message, observer, true) + beginTransaction ({ + bookmark, + txConfig, + database, + mode, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeError, + afterError, + beforeComplete, + afterComplete + }) + observer.prepareToHandleSingleResponse() + + this._connection.write( + RequestMessage.begin({ bookmark, txConfig, database, mode }), + observer, + true + ) + + return observer } - run (statement, parameters, observer, { bookmark, txConfig, database, mode }) { - const runMessage = RequestMessage.runWithMetadata(statement, parameters, { + run ( + statement, + parameters, + { bookmark, txConfig, database, - mode + mode, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete, + flush = true + } = {} + ) { + const observer = new ResultStreamObserver({ + connection: this._connection, + beforeKeys, + afterKeys, + beforeError, + afterError, + beforeComplete, + afterComplete }) - const pullMessage = RequestMessage.pull() - this._connection.write(runMessage, observer, false) - this._connection.write(pullMessage, observer, true) + this._connection.write( + RequestMessage.runWithMetadata(statement, parameters, { + bookmark, + txConfig, + database, + mode + }), + observer, + false + ) + this._connection.write(RequestMessage.pull(), observer, flush) + + return observer } } diff --git a/src/internal/connection-channel.js b/src/internal/connection-channel.js index 4a6b0454f..829b9a5d8 100644 --- a/src/internal/connection-channel.js +++ b/src/internal/connection-channel.js @@ -23,6 +23,8 @@ import { newError, PROTOCOL_ERROR } from '../error' import ChannelConfig from './channel-config' import ProtocolHandshaker from './protocol-handshaker' import Connection from './connection' +import BoltProtocol from './bolt-protocol-v1' +import { ResultStreamObserver } from './stream-observers' // Signature bytes for each response message type const SUCCESS = 0x70 // 0111 0000 // SUCCESS @@ -74,6 +76,10 @@ export default class ChannelConnection extends Connection { this._dbConnectionId = null // bolt protocol is initially not initialized + /** + * @private + * @type {BoltProtocol} + */ this._protocol = null // error extracted from a FAILURE message @@ -197,9 +203,14 @@ export default class ChannelConnection extends Connection { * @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) => { - const observer = new InitializationObserver(this, resolve, reject) - this._protocol.initialize(userAgent, authToken, observer) + this._protocol.initialize({ + userAgent, + authToken, + onError: err => reject(err), + onComplete: () => resolve(self) + }) }) } @@ -236,7 +247,7 @@ export default class ChannelConnection extends Connection { /** * Write a message to the network channel. * @param {RequestMessage} message the message to write. - * @param {StreamObserver} observer the response observer. + * @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) { @@ -251,8 +262,7 @@ export default class ChannelConnection extends Connection { .packer() .packStruct( message.signature, - message.fields.map(field => this._packable(field)), - err => this._handleFatalError(err) + message.fields.map(field => this._packable(field)) ) this._chunker.messageBoundary() @@ -365,12 +375,6 @@ export default class ChannelConnection extends Connection { resetAndFlush () { return new Promise((resolve, reject) => { this._protocol.reset({ - onNext: record => { - const neo4jError = this._handleProtocolError( - 'Received RECORD as a response for RESET: ' + JSON.stringify(record) - ) - reject(neo4jError) - }, onError: error => { if (this._isBroken) { // handling a fatal error, no need to raise a protocol violation @@ -382,7 +386,7 @@ export default class ChannelConnection extends Connection { reject(neo4jError) } }, - onCompleted: () => { + onComplete: () => { resolve() } }) @@ -391,16 +395,10 @@ export default class ChannelConnection extends Connection { _resetOnFailure () { this._protocol.reset({ - onNext: record => { - this._handleProtocolError( - 'Received RECORD as a response for RESET: ' + JSON.stringify(record) - ) - }, - // clear the current failure when response for RESET is received onError: () => { this._currentFailure = null }, - onCompleted: () => { + onComplete: () => { this._currentFailure = null } }) @@ -450,7 +448,7 @@ export default class ChannelConnection extends Connection { 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(NO_OP_OBSERVER) + this._protocol.prepareToClose() } this._ch.close(() => { @@ -466,9 +464,7 @@ export default class ChannelConnection extends Connection { } _packable (value) { - return this._protocol - .packer() - .packable(value, err => this._handleFatalError(err)) + return this._protocol.packer().packable(value) } _handleProtocolError (message) { @@ -479,42 +475,3 @@ export default class ChannelConnection extends Connection { return error } } - -class InitializationObserver { - constructor (connection, onSuccess, onError) { - this._connection = connection - this._onSuccess = onSuccess - this._onError = onError - } - - onNext (record) { - this.onError( - newError('Received RECORD when initializing ' + JSON.stringify(record)) - ) - } - - onError (error) { - this._connection._updateCurrentObserver() // make sure this exact observer will not be called again - this._connection._handleFatalError(error) // initialization errors are fatal - - this._onError(error) - } - - onCompleted (metadata) { - if (metadata) { - // read server version from the response metadata, if it is available - const serverVersion = metadata.server - if (!this._connection.version) { - this._connection.version = serverVersion - } - - // read database connection id from the response metadata, if it is available - const dbConnectionId = metadata.connection_id - if (!this._connection.databaseId) { - this._connection.databaseId = dbConnectionId - } - } - - this._onSuccess(this._connection) - } -} diff --git a/src/internal/connection-holder.js b/src/internal/connection-holder.js index bd6cbba6b..84a7457f1 100644 --- a/src/internal/connection-holder.js +++ b/src/internal/connection-holder.js @@ -75,14 +75,10 @@ export default class ConnectionHolder { /** * Get the current connection promise. - * @param {StreamObserver} streamObserver an observer for this connection. * @return {Promise} promise resolved with the current connection. */ - getConnection (streamObserver) { - return this._connectionPromise.then(connection => { - streamObserver.resolveConnection(connection) - return connection - }) + getConnection () { + return this._connectionPromise } /** @@ -143,7 +139,7 @@ class EmptyConnectionHolder extends ConnectionHolder { // nothing to initialize } - getConnection (streamObserver) { + getConnection () { return Promise.reject( newError('This connection holder does not serve connections') ) diff --git a/src/internal/connection.js b/src/internal/connection.js index 5383beb76..f6baa4470 100644 --- a/src/internal/connection.js +++ b/src/internal/connection.js @@ -17,6 +17,9 @@ * limitations under the License. */ +import { ResultStreamObserver } from './stream-observers' +import BoltProtocol from './bolt-protocol-v1' + export default class Connection { /** * @param {ConnectionErrorHandler} errorHandler the error handler @@ -86,7 +89,7 @@ export default class Connection { /** * Write a message to the network channel. * @param {RequestMessage} message the message to write. - * @param {StreamObserver} observer the response observer. + * @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) { diff --git a/src/internal/connectivity-verifier.js b/src/internal/connectivity-verifier.js index b47d6f24b..b05332fc7 100644 --- a/src/internal/connectivity-verifier.js +++ b/src/internal/connectivity-verifier.js @@ -19,7 +19,7 @@ import ConnectionHolder from './connection-holder' import { READ } from '../driver' -import StreamObserver from './stream-observer' +import { ResultStreamObserver } from './stream-observers' /** * Verifies connectivity using the given connection provider. @@ -54,10 +54,9 @@ function acquireAndReleaseDummyConnection (connectionProvider, database) { connectionProvider }) connectionHolder.initializeConnection() - const dummyObserver = new StreamObserver() - const connectionPromise = connectionHolder.getConnection(dummyObserver) - return connectionPromise + return connectionHolder + .getConnection() .then(connection => { // able to establish a connection return connectionHolder.close().then(() => connection.server) diff --git a/src/internal/packstream-v1.js b/src/internal/packstream-v1.js index 596734551..c9783fc12 100644 --- a/src/internal/packstream-v1.js +++ b/src/internal/packstream-v1.js @@ -108,10 +108,9 @@ class Packer { /** * Creates a packable function out of the provided value * @param x the value to pack - * @param onError callback for the case when value cannot be packed * @returns Function */ - packable (x, onError) { + packable (x) { if (x === null) { return () => this._ch.writeUInt8(NULL) } else if (x === true) { @@ -121,39 +120,36 @@ class Packer { } else if (typeof x === 'number') { return () => this.packFloat(x) } else if (typeof x === 'string') { - return () => this.packString(x, onError) + return () => this.packString(x) } else if (isInt(x)) { return () => this.packInteger(x) } else if (x instanceof Int8Array) { - return () => this.packBytes(x, onError) + return () => this.packBytes(x) } else if (x instanceof Array) { return () => { - this.packListHeader(x.length, onError) + this.packListHeader(x.length) for (let i = 0; i < x.length; i++) { - this.packable(x[i] === undefined ? null : x[i], onError)() + this.packable(x[i] === undefined ? null : x[i])() } } } else if (isIterable(x)) { - return this.packableIterable(x, onError) + return this.packableIterable(x) } else if (x instanceof Node) { return this._nonPackableValue( - `It is not allowed to pass nodes in query parameters, given: ${x}`, - onError + `It is not allowed to pass nodes in query parameters, given: ${x}` ) } else if (x instanceof Relationship) { return this._nonPackableValue( - `It is not allowed to pass relationships in query parameters, given: ${x}`, - onError + `It is not allowed to pass relationships in query parameters, given: ${x}` ) } else if (x instanceof Path) { return this._nonPackableValue( - `It is not allowed to pass paths in query parameters, given: ${x}`, - onError + `It is not allowed to pass paths in query parameters, given: ${x}` ) } else if (x instanceof Structure) { var packableFields = [] for (var i = 0; i < x.fields.length; i++) { - packableFields[i] = this.packable(x.fields[i], onError) + packableFields[i] = this.packable(x.fields[i]) } return () => this.packStruct(x.signature, packableFields) } else if (typeof x === 'object') { @@ -166,30 +162,27 @@ class Packer { count++ } } - this.packMapHeader(count, onError) + this.packMapHeader(count) for (let i = 0; i < keys.length; i++) { let key = keys[i] if (x[key] !== undefined) { this.packString(key) - this.packable(x[key], onError)() + this.packable(x[key])() } } } } else { - return this._nonPackableValue( - `Unable to pack the given value: ${x}`, - onError - ) + return this._nonPackableValue(`Unable to pack the given value: ${x}`) } } - packableIterable (iterable, onError) { + packableIterable (iterable) { try { const array = Array.from(iterable) - return this.packable(array, onError) + return this.packable(array) } catch (e) { // handle errors from iterable to array conversion - onError(newError(`Cannot pack given iterable, ${e.message}: ${iterable}`)) + throw newError(`Cannot pack given iterable, ${e.message}: ${iterable}`) } } @@ -198,9 +191,9 @@ class Packer { * @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, onError) { + packStruct (signature, packableFields) { packableFields = packableFields || [] - this.packStructHeader(packableFields.length, signature, onError) + this.packStructHeader(packableFields.length, signature) for (let i = 0; i < packableFields.length; i++) { packableFields[i]() } @@ -232,7 +225,7 @@ class Packer { this._ch.writeFloat64(x) } - packString (x, onError) { + packString (x) { let bytes = utf8.encode(x) let size = bytes.length if (size < 0x10) { @@ -255,11 +248,11 @@ class Packer { this._ch.writeUInt8(size % 256) this._ch.writeBytes(bytes) } else { - onError(newError('UTF-8 strings of size ' + size + ' are not supported')) + throw newError('UTF-8 strings of size ' + size + ' are not supported') } } - packListHeader (size, onError) { + packListHeader (size) { if (size < 0x10) { this._ch.writeUInt8(TINY_LIST | size) } else if (size < 0x100) { @@ -276,26 +269,24 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Lists of size ' + size + ' are not supported')) + throw newError('Lists of size ' + size + ' are not supported') } } - packBytes (array, onError) { + packBytes (array) { if (this._byteArraysSupported) { - this.packBytesHeader(array.length, onError) + this.packBytesHeader(array.length) for (let i = 0; i < array.length; i++) { this._ch.writeInt8(array[i]) } } else { - onError( - newError( - 'Byte arrays are not supported by the database this driver is connected to' - ) + throw newError( + 'Byte arrays are not supported by the database this driver is connected to' ) } } - packBytesHeader (size, onError) { + packBytesHeader (size) { if (size < 0x100) { this._ch.writeUInt8(BYTES_8) this._ch.writeUInt8(size) @@ -310,11 +301,11 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Byte arrays of size ' + size + ' are not supported')) + throw newError('Byte arrays of size ' + size + ' are not supported') } } - packMapHeader (size, onError) { + packMapHeader (size) { if (size < 0x10) { this._ch.writeUInt8(TINY_MAP | size) } else if (size < 0x100) { @@ -331,11 +322,11 @@ class Packer { this._ch.writeUInt8(((size / 256) >> 0) % 256) this._ch.writeUInt8(size % 256) } else { - onError(newError('Maps of size ' + size + ' are not supported')) + throw newError('Maps of size ' + size + ' are not supported') } } - packStructHeader (size, signature, onError) { + packStructHeader (size, signature) { if (size < 0x10) { this._ch.writeUInt8(TINY_STRUCT | size) this._ch.writeUInt8(signature) @@ -348,7 +339,7 @@ class Packer { this._ch.writeUInt8((size / 256) >> 0) this._ch.writeUInt8(size % 256) } else { - onError(newError('Structures of size ' + size + ' are not supported')) + throw newError('Structures of size ' + size + ' are not supported') } } @@ -356,11 +347,10 @@ class Packer { this._byteArraysSupported = false } - _nonPackableValue (message, onError) { - if (onError) { - onError(newError(message, PROTOCOL_ERROR)) + _nonPackableValue (message) { + return () => { + throw newError(message, PROTOCOL_ERROR) } - return () => undefined } } diff --git a/src/internal/packstream-v2.js b/src/internal/packstream-v2.js index 641576183..d359d5018 100644 --- a/src/internal/packstream-v2.js +++ b/src/internal/packstream-v2.js @@ -72,23 +72,23 @@ export class Packer extends v1.Packer { throw new Error('Bolt V2 should always support byte arrays') } - packable (obj, onError) { + packable (obj) { if (isPoint(obj)) { - return () => packPoint(obj, this, onError) + return () => packPoint(obj, this) } else if (isDuration(obj)) { - return () => packDuration(obj, this, onError) + return () => packDuration(obj, this) } else if (isLocalTime(obj)) { - return () => packLocalTime(obj, this, onError) + return () => packLocalTime(obj, this) } else if (isTime(obj)) { - return () => packTime(obj, this, onError) + return () => packTime(obj, this) } else if (isDate(obj)) { - return () => packDate(obj, this, onError) + return () => packDate(obj, this) } else if (isLocalDateTime(obj)) { - return () => packLocalDateTime(obj, this, onError) + return () => packLocalDateTime(obj, this) } else if (isDateTime(obj)) { - return () => packDateTime(obj, this, onError) + return () => packDateTime(obj, this) } else { - return super.packable(obj, onError) + return super.packable(obj) } } } @@ -156,14 +156,13 @@ export class Unpacker extends v1.Unpacker { * Pack given 2D or 3D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint (point, packer, onError) { +function packPoint (point, packer) { const is2DPoint = point.z === null || point.z === undefined if (is2DPoint) { - packPoint2D(point, packer, onError) + packPoint2D(point, packer) } else { - packPoint3D(point, packer, onError) + packPoint3D(point, packer) } } @@ -171,31 +170,29 @@ function packPoint (point, packer, onError) { * Pack given 2D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint2D (point, packer, onError) { +function packPoint2D (point, packer) { const packableStructFields = [ - packer.packable(int(point.srid), onError), - packer.packable(point.x, onError), - packer.packable(point.y, onError) + packer.packable(int(point.srid)), + packer.packable(point.x), + packer.packable(point.y) ] - packer.packStruct(POINT_2D, packableStructFields, onError) + packer.packStruct(POINT_2D, packableStructFields) } /** * Pack given 3D point. * @param {Point} point the point value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packPoint3D (point, packer, onError) { +function packPoint3D (point, packer) { const packableStructFields = [ - packer.packable(int(point.srid), onError), - packer.packable(point.x, onError), - packer.packable(point.y, onError), - packer.packable(point.z, onError) + packer.packable(int(point.srid)), + packer.packable(point.x), + packer.packable(point.y), + packer.packable(point.z) ] - packer.packStruct(POINT_3D, packableStructFields, onError) + packer.packStruct(POINT_3D, packableStructFields) } /** @@ -238,21 +235,20 @@ function unpackPoint3D (unpacker, structSize, buffer) { * Pack given duration. * @param {Duration} value the duration value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDuration (value, packer, onError) { +function packDuration (value, packer) { const months = int(value.months) const days = int(value.days) const seconds = int(value.seconds) const nanoseconds = int(value.nanoseconds) const packableStructFields = [ - packer.packable(months, onError), - packer.packable(days, onError), - packer.packable(seconds, onError), - packer.packable(nanoseconds, onError) + packer.packable(months), + packer.packable(days), + packer.packable(seconds), + packer.packable(nanoseconds) ] - packer.packStruct(DURATION, packableStructFields, onError) + packer.packStruct(DURATION, packableStructFields) } /** @@ -277,9 +273,8 @@ function unpackDuration (unpacker, structSize, buffer) { * Pack given local time. * @param {LocalTime} value the local time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packLocalTime (value, packer, onError) { +function packLocalTime (value, packer) { const nanoOfDay = localTimeToNanoOfDay( value.hour, value.minute, @@ -287,8 +282,8 @@ function packLocalTime (value, packer, onError) { value.nanosecond ) - const packableStructFields = [packer.packable(nanoOfDay, onError)] - packer.packStruct(LOCAL_TIME, packableStructFields, onError) + const packableStructFields = [packer.packable(nanoOfDay)] + packer.packStruct(LOCAL_TIME, packableStructFields) } /** @@ -316,9 +311,8 @@ function unpackLocalTime ( * Pack given time. * @param {Time} value the time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packTime (value, packer, onError) { +function packTime (value, packer) { const nanoOfDay = localTimeToNanoOfDay( value.hour, value.minute, @@ -328,10 +322,10 @@ function packTime (value, packer, onError) { const offsetSeconds = int(value.timeZoneOffsetSeconds) const packableStructFields = [ - packer.packable(nanoOfDay, onError), - packer.packable(offsetSeconds, onError) + packer.packable(nanoOfDay), + packer.packable(offsetSeconds) ] - packer.packStruct(TIME, packableStructFields, onError) + packer.packStruct(TIME, packableStructFields) } /** @@ -363,13 +357,12 @@ function unpackTime (unpacker, structSize, buffer, disableLosslessIntegers) { * Pack given neo4j date. * @param {Date} value the date value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDate (value, packer, onError) { +function packDate (value, packer) { const epochDay = dateToEpochDay(value.year, value.month, value.day) - const packableStructFields = [packer.packable(epochDay, onError)] - packer.packStruct(DATE, packableStructFields, onError) + const packableStructFields = [packer.packable(epochDay)] + packer.packStruct(DATE, packableStructFields) } /** @@ -392,9 +385,8 @@ function unpackDate (unpacker, structSize, buffer, disableLosslessIntegers) { * Pack given local date time. * @param {LocalDateTime} value the local date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packLocalDateTime (value, packer, onError) { +function packLocalDateTime (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -407,10 +399,10 @@ function packLocalDateTime (value, packer, onError) { const nano = int(value.nanosecond) const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError) + packer.packable(epochSecond), + packer.packable(nano) ] - packer.packStruct(LOCAL_DATE_TIME, packableStructFields, onError) + packer.packStruct(LOCAL_DATE_TIME, packableStructFields) } /** @@ -443,13 +435,12 @@ function unpackLocalDateTime ( * Pack given date time. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTime (value, packer, onError) { +function packDateTime (value, packer) { if (value.timeZoneId) { - packDateTimeWithZoneId(value, packer, onError) + packDateTimeWithZoneId(value, packer) } else { - packDateTimeWithZoneOffset(value, packer, onError) + packDateTimeWithZoneOffset(value, packer) } } @@ -457,9 +448,8 @@ function packDateTime (value, packer, onError) { * Pack given date time with zone offset. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTimeWithZoneOffset (value, packer, onError) { +function packDateTimeWithZoneOffset (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -473,11 +463,11 @@ function packDateTimeWithZoneOffset (value, packer, onError) { const timeZoneOffsetSeconds = int(value.timeZoneOffsetSeconds) const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError), - packer.packable(timeZoneOffsetSeconds, onError) + packer.packable(epochSecond), + packer.packable(nano), + packer.packable(timeZoneOffsetSeconds) ] - packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields, onError) + packer.packStruct(DATE_TIME_WITH_ZONE_OFFSET, packableStructFields) } /** @@ -523,9 +513,8 @@ function unpackDateTimeWithZoneOffset ( * Pack given date time with zone id. * @param {DateTime} value the date time value to pack. * @param {Packer} packer the packer to use. - * @param {function} onError the error callback. */ -function packDateTimeWithZoneId (value, packer, onError) { +function packDateTimeWithZoneId (value, packer) { const epochSecond = localDateTimeToEpochSecond( value.year, value.month, @@ -539,11 +528,11 @@ function packDateTimeWithZoneId (value, packer, onError) { const timeZoneId = value.timeZoneId const packableStructFields = [ - packer.packable(epochSecond, onError), - packer.packable(nano, onError), - packer.packable(timeZoneId, onError) + packer.packable(epochSecond), + packer.packable(nano), + packer.packable(timeZoneId) ] - packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields, onError) + packer.packStruct(DATE_TIME_WITH_ZONE_ID, packableStructFields) } /** diff --git a/src/internal/routing-util.js b/src/internal/routing-util.js index 8ab0ec835..de09f8826 100644 --- a/src/internal/routing-util.js +++ b/src/internal/routing-util.js @@ -132,7 +132,7 @@ export default class RoutingUtil { } _callAvailableRoutingProcedure (session, database) { - return session._run(null, null, (connection, streamObserver) => { + return session._run(null, null, connection => { let query let params @@ -148,11 +148,12 @@ export default class RoutingUtil { params = { context: this._routingContext } } - connection.protocol().run(query, params, streamObserver, { + return connection.protocol().run(query, params, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), mode: session._mode, - database: session._database + database: session._database, + afterComplete: session._onComplete }) }) } diff --git a/src/internal/stream-observer.js b/src/internal/stream-observer.js deleted file mode 100644 index 0bcc98df1..000000000 --- a/src/internal/stream-observer.js +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (c) 2002-2019 "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' - -/** - * 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 StreamObserver { - constructor () { - this._fieldKeys = null - this._fieldLookup = null - this._queuedRecords = [] - this._tail = null - this._error = null - this._hasFailed = false - this._observer = null - this._conn = null - this._meta = {} - } - - /** - * 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) { - let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) - if (this._observer) { - this._observer.onNext(record) - } else { - this._queuedRecords.push(record) - } - } - - onCompleted (meta) { - 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 - } - } - } else { - // End of stream - if (this._observer) { - this._observer.onCompleted(meta) - } else { - this._tail = meta - } - } - this._copyMetadataOnCompletion(meta) - } - - _copyMetadataOnCompletion (meta) { - for (var key in meta) { - if (meta.hasOwnProperty(key)) { - this._meta[key] = meta[key] - } - } - } - - serverMetadata () { - const serverMeta = { server: this._conn.server } - return Object.assign({}, this._meta, serverMeta) - } - - resolveConnection (conn) { - this._conn = conn - } - - /** - * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. - * Response for RUN initializes statement 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._fieldKeys = [] - } - - /** - * Mark this observer as if it has completed with no metadata. - */ - markCompleted () { - this._fieldKeys = [] - this._tail = {} - } - - /** - * 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) { - if (this._hasFailed) { - return - } - this._hasFailed = true - - if (this._observer) { - if (this._observer.onError) { - this._observer.onError(error) - } else { - console.log(error) - } - } else { - this._error = error - } - } - - /** - * Subscribe to events with provided observer. - * @param {Object} observer - Observer object - * @param {function(record: Object)} observer.onNext - Handle records, one by one. - * @param {function(metadata: Object)} observer.onComplete - Handle stream tail, the metadata. - * @param {function(error: Object)} observer.onError - Handle errors. - */ - subscribe (observer) { - if (this._error) { - observer.onError(this._error) - return - } - if (this._queuedRecords.length > 0) { - for (let i = 0; i < this._queuedRecords.length; i++) { - observer.onNext(this._queuedRecords[i]) - } - } - if (this._tail) { - observer.onCompleted(this._tail) - } - this._observer = observer - } - - hasFailed () { - return this._hasFailed - } -} - -export default StreamObserver diff --git a/src/internal/stream-observers.js b/src/internal/stream-observers.js new file mode 100644 index 000000000..06267ff03 --- /dev/null +++ b/src/internal/stream-observers.js @@ -0,0 +1,473 @@ +/** + * Copyright (c) 2002-2019 "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' +import Connection from './connection' +import { newError, PROTOCOL_ERROR } from '../error' +import { isString } from './util' + +const DefaultBatchSize = 100 + +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 {Connection} connection + * @param {} param.moreFunction - + * @param {} param.discardFunction - + * @param {} param.batchSize - + * @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 ({ + connection, + moreFunction, + discardFunction, + batchSize = DefaultBatchSize, + beforeError, + afterError, + beforeKeys, + afterKeys, + beforeComplete, + afterComplete + } = {}) { + super() + + this._connection = connection + + this._fieldKeys = null + this._fieldLookup = null + this._head = null + this._queuedRecords = [] + this._tail = null + this._error = null + this._hasFailed = false + this._observers = [] + this._meta = {} + + this._beforeError = beforeError + this._afterError = afterError + this._beforeKeys = beforeKeys + this._afterKeys = afterKeys + this._beforeComplete = beforeComplete + this._afterComplete = afterComplete + + this._statementId = null + this._moreFunction = moreFunction + this._discardFunction = discardFunction + this._discard = false + this._batchSize = batchSize + } + + /** + * 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) { + let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) + if (this._observers.some(o => o.onNext)) { + this._observers.forEach(o => (o.onNext ? o.onNext(record) : {})) + } else { + this._queuedRecords.push(record) + } + } + + onCompleted (meta) { + 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) { + this._statementId = 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 => + o.onKeys ? o.onKeys(this._fieldKeys) : {} + ) + } + + if (this._afterKeys) { + this._afterKeys(this._fieldKeys) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } else { + if (meta.has_more) { + // 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 + if (this._discard) { + this._discardFunction({ + connection: this._connection, + statementId: this._statementId + }) + } else { + this._moreFunction({ + connection: this._connection, + statementId: this._statementId, + n: this._batchSize + }) + } + + delete meta.has_more + } else { + const completionMetadata = Object.assign( + this._connection ? { server: this._connection.server } : {}, + this._meta, + meta + ) + + 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 => + o.onCompleted ? o.onCompleted(completionMetadata) : {} + ) + } + + if (this._afterComplete) { + this._afterComplete(completionMetadata) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + } + } + + _storeMetadataForCompletion (meta) { + const keys = Object.keys(meta) + let index = keys.length + let key = '' + + while (index--) { + key = keys[index] + this._meta[key] = meta[key] + } + } + + /** + * Stream observer defaults to handling responses for two messages: RUN + PULL_ALL or RUN + DISCARD_ALL. + * Response for RUN initializes statement 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 = [] + } + + /** + * Mark this observer as if it has completed with no metadata. + */ + markCompleted () { + this._head = [] + this._fieldKeys = [] + this._tail = {} + } + + /** + * Discard pending record stream + */ + discard () { + this._discard = true + } + + /** + * 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) { + if (this._hasFailed) { + return + } + + this._hasFailed = true + 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 => + o.onError ? o.onError(error) : console.log(error) + ) + } + + if (this._afterError) { + this._afterError(error) + } + } + + if (beforeHandlerResult) { + Promise.resolve(beforeHandlerResult).then(() => continuation()) + } else { + continuation() + } + } + + /** + * 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._error) { + observer.onError(this._error) + return + } + 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._tail && observer.onCompleted) { + observer.onCompleted(this._tail) + } + this._observers.push(observer) + } + + hasFailed () { + return this._hasFailed + } +} + +class LoginObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {Connection} param.connection + * @param {function(err: Error)} param.beforeError + * @param {function(err: Error)} param.afterError + * @param {function(metadata)} param.beforeComplete + * @param {function(metadata)} param.afterComplete + */ + constructor ({ + connection, + beforeError, + afterError, + beforeComplete, + afterComplete + } = {}) { + super() + + this._connection = connection + this._beforeError = beforeError + this._afterError = afterError + this._beforeComplete = beforeComplete + this._afterComplete = afterComplete + } + + onNext (record) { + this.onError( + newError('Received RECORD when initializing ' + JSON.stringify(record)) + ) + } + + onError (error) { + if (this._beforeError) { + this._beforeError(error) + } + + this._connection._updateCurrentObserver() // make sure this exact observer will not be called again + this._connection._handleFatalError(error) // initialization errors are fatal + + if (this._afterError) { + this._afterError(error) + } + } + + onCompleted (metadata) { + if (this._beforeComplete) { + this._beforeComplete(metadata) + } + + if (metadata) { + // read server version from the response metadata, if it is available + const serverVersion = metadata.server + if (!this._connection.version) { + this._connection.version = serverVersion + } + + // read database connection id from the response metadata, if it is available + const dbConnectionId = metadata.connection_id + if (!this._connection.databaseId) { + this._connection.databaseId = dbConnectionId + } + } + + if (this._afterComplete) { + this._afterComplete(metadata) + } + } +} + +class ResetObserver extends StreamObserver { + /** + * + * @param {Object} param - + * @param {Connection} param.connection + * @param {function(err: Error)} param.onError + * @param {function(metadata)} param.onComplete + */ + constructor ({ connection, onError, onComplete } = {}) { + super() + + this._connection = connection + 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._connection._handleProtocolError(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 }) + + if (error instanceof Error) { + this.onError(error) + } else if (isString(error)) { + this.onError({ error: error }) + } + } +} + +class CompletedObserver extends ResultStreamObserver { + constructor () { + super() + super.markCompleted() + } +} + +export { + StreamObserver, + ResultStreamObserver, + LoginObserver, + ResetObserver, + FailedObserver, + CompletedObserver +} diff --git a/src/result.js b/src/result.js index a09f55b0c..766cb9268 100644 --- a/src/result.js +++ b/src/result.js @@ -19,11 +19,13 @@ import ResultSummary from './result-summary' import { EMPTY_CONNECTION_HOLDER } from './internal/connection-holder' +import { ResultStreamObserver } from './internal/stream-observers' const DEFAULT_ON_ERROR = error => { console.log('Uncaught error when processing result: ' + error) } const DEFAULT_ON_COMPLETED = summary => {} +const DEFAULT_METADATA_SUPPLIER = metadata => {} /** * A stream of {@link Record} representing the result of a statement. @@ -37,57 +39,67 @@ class Result { * Inject the observer to be used. * @constructor * @access private - * @param {StreamObserver} streamObserver + * @param {Promise} streamObserverPromise * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement - * @param metaSupplier function, when called provides metadata * @param {ConnectionHolder} connectionHolder - to be notified when result is either fully consumed or error happened. */ - constructor ( - streamObserver, - statement, - parameters, - metaSupplier, - connectionHolder - ) { + constructor (streamObserverPromise, statement, parameters, connectionHolder) { this._stack = captureStacktrace() - this._streamObserver = streamObserver + this._streamObserverPromise = streamObserverPromise this._p = null this._statement = statement this._parameters = parameters || {} - this._metaSupplier = - metaSupplier || - function () { - return {} - } this._connectionHolder = connectionHolder || EMPTY_CONNECTION_HOLDER } + keys () { + return new Promise((resolve, reject) => { + this._streamObserverPromise.then(observer => + observer.subscribe({ + onKeys: keys => resolve(keys), + onError: err => reject(err) + }) + ) + }) + } + + summary () { + return new Promise((resolve, reject) => { + this._streamObserverPromise.then(o => + o.subscribe({ + onCompleted: metadata => resolve(metadata), + onError: err => reject(err) + }) + ) + }) + } + /** * Create and return new Promise * @return {Promise} new Promise. * @access private */ - _createPromise () { - if (this._p) { - return - } - let self = this - this._p = new Promise((resolve, reject) => { - let records = [] - let observer = { - onNext: record => { - records.push(record) - }, - onCompleted: summary => { - resolve({ records: records, summary: summary }) - }, - onError: error => { - reject(error) + _getOrCreatePromise () { + if (!this._p) { + this._p = new Promise((resolve, reject) => { + let records = [] + let observer = { + onNext: record => { + records.push(record) + }, + onCompleted: summary => { + resolve({ records: records, summary: summary }) + }, + onError: error => { + reject(error) + } } - } - self.subscribe(observer) - }) + this.subscribe(observer) + }) + } + + return this._p } /** @@ -100,8 +112,7 @@ class Result { * @return {Promise} promise. */ then (onFulfilled, onRejected) { - this._createPromise() - return this._p.then(onFulfilled, onRejected) + return this._getOrCreatePromise().then(onFulfilled, onRejected) } /** @@ -111,8 +122,7 @@ class Result { * @return {Promise} promise. */ catch (onRejected) { - this._createPromise() - return this._p.catch(onRejected) + return this._getOrCreatePromise().catch(onRejected) } /** @@ -126,22 +136,15 @@ class Result { * @return */ subscribe (observer) { - const self = this - const onCompletedOriginal = observer.onCompleted || DEFAULT_ON_COMPLETED const onCompletedWrapper = metadata => { - const additionalMeta = self._metaSupplier() - for (let key in additionalMeta) { - if (additionalMeta.hasOwnProperty(key)) { - metadata[key] = additionalMeta[key] - } - } - const sum = new ResultSummary(this._statement, this._parameters, metadata) - // notify connection holder that the used connection is not needed any more because result has // been fully consumed; call the original onCompleted callback after that - self._connectionHolder.releaseConnection().then(() => { - onCompletedOriginal.call(observer, sum) + this._connectionHolder.releaseConnection().then(() => { + onCompletedOriginal.call( + observer, + new ResultSummary(this._statement, this._parameters, metadata) + ) }) } observer.onCompleted = onCompletedWrapper @@ -150,14 +153,14 @@ class Result { const onErrorWrapper = error => { // 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 - self._connectionHolder.releaseConnection().then(() => { + this._connectionHolder.releaseConnection().then(() => { replaceStacktrace(error, this._stack) onErrorOriginal.call(observer, error) }) } observer.onError = onErrorWrapper - this._streamObserver.subscribe(observer) + this._streamObserverPromise.then(o => o.subscribe(observer)) } } diff --git a/src/session.js b/src/session.js index 94ef91982..48e31fc09 100644 --- a/src/session.js +++ b/src/session.js @@ -16,7 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import StreamObserver from './internal/stream-observer' +import { + ResultStreamObserver, + FailedObserver +} from './internal/stream-observers' import Result from './result' import Transaction from './transaction' import { newError } from './error' @@ -80,6 +83,7 @@ class Session { this._hasTx = false this._lastBookmark = bookmark this._transactionExecutor = _createTransactionExecutor(config) + this._onComplete = this._onCompleteCallback.bind(this) } /** @@ -100,41 +104,39 @@ class Session { ? new TxConfig(transactionConfig) : TxConfig.empty() - return this._run(query, params, (connection, streamObserver) => - connection.protocol().run(query, params, streamObserver, { + return this._run(query, params, connection => + connection.protocol().run(query, params, { bookmark: this._lastBookmark, txConfig: autoCommitTxConfig, mode: this._mode, - database: this._database + database: this._database, + afterComplete: this._onComplete }) ) } - _run (statement, parameters, statementRunner) { - const streamObserver = new SessionStreamObserver(this) + _run (statement, parameters, customRunner) { const connectionHolder = this._connectionHolderWithMode(this._mode) + + let observerPromise if (!this._hasTx) { connectionHolder.initializeConnection() - connectionHolder - .getConnection(streamObserver) - .then(connection => statementRunner(connection, streamObserver)) - .catch(error => streamObserver.onError(error)) + observerPromise = connectionHolder + .getConnection() + .then(connection => customRunner(connection)) + .catch(error => Promise.resolve(new FailedObserver({ error }))) } else { - streamObserver.onError( - newError( - 'Statements cannot be run directly on a ' + - 'session with an open transaction; either run from within the ' + - 'transaction or use a different session.' - ) + observerPromise = Promise.resolve( + new FailedObserver({ + error: newError( + 'Statements cannot be run directly on a ' + + 'session with an open transaction; either run from within the ' + + 'transaction or use a different session.' + ) + }) ) } - return new Result( - streamObserver, - statement, - parameters, - () => streamObserver.serverMetadata(), - connectionHolder - ) + return new Result(observerPromise, statement, parameters, connectionHolder) } /** @@ -252,20 +254,15 @@ class Session { /** * Close this session. - * @param {function()} callback - Function to be called after the session has been closed - * @return + * @return ${Promise} */ - close (callback = () => null) { + async close () { if (this._open) { this._open = false this._transactionExecutor.close() - this._readConnectionHolder.close().then(() => { - this._writeConnectionHolder.close().then(() => { - callback() - }) - }) - } else { - callback() + + await this._readConnectionHolder.close() + await this._writeConnectionHolder.close() } } @@ -278,21 +275,9 @@ class Session { throw newError('Unknown access mode: ' + mode) } } -} - -/** - * @private - */ -class SessionStreamObserver extends StreamObserver { - constructor (session) { - super() - this._session = session - } - onCompleted (meta) { - super.onCompleted(meta) - const bookmark = new Bookmark(meta.bookmark) - this._session._updateBookmark(bookmark) + _onCompleteCallback (meta) { + this._updateBookmark(new Bookmark(meta.bookmark)) } } diff --git a/src/transaction.js b/src/transaction.js index 68f6724db..edc9a103c 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -16,13 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import StreamObserver from './internal/stream-observer' import Result from './result' import { validateStatementAndParameters } from './internal/util' -import { EMPTY_CONNECTION_HOLDER } from './internal/connection-holder' +import ConnectionHolder, { + EMPTY_CONNECTION_HOLDER +} from './internal/connection-holder' import Bookmark from './internal/bookmark' import TxConfig from './internal/tx-config' +import { + ResultStreamObserver, + FailedObserver, + CompletedObserver +} from './internal/stream-observers' + /** * Represents a transaction in the Neo4j database. * @@ -40,22 +47,24 @@ class Transaction { this._state = _states.ACTIVE this._onClose = onClose this._onBookmark = onBookmark + this._onError = this._onErrorCallback.bind(this) + this._onComplete = this._onCompleteCallback.bind(this) } _begin (bookmark, txConfig) { - const streamObserver = new _TransactionStreamObserver(this) - this._connectionHolder - .getConnection(streamObserver) + .getConnection() .then(conn => - conn.protocol().beginTransaction(streamObserver, { + conn.protocol().beginTransaction({ bookmark: bookmark, txConfig: txConfig, mode: this._connectionHolder.mode(), - database: this._connectionHolder.database() + database: this._connectionHolder.database(), + beforeError: this._onError, + afterComplete: this._onComplete }) ) - .catch(error => streamObserver.onError(error)) + .catch(error => this._onError(error)) } /** @@ -72,12 +81,11 @@ class Transaction { parameters ) - return this._state.run( - this._connectionHolder, - new _TransactionStreamObserver(this), - query, - params - ) + return this._state.run(query, params, { + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete + }) } /** @@ -88,10 +96,11 @@ class Transaction { * @returns {Result} New Result */ commit () { - let committed = this._state.commit( - this._connectionHolder, - new _TransactionStreamObserver(this) - ) + let committed = this._state.commit({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete + }) this._state = committed.state // clean up this._onClose() @@ -106,10 +115,11 @@ class Transaction { * @returns {Result} New Result */ rollback () { - let committed = this._state.rollback( - this._connectionHolder, - new _TransactionStreamObserver(this) - ) + let committed = this._state.rollback({ + connectionHolder: this._connectionHolder, + onError: this._onError, + onComplete: this._onComplete + }) this._state = committed.state // clean up this._onClose() @@ -124,7 +134,7 @@ class Transaction { return this._state === _states.ACTIVE } - _onError () { + _onErrorCallback (err) { // 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 @@ -134,232 +144,226 @@ class Transaction { // release connection back to the pool return this._connectionHolder.releaseConnection() } -} - -/** Internal stream observer used for transactional results */ -class _TransactionStreamObserver extends StreamObserver { - constructor (tx) { - super() - this._tx = tx - } - onError (error) { - if (!this._hasFailed) { - this._tx._onError().then(() => { - super.onError(error) - }) - } - } - - onCompleted (meta) { - super.onCompleted(meta) - const bookmark = new Bookmark(meta.bookmark) - this._tx._onBookmark(bookmark) + _onCompleteCallback (meta) { + this._onBookmark(new Bookmark(meta.bookmark)) } } -/** internal state machine of the transaction */ let _states = { // The transaction is running with no explicit success or failure marked ACTIVE: { - commit: (connectionHolder, observer) => { + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: finishTransaction(true, connectionHolder, observer), + result: finishTransaction(true, connectionHolder, onError, onComplete), state: _states.SUCCEEDED } }, - rollback: (connectionHolder, observer) => { + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: finishTransaction(false, connectionHolder, observer), + result: finishTransaction(false, connectionHolder, onError, onComplete), state: _states.ROLLED_BACK } }, - run: (connectionHolder, observer, statement, parameters) => { + run: (statement, parameters, { connectionHolder, onError, onComplete }) => { // RUN in explicit transaction can't contain bookmarks and transaction configuration - const bookmark = Bookmark.empty() - const txConfig = TxConfig.empty() - - connectionHolder - .getConnection(observer) + const observerPromise = connectionHolder + .getConnection() .then(conn => - conn.protocol().run(statement, parameters, observer, { - bookmark: bookmark, - txConfig: txConfig, + conn.protocol().run(statement, parameters, { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), mode: connectionHolder.mode(), - database: connectionHolder.database() + database: connectionHolder.database(), + beforeError: onError, + afterComplete: onComplete }) ) - .catch(error => observer.onError(error)) + .catch(error => new FailedObserver({ error, onError })) - return _newRunResult(observer, statement, parameters, () => - observer.serverMetadata() - ) + return newCompletedResult(observerPromise, statement, parameters) } }, // An error has occurred, transaction can no longer be used and no more messages will // be sent for this transaction. FAILED: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit statements in this transaction, because previous statements in the ' + - 'transaction has failed and the transaction has been rolled back. Please start a new' + - ' transaction to run another statement.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit statements in this transaction, because previous statements in the ' + + 'transaction has failed and the transaction has been rolled back. Please start a new ' + + 'transaction to run another statement.', + onError + }), + 'COMMIT', + {} + ), state: _states.FAILED } }, - rollback: (connectionHolder, observer) => { - observer.markCompleted() + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult(new CompletedObserver(), 'ROLLBACK', {}), state: _states.FAILED } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because previous statements in the ' + - 'transaction has failed and the transaction has already been rolled back.' - }) - return _newDummyResult(observer, statement, parameters) + run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because previous statements in the ' + + 'transaction has failed and the transaction has already been rolled back.', + onError + }), + statement, + parameters + ) } }, // This transaction has successfully committed SUCCEEDED: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit statements in this transaction, because commit has already been successfully called on the transaction and transaction has been closed. Please start a new' + - ' transaction to run another statement.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit statements in this transaction, because commit has already been ' + + 'successfully called on the transaction and transaction has been closed. Please ' + + 'start a new transaction to run another statement.', + onError + }), + 'COMMIT', + {} + ), state: _states.SUCCEEDED } }, - rollback: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot rollback transaction, because transaction has already been successfully closed.' - }) + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot rollback transaction, because transaction has already been successfully closed.', + onError + }), + 'ROLLBACK', + {} + ), state: _states.SUCCEEDED } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because transaction has already been successfully closed.' - }) - return _newDummyResult(observer, statement, parameters) + run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because transaction has already been successfully closed.', + onError + }), + statement, + parameters + ) } }, // This transaction has been rolled back ROLLED_BACK: { - commit: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot commit this transaction, because it has already been rolled back.' - }) + commit: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'COMMIT', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot commit this transaction, because it has already been rolled back.', + onError + }), + 'COMMIT', + {} + ), state: _states.ROLLED_BACK } }, - rollback: (connectionHolder, observer) => { - observer.onError({ - error: - 'Cannot rollback transaction, because transaction has already been rolled back.' - }) + rollback: ({ connectionHolder, onError, onComplete }) => { return { - result: _newDummyResult(observer, 'ROLLBACK', {}), + result: newCompletedResult( + new FailedObserver({ + error: + 'Cannot rollback transaction, because transaction has already been rolled back.' + }), + 'ROLLBACK', + {} + ), state: _states.ROLLED_BACK } }, - run: (connectionHolder, observer, statement, parameters) => { - observer.onError({ - error: - 'Cannot run statement, because transaction has already been rolled back.' - }) - return _newDummyResult(observer, statement, parameters) + run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + return newCompletedResult( + new FailedObserver({ + error: + 'Cannot run statement, because transaction has already been rolled back.', + onError + }), + statement, + parameters + ) } } } -function finishTransaction (commit, connectionHolder, observer) { - connectionHolder - .getConnection(observer) +/** + * + * @param {boolean} commit + * @param {ConnectionHolder} connectionHolder + * @param {function(err:Error): any} onError + * @param {function(metadata:object): any} onComplete + */ +function finishTransaction (commit, connectionHolder, onError, onComplete) { + const observerPromise = connectionHolder + .getConnection() .then(connection => { if (commit) { - return connection.protocol().commitTransaction(observer) + return connection.protocol().commitTransaction({ + beforeError: onError, + afterComplete: onComplete + }) } else { - return connection.protocol().rollbackTransaction(observer) + return connection.protocol().rollbackTransaction({ + beforeError: onError, + afterComplete: onComplete + }) } }) - .catch(error => observer.onError(error)) + .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( - observer, + observerPromise, commit ? 'COMMIT' : 'ROLLBACK', {}, - emptyMetadataSupplier, connectionHolder ) } /** * Creates a {@link Result} with empty connection holder. - * Should be used as a result for running cypher statements. They can result in metadata but should not - * influence real connection holder to release connections because single transaction can have - * {@link Transaction#run} called multiple times. - * @param {StreamObserver} observer - an observer for the created result. - * @param {string} statement - the cypher statement that produced the result. - * @param {object} parameters - the parameters for cypher statement that produced the result. - * @param {function} metadataSupplier - the function that returns a metadata object. - * @return {Result} new result. - * @private - */ -function _newRunResult (observer, statement, parameters, metadataSupplier) { - return new Result( - observer, - statement, - parameters, - metadataSupplier, - EMPTY_CONNECTION_HOLDER - ) -} - -/** - * Creates a {@link Result} without metadata supplier and 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 {StreamObserver} observer - an observer for the created result. + * @param {ResultStreamObserver} observer - an observer for the created result. * @param {string} statement - the cypher statement that produced the result. * @param {object} parameters - the parameters for cypher statement that produced the result. * @return {Result} new result. * @private */ -function _newDummyResult (observer, statement, parameters) { +function newCompletedResult (observerPromise, statement, parameters) { return new Result( - observer, + Promise.resolve(observerPromise), statement, parameters, - emptyMetadataSupplier, EMPTY_CONNECTION_HOLDER ) } -function emptyMetadataSupplier () { - return {} -} - export default Transaction diff --git a/test/examples.test.js b/test/examples.test.js index 370484c21..d7fde7492 100644 --- a/test/examples.test.js +++ b/test/examples.test.js @@ -27,13 +27,16 @@ import sharedNeo4j from './internal/shared-neo4j' * DO NOT add tests to this file that are not for that exact purpose. * DO NOT modify these tests without ensuring they remain consistent with the equivalent examples in other drivers */ + describe('#integration examples', () => { + const originalConsole = console + let driverGlobal - let console let originalTimeout - let testResultPromise - let resolveTestResultPromise + let consoleOverride + let consoleOverridePromise + let consoleOverridePromiseResolve const user = sharedNeo4j.username const password = sharedNeo4j.password @@ -46,20 +49,18 @@ describe('#integration examples', () => { driverGlobal = neo4j.driver(uri, sharedNeo4j.authToken) }) - beforeEach(done => { - testResultPromise = new Promise((resolve, reject) => { - resolveTestResultPromise = resolve + beforeEach(async () => { + consoleOverridePromise = new Promise((resolve, reject) => { + consoleOverridePromiseResolve = resolve }) - - // Override console.log, to assert on stdout output - console = { log: resolveTestResultPromise } + consoleOverride = { log: msg => consoleOverridePromiseResolve(msg) } const session = driverGlobal.session() - session.run('MATCH (n) DETACH DELETE n').then(() => { - session.close(() => { - done() - }) - }) + try { + await session.run('MATCH (n) DETACH DELETE n') + } finally { + await session.close() + } }) afterAll(() => { @@ -67,35 +68,37 @@ describe('#integration examples', () => { driverGlobal.close() }) - it('autocommit transaction example', done => { + it('autocommit transaction example', async () => { const driver = driverGlobal // tag::autocommit-transaction[] - function addPerson (name) { + async function addPerson (name) { const session = driver.session() - return session - .run('CREATE (a:Person {name: $name})', { name: name }) - .then(result => { - session.close() - return result + try { + return await session.run('CREATE (a:Person {name: $name})', { + name: name }) + } finally { + await session.close() + } } // end::autocommit-transaction[] - addPerson('Alice').then(() => { - const session = driver.session() - session - .run('MATCH (a:Person {name: $name}) RETURN count(a) AS result', { + await addPerson('Alice') + + const session = driver.session() + try { + const result = await session.run( + 'MATCH (a:Person {name: $name}) RETURN count(a) AS result', + { name: 'Alice' - }) - .then(result => { - session.close(() => { - expect(result.records[0].get('result').toInt()).toEqual(1) - done() - }) - }) - }) + } + ) + expect(result.records[0].get('result').toInt()).toEqual(1) + } finally { + await session.close() + } }) it('basic auth example', done => { @@ -238,270 +241,253 @@ describe('#integration examples', () => { driver.close() }) - it('cypher error example', done => { + it('cypher error example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Bob' // tag::cypher-error[] const session = driver.session() - - const readTxPromise = session.readTransaction(tx => - tx.run('SELECT * FROM Employees WHERE name = $name', { name: personName }) - ) - - readTxPromise.catch(error => { - session.close() + try { + await session.readTransaction(tx => + tx.run('SELECT * FROM Employees WHERE name = $name', { + name: personName + }) + ) + } catch (error) { console.log(error.message) - }) + } finally { + await session.close() + } // end::cypher-error[] - testResultPromise.then(loggedMsg => { - expect(removeLineBreaks(loggedMsg)).toBe( - removeLineBreaks( - "Invalid input 'L': expected 't/T' (line 1, column 3 (offset: 2))\n" + - '"SELECT * FROM Employees WHERE name = $name"\n' + - ' ^' - ) + expect(removeLineBreaks(await consoleLoggedMsg)).toBe( + removeLineBreaks( + "Invalid input 'L': expected 't/T' (line 1, column 3 (offset: 2))\n" + + '"SELECT * FROM Employees WHERE name = $name"\n' + + ' ^' ) - done() - }) + ) }) - it('driver lifecycle example', done => { + it('driver lifecycle example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise + // tag::driver-lifecycle[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) - driver - .verifyConnectivity() - .then(() => { - console.log('Driver created') - }) - .catch(error => { - console.log(`connectivity verification failed. ${error}`) - }) + try { + await driver.verifyConnectivity() + console.log('Driver created') + } catch (error) { + console.log(`connectivity verification failed. ${error}`) + } const session = driver.session() - session - .run('CREATE (i:Item)') - .then(() => { - session.close() + try { + await session.run('CREATE (i:Item)') + } catch (error) { + console.log(`unable to execute statement. ${error}`) + } finally { + await session.close() + } - // ... on application exit: - driver.close() - }) - .catch(error => { - console.log(`unable to execute statement. ${error}`) - }) + // ... on application exit: + driver.close() // end::driver-lifecycle[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual('Driver created') - done() - }) + expect(await consoleLoggedMsg).toEqual('Driver created') }) - it('hello world example', done => { + it('hello world example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise // tag::hello-world[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) const session = driver.session() - const resultPromise = session.writeTransaction(tx => - tx.run( - 'CREATE (a:Greeting) SET a.message = $message RETURN a.message + ", from node " + id(a)', - { message: 'hello, world' } + try { + const result = await session.writeTransaction(tx => + tx.run( + 'CREATE (a:Greeting) SET a.message = $message RETURN a.message + ", from node " + id(a)', + { message: 'hello, world' } + ) ) - ) - - resultPromise.then(result => { - session.close() const singleRecord = result.records[0] const greeting = singleRecord.get(0) console.log(greeting) + } finally { + await session.close() + } - // on application exit: - driver.close() - }) + // on application exit: + driver.close() // end::hello-world[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg.indexOf('hello, world, from node') === 0).toBeTruthy() - done() - }) + expect(await consoleLoggedMsg).toContain('hello, world, from node') }) const require = () => { return neo4j } - it('language guide page example', done => { + it('language guide page example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise // tag::language-guide-page[] const neo4j = require('neo4j-driver') const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) const session = driver.session() - const personName = 'Alice' - const resultPromise = session.run( - 'CREATE (a:Person {name: $name}) RETURN a', - { name: personName } - ) - resultPromise.then(result => { - session.close() + try { + const result = await session.run( + 'CREATE (a:Person {name: $name}) RETURN a', + { name: personName } + ) const singleRecord = result.records[0] const node = singleRecord.get(0) console.log(node.properties.name) + } finally { + await session.close() + } - // on application exit: - driver.close() - }) + // on application exit: + driver.close() // end::language-guide-page[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual(personName) - done() - }) + expect(await consoleLoggedMsg).toEqual(personName) }) - it('read write transaction example', done => { + it('read write transaction example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::read-write-transaction[] const session = driver.session() - const writeTxPromise = session.writeTransaction(tx => - tx.run('CREATE (a:Person {name: $name})', { name: personName }) - ) + try { + await session.writeTransaction(tx => + tx.run('CREATE (a:Person {name: $name})', { name: personName }) + ) - writeTxPromise.then(() => { - const readTxPromise = session.readTransaction(tx => + const result = await session.readTransaction(tx => tx.run('MATCH (a:Person {name: $name}) RETURN id(a)', { name: personName }) ) - readTxPromise.then(result => { - session.close() - - const singleRecord = result.records[0] - const createdNodeId = singleRecord.get(0) + const singleRecord = result.records[0] + const createdNodeId = singleRecord.get(0) - console.log('Matched created node with id: ' + createdNodeId) - }) - }) + console.log('Matched created node with id: ' + createdNodeId) + } finally { + await session.close() + } // end::read-write-transaction[] - testResultPromise.then(loggedMsg => { - expect( - loggedMsg.indexOf('Matched created node with id') === 0 - ).toBeTruthy() - done() - }) + expect(await consoleLoggedMsg).toContain('Matched created node with id') }) - it('result consume example', done => { + it('result consume example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const names = { nameA: 'Alice', nameB: 'Bob' } const tmpSession = driver.session() + try { + await tmpSession.run( + 'CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', + names + ) + // tag::result-consume[] + const session = driver.session() + const result = session.run( + 'MATCH (a:Person) RETURN a.name ORDER BY a.name' + ) + const collectedNames = [] - tmpSession - .run('CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', names) - .then(() => { - tmpSession.close(() => { - // tag::result-consume[] - const session = driver.session() - const result = session.run( - 'MATCH (a:Person) RETURN a.name ORDER BY a.name' - ) - const collectedNames = [] - - result.subscribe({ - onNext: record => { - const name = record.get(0) - collectedNames.push(name) - }, - onCompleted: () => { - session.close() - - console.log('Names: ' + collectedNames.join(', ')) - }, - onError: error => { - console.log(error) - } - }) - // end::result-consume[] - }) + result.subscribe({ + onNext: record => { + const name = record.get(0) + collectedNames.push(name) + }, + onCompleted: () => { + session.close() + + console.log('Names: ' + collectedNames.join(', ')) + }, + onError: error => { + console.log(error) + } }) + // end::result-consume[] + } finally { + await tmpSession.close() + } - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toEqual('Names: Alice, Bob') - done() - }) + expect(await consoleLoggedMsg).toEqual('Names: Alice, Bob') }) - it('result retain example', done => { + it('result retain example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const companyName = 'Acme' const personNames = { nameA: 'Alice', nameB: 'Bob' } const tmpSession = driver.session() - tmpSession - .run( + try { + await tmpSession.run( 'CREATE (a:Person {name: $nameA}), (b:Person {name: $nameB})', personNames ) - .then(() => { - tmpSession.close(() => { - // tag::result-retain[] - const session = driver.session() - const readTxPromise = session.readTransaction(tx => - tx.run('MATCH (a:Person) RETURN a.name AS name') + // tag::result-retain[] + const session = driver.session() + try { + const result = await session.readTransaction(tx => + tx.run('MATCH (a:Person) RETURN a.name AS name') + ) + + const nameRecords = result.records + for (let i = 0; i < nameRecords.length; i++) { + const name = nameRecords[i].get('name') + + await session.writeTransaction(tx => + tx.run( + 'MATCH (emp:Person {name: $person_name}) ' + + 'MERGE (com:Company {name: $company_name}) ' + + 'MERGE (emp)-[:WORKS_FOR]->(com)', + { person_name: name, company_name: companyName } + ) ) + } - const addEmployeesPromise = readTxPromise.then(result => { - const nameRecords = result.records - - let writeTxsPromise = Promise.resolve() - for (let i = 0; i < nameRecords.length; i++) { - const name = nameRecords[i].get('name') - - writeTxsPromise = writeTxsPromise.then(() => - session.writeTransaction(tx => - tx.run( - 'MATCH (emp:Person {name: $person_name}) ' + - 'MERGE (com:Company {name: $company_name}) ' + - 'MERGE (emp)-[:WORKS_FOR]->(com)', - { person_name: name, company_name: companyName } - ) - ) - ) - } - - return writeTxsPromise.then(() => nameRecords.length) - }) - - addEmployeesPromise.then(employeesCreated => { - session.close() - console.log('Created ' + employeesCreated + ' employees') - }) - // end::result-retain[] - }) - }) + console.log(`Created ${nameRecords.length} employees`) + } finally { + await session.close() + } + // end::result-retain[] + } finally { + await tmpSession.close() + } - testResultPromise.then(loggedMsg => { - driver.close() - expect(loggedMsg).toEqual('Created 2 employees') - done() - }) + expect(await consoleLoggedMsg).toEqual('Created 2 employees') }) it('service unavailable example', done => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const uri = 'bolt://localhost:7688' // wrong port const password = 'wrongPassword' @@ -522,7 +508,7 @@ describe('#integration examples', () => { }) // end::service-unavailable[] - testResultPromise.then(loggedMsg => { + consoleLoggedMsg.then(loggedMsg => { driver.close() expect(loggedMsg).toBe( 'Unable to create node: ' + neo4j.error.SERVICE_UNAVAILABLE @@ -531,51 +517,48 @@ describe('#integration examples', () => { }) }) - it('session example', done => { + it('session example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::session[] const session = driver.session() - session - .run('CREATE (a:Person {name: $name})', { name: personName }) - .then(() => { - session.close(() => { - console.log('Person created, session closed') - }) - }) + try { + await session.run('CREATE (a:Person {name: $name})', { name: personName }) + console.log('Person created, session closed') + } finally { + await session.close() + } // end::session[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toBe('Person created, session closed') - done() - }) + expect(await consoleLoggedMsg).toBe('Person created, session closed') }) - it('transaction function example', done => { + it('transaction function example', async () => { + const console = consoleOverride + const consoleLoggedMsg = consoleOverridePromise const driver = driverGlobal const personName = 'Alice' // tag::transaction-function[] const session = driver.session() - const writeTxPromise = session.writeTransaction(tx => - tx.run('CREATE (a:Person {name: $name})', { name: personName }) - ) - - writeTxPromise.then(result => { - session.close() + try { + const result = await session.writeTransaction(tx => + tx.run('CREATE (a:Person {name: $name})', { name: personName }) + ) if (result) { console.log('Person created') } - }) + } finally { + await session.close() + } // end::transaction-function[] - testResultPromise.then(loggedMsg => { - expect(loggedMsg).toBe('Person created') - done() - }) + expect(await consoleLoggedMsg).toBe('Person created') }) it('pass bookmarks example', done => { diff --git a/test/internal/bolt-protocol-v1.test.js b/test/internal/bolt-protocol-v1.test.js index 8d1e9c73f..0e88d3bf2 100644 --- a/test/internal/bolt-protocol-v1.test.js +++ b/test/internal/bolt-protocol-v1.test.js @@ -23,6 +23,7 @@ import Bookmark from '../../src/internal/bookmark' import TxConfig from '../../src/internal/tx-config' import { WRITE } from '../../src/driver' import utils from './test-utils' +import { LoginObserver } from '../../src/internal/stream-observers' describe('#unit BoltProtocolV1', () => { beforeEach(() => { @@ -56,11 +57,22 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) + const onError = error => {} + const onComplete = () => {} const clientName = 'js-driver/1.2.3' const authToken = { username: 'neo4j', password: 'secret' } - const observer = {} - protocol.initialize(clientName, authToken, observer) + const observer = protocol.initialize({ + userAgent: clientName, + authToken, + onError, + onComplete + }) + + expect(observer).toBeTruthy() + expect(observer instanceof LoginObserver).toBeTruthy() + expect(observer._afterError).toBe(onError) + expect(observer._afterComplete).toBe(onComplete) recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage( @@ -76,9 +88,7 @@ describe('#unit BoltProtocolV1', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty(), mode: WRITE @@ -98,9 +108,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.reset(observer) + const observer = protocol.reset() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.reset()) @@ -113,9 +121,8 @@ describe('#unit BoltProtocolV1', () => { const protocol = new BoltProtocolV1(recorder, null, false) const bookmark = new Bookmark('neo4j:bookmark:v1:tx42') - const observer = {} - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark: bookmark, txConfig: TxConfig.empty(), mode: WRITE @@ -135,9 +142,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.commitTransaction(observer) + const observer = protocol.commitTransaction() recorder.verifyMessageCount(2) @@ -151,9 +156,7 @@ describe('#unit BoltProtocolV1', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = {} - - protocol.rollbackTransaction(observer) + const observer = protocol.rollbackTransaction() recorder.verifyMessageCount(2) @@ -164,14 +167,14 @@ describe('#unit BoltProtocolV1', () => { }) describe('Bolt V3', () => { + /** + * @param {function(protocol: BoltProtocolV1)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( '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' ) @@ -179,9 +182,7 @@ describe('#unit BoltProtocolV1', () => { describe('beginTransaction', () => { function verifyBeginTransaction (txConfig) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { txConfig }) - ) + verifyError(protocol => protocol.beginTransaction({ txConfig })) } it('should throw error when txConfig.timeout is set', () => { @@ -202,7 +203,7 @@ describe('#unit BoltProtocolV1', () => { describe('run', () => { function verifyRun (txConfig) { verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { txConfig }) + protocol.run('statement', {}, { txConfig }) ) } @@ -221,14 +222,14 @@ describe('#unit BoltProtocolV1', () => { }) describe('Bolt V4', () => { + /** + * @param {function(protocol: BoltProtocolV1)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV1(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( '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' ) @@ -236,9 +237,7 @@ describe('#unit BoltProtocolV1', () => { describe('beginTransaction', () => { function verifyBeginTransaction (database) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { database }) - ) + verifyError(protocol => protocol.beginTransaction({ database })) } it('should throw error when database is set', () => { @@ -248,9 +247,7 @@ describe('#unit BoltProtocolV1', () => { describe('run', () => { function verifyRun (database) { - verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { database }) - ) + verifyError(protocol => protocol.run('statement', {}, { database })) } it('should throw error when database is set', () => { diff --git a/test/internal/bolt-protocol-v3.test.js b/test/internal/bolt-protocol-v3.test.js index d26e13610..2c4471f72 100644 --- a/test/internal/bolt-protocol-v3.test.js +++ b/test/internal/bolt-protocol-v3.test.js @@ -49,9 +49,8 @@ describe('#unit BoltProtocolV3', () => { const clientName = 'js-driver/1.2.3' const authToken = { username: 'neo4j', password: 'secret' } - const observer = {} - protocol.initialize(clientName, authToken, observer) + const observer = protocol.initialize({ userAgent: clientName, authToken }) recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage( @@ -75,9 +74,8 @@ describe('#unit BoltProtocolV3', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark, txConfig, mode: WRITE @@ -109,9 +107,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark, txConfig, mode: WRITE @@ -129,9 +125,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.commitTransaction(observer) + const observer = protocol.commitTransaction() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.commit()) @@ -143,9 +137,7 @@ describe('#unit BoltProtocolV3', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = {} - - protocol.rollbackTransaction(observer) + const observer = protocol.rollbackTransaction() recorder.verifyMessageCount(1) expect(recorder.messages[0]).toBeMessage(RequestMessage.rollback()) @@ -154,14 +146,14 @@ describe('#unit BoltProtocolV3', () => { }) describe('Bolt V4', () => { + /** + * @param {function(protocol: BoltProtocolV3)} fn + */ function verifyError (fn) { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV3(recorder, null, false) - const observer = { - onError: () => {} - } - expect(() => fn(protocol, observer)).toThrowError( + expect(() => fn(protocol)).toThrowError( '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' ) @@ -169,9 +161,7 @@ describe('#unit BoltProtocolV3', () => { describe('beginTransaction', () => { function verifyBeginTransaction (database) { - verifyError((protocol, observer) => - protocol.beginTransaction(observer, { database }) - ) + verifyError(protocol => protocol.beginTransaction({ database })) } it('should throw error when database is set', () => { @@ -181,9 +171,7 @@ describe('#unit BoltProtocolV3', () => { describe('run', () => { function verifyRun (database) { - verifyError((protocol, observer) => - protocol.run('statement', {}, observer, { database }) - ) + verifyError(protocol => protocol.run('statement', {}, { database })) } it('should throw error when database is set', () => { diff --git a/test/internal/bolt-protocol-v4.test.js b/test/internal/bolt-protocol-v4.test.js index 6868e59b9..a5a3bfa0c 100644 --- a/test/internal/bolt-protocol-v4.test.js +++ b/test/internal/bolt-protocol-v4.test.js @@ -44,9 +44,8 @@ describe('#unit BoltProtocolV4', () => { const statement = 'RETURN $x, $y' const parameters = { x: 'x', y: 'y' } - const observer = {} - protocol.run(statement, parameters, observer, { + const observer = protocol.run(statement, parameters, { bookmark, txConfig, database, @@ -81,9 +80,7 @@ describe('#unit BoltProtocolV4', () => { const recorder = new utils.MessageRecordingConnection() const protocol = new BoltProtocolV4(recorder, null, false) - const observer = {} - - protocol.beginTransaction(observer, { + const observer = protocol.beginTransaction({ bookmark, txConfig, database, diff --git a/test/internal/connection-channel.test.js b/test/internal/connection-channel.test.js index 1eca98b16..26cee3c52 100644 --- a/test/internal/connection-channel.test.js +++ b/test/internal/connection-channel.test.js @@ -18,6 +18,7 @@ */ import DummyChannel from './dummy-channel' +import Connection from '../../src/internal/connection' import ChannelConnection from '../../src/internal/connection-channel' import { Packer } from '../../src/internal/packstream-v1' import { Chunker } from '../../src/internal/chunking' @@ -27,13 +28,13 @@ import sharedNeo4j from '../internal/shared-neo4j' import { ServerVersion, VERSION_3_5_0 } from '../../src/internal/server-version' import lolex from 'lolex' import Logger from '../../src/internal/logger' -import StreamObserver from '../../src/internal/stream-observer' import ConnectionErrorHandler from '../../src/internal/connection-error-handler' import testUtils from '../internal/test-utils' import Bookmark from '../../src/internal/bookmark' import TxConfig from '../../src/internal/tx-config' import { WRITE } from '../../src/driver' import ServerAddress from '../../src/internal/server-address' +import { ResultStreamObserver } from '../../src/internal/stream-observers' const ILLEGAL_MESSAGE = { signature: 42, fields: [] } const SUCCESS_MESSAGE = { signature: 0x70, fields: [{}] } @@ -41,6 +42,7 @@ const FAILURE_MESSAGE = { signature: 0x7f, fields: [newError('Hello')] } const RECORD_MESSAGE = { signature: 0x71, fields: [{ value: 'Hello' }] } describe('#integration ChannelConnection', () => { + /** @type {Connection} */ let connection afterEach(done => { @@ -69,9 +71,11 @@ describe('#integration ChannelConnection', () => { connection = createConnection('bolt://localhost') connection._negotiateProtocol().then(() => { - connection.protocol().initialize('mydriver/0.0.0', basicAuthToken(), { - onCompleted: msg => { - expect(msg).not.toBeNull() + connection.protocol().initialize({ + userAgent: 'mydriver/0.0.0', + authToken: basicAuthToken(), + onComplete: metadata => { + expect(metadata).not.toBeNull() done() }, onError: console.log @@ -92,15 +96,20 @@ describe('#integration ChannelConnection', () => { done() } } - const streamObserver = new StreamObserver() - streamObserver.subscribe(pullAllObserver) connection.connect('mydriver/0.0.0', basicAuthToken()).then(() => { - connection.protocol().run('RETURN 1.0', {}, streamObserver, { - bookmark: Bookmark.empty(), - txConfig: TxConfig.empty(), - mode: WRITE - }) + connection + .protocol() + .run( + 'RETURN 1.0', + {}, + { + bookmark: Bookmark.empty(), + txConfig: TxConfig.empty(), + mode: WRITE + } + ) + .subscribe(pullAllObserver) }) }) @@ -218,7 +227,7 @@ describe('#integration ChannelConnection', () => { expect(connection.isOpen()).toBeFalsy() - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.subscribe({ onError: error => { expect(error).toEqual(initialError) @@ -239,7 +248,8 @@ describe('#integration ChannelConnection', () => { it('should not queue INIT observer when broken', done => { testQueueingOfObserversWithBrokenConnection( - connection => connection.protocol().initialize('Hello', {}, {}), + connection => + connection.protocol().initialize({ userAgent: 'Hello', authToken: {} }), done ) }) @@ -252,7 +262,6 @@ describe('#integration ChannelConnection', () => { .run( 'RETURN 1', {}, - {}, { bookmark: Bookmark.empty(), txConfig: TxConfig.empty() } ), done @@ -321,7 +330,7 @@ describe('#integration ChannelConnection', () => { .then(() => done.fail('Should fail')) .catch(error => { expect(error.message).toEqual( - 'Received RECORD as a response for RESET: {"value":"Hello"}' + 'Received RECORD when resetting: received record is: {"value":"Hello"}' ) expect(connection._isBroken).toBeTruthy() expect(connection.isOpen()).toBeFalsy() diff --git a/test/internal/connection-holder.test.js b/test/internal/connection-holder.test.js index 7c04a7b9e..7cbcaec76 100644 --- a/test/internal/connection-holder.test.js +++ b/test/internal/connection-holder.test.js @@ -23,11 +23,11 @@ import ConnectionHolder, { import SingleConnectionProvider from '../../src/internal/connection-provider-single' import { READ, WRITE } from '../../src/driver' import FakeConnection from './fake-connection' -import StreamObserver from '../../src/internal/stream-observer' +import Connection from '../../src/internal/connection' describe('#unit EmptyConnectionHolder', () => { it('should return rejected promise instead of connection', done => { - EMPTY_CONNECTION_HOLDER.getConnection(new StreamObserver()).catch(() => { + EMPTY_CONNECTION_HOLDER.getConnection().catch(() => { done() }) }) @@ -60,7 +60,7 @@ describe('#unit ConnectionHolder', () => { expect(connectionProvider.acquireConnectionInvoked).toBe(1) }) - it('should return acquired during initialization connection', done => { + it('should return connection promise', done => { const connection = new FakeConnection() const connectionProvider = newSingleConnectionProvider(connection) const connectionHolder = new ConnectionHolder({ @@ -70,25 +70,24 @@ describe('#unit ConnectionHolder', () => { connectionHolder.initializeConnection() - connectionHolder.getConnection(new StreamObserver()).then(conn => { + connectionHolder.getConnection().then(conn => { expect(conn).toBe(connection) done() }) }) - it('should make stream observer aware about connection when initialization successful', done => { + it('should return connection promise with version', done => { const connection = new FakeConnection().withServerVersion('Neo4j/9.9.9') const connectionProvider = newSingleConnectionProvider(connection) const connectionHolder = new ConnectionHolder({ mode: READ, connectionProvider }) - const streamObserver = new StreamObserver() connectionHolder.initializeConnection() - connectionHolder.getConnection(streamObserver).then(conn => { - verifyConnection(streamObserver, 'Neo4j/9.9.9') + connectionHolder.getConnection().then(conn => { + verifyConnection(conn, 'Neo4j/9.9.9') done() }) }) @@ -101,11 +100,10 @@ describe('#unit ConnectionHolder', () => { mode: READ, connectionProvider }) - const streamObserver = new StreamObserver() connectionHolder.initializeConnection() - connectionHolder.getConnection(streamObserver).catch(error => { + connectionHolder.getConnection().catch(error => { expect(error.message).toEqual(errorMessage) done() }) @@ -310,11 +308,12 @@ function newSingleConnectionProvider (connection) { return new SingleConnectionProvider(Promise.resolve(connection)) } -function verifyConnection (streamObserver, expectedServerVersion) { - expect(streamObserver._conn).toBeDefined() - expect(streamObserver._conn).not.toBeNull() - - // server version is taken from connection, verify it as well - const metadata = streamObserver.serverMetadata() - expect(metadata.server.version).toEqual(expectedServerVersion) +/** + * @param {Connection} connection + * @param {*} expectedServerVersion + */ +function verifyConnection (connection, expectedServerVersion) { + expect(connection).toBeDefined() + expect(connection.server).toBeDefined() + expect(connection.server.version).toEqual(expectedServerVersion) } diff --git a/test/internal/node/direct.driver.boltkit.test.js b/test/internal/node/direct.driver.boltkit.test.js index 13e08d5ea..a1b4a730a 100644 --- a/test/internal/node/direct.driver.boltkit.test.js +++ b/test/internal/node/direct.driver.boltkit.test.js @@ -105,7 +105,7 @@ describe('#stub-direct direct driver with stub server', () => { tx.commit().then(() => { expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -156,7 +156,7 @@ describe('#stub-direct direct driver with stub server', () => { tx.commit().then(() => { expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -218,7 +218,7 @@ describe('#stub-direct direct driver with stub server', () => { 'neo4j:bookmark:v1:tx424242' ) - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -307,7 +307,7 @@ describe('#stub-direct direct driver with stub server', () => { const records = result.records expect(records.length).toEqual(1) expect(records[0].get(0).toNumber()).toEqual(42) - session.close(() => { + session.close().then(() => { expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) driver.close() server.exit(code => { @@ -362,7 +362,7 @@ describe('#stub-direct direct driver with stub server', () => { ) expect(error.message).toEqual('/ by zero') - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -415,7 +415,7 @@ describe('#stub-direct direct driver with stub server', () => { .then(result => { const names = result.records.map(record => record.get(0)) expect(names).toEqual(['Foo', 'Bar']) - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -475,7 +475,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(connectionKey).toBeTruthy() const connection = openConnections(driver, connectionKey) - session.close(() => { + session.close().then(() => { // generate a fake fatal error connection._handleFatalError( newError('connection reset', SERVICE_UNAVAILABLE) @@ -539,7 +539,7 @@ describe('#stub-direct direct driver with stub server', () => { ) ) .then(() => - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { diff --git a/test/internal/node/routing.driver.boltkit.test.js b/test/internal/node/routing.driver.boltkit.test.js index 52e859a6c..0c22d8d6c 100644 --- a/test/internal/node/routing.driver.boltkit.test.js +++ b/test/internal/node/routing.driver.boltkit.test.js @@ -914,7 +914,7 @@ describe('#stub-routing routing driver with stub server', () => { // When const session1 = driver.session({ defaultAccessMode: WRITE }) session1.run("CREATE (n {name:'Bob'})").then(() => { - session1.close(() => { + session1.close().then(() => { const openConnectionsCount = numberOfOpenConnections(driver) const session2 = driver.session({ defaultAccessMode: WRITE }) session2.run('CREATE ()').then(() => { @@ -1134,7 +1134,7 @@ describe('#stub-routing routing driver with stub server', () => { session .run('MATCH (n) RETURN n.name') .then(() => { - session.close(() => { + session.close().then(() => { driver.close() server.exit(code => { expect(code).toEqual(0) @@ -1495,7 +1495,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(3) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenReader.exit(code2 => { @@ -1545,7 +1545,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(0) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenWriter.exit(code2 => { @@ -1602,7 +1602,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual(SESSION_EXPIRED) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenReader1.exit(code2 => { @@ -1659,7 +1659,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual(SESSION_EXPIRED) expect(invocations).toEqual(2) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { brokenWriter1.exit(code2 => { @@ -1717,7 +1717,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(3) expect(invocations).toEqual(3) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { brokenReader1.exit(code2 => { @@ -1781,7 +1781,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result.records.length).toEqual(0) expect(invocations).toEqual(3) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { brokenWriter1.exit(code2 => { @@ -1827,7 +1827,7 @@ describe('#stub-routing routing driver with stub server', () => { const session = driver.session({ defaultAccessMode: READ }) session.run('MATCH (n) RETURN n.name').then(result => { expect(result.records.length).toEqual(3) - session.close(() => { + session.close().then(() => { // stop existing router and reader router1.exit(code1 => { tmpReader.exit(code2 => { @@ -1852,7 +1852,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[0].get('name')).toEqual('Bob') expect(records[1].get('name')).toEqual('Alice') - session.close(() => { + session.close().then(() => { driver.close() router2.exit(code => { expect(code).toEqual(0) @@ -1903,7 +1903,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[0].get('name')).toEqual('Bob') expect(records[1].get('name')).toEqual('Alice') - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { router2.exit(code2 => { @@ -1937,7 +1937,7 @@ describe('#stub-routing routing driver with stub server', () => { const names = result.records.map(record => record.get('name')) expect(names).toEqual(['Alice', 'Bob']) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code => { expect(code).toEqual(0) @@ -1979,7 +1979,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(result2.records.length).toEqual(3) expect(result2.summary.server.address).toEqual('127.0.0.1:9004') - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { reader1.exit(code2 => { @@ -2017,7 +2017,7 @@ describe('#stub-routing routing driver with stub server', () => { const session = driver.session({ defaultAccessMode: READ }) session.run('MATCH (n) RETURN n.name').then(result => { - session.close(() => { + session.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2065,7 +2065,7 @@ describe('#stub-routing routing driver with stub server', () => { readSession .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) .then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2124,7 +2124,7 @@ describe('#stub-routing routing driver with stub server', () => { readSession .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) .then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2139,7 +2139,7 @@ describe('#stub-routing routing driver with stub server', () => { boltStub.run(() => { const writeSession = driver.session({ defaultAccessMode: WRITE }) writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close(() => { + writeSession.close().then(() => { expect(result.records).toEqual([]) driver.close() @@ -2193,7 +2193,7 @@ describe('#stub-routing routing driver with stub server', () => { const readSession = driver.session({ defaultAccessMode: READ }) readSession.run('MATCH (n) RETURN n.name').then(result => { - readSession.close(() => { + readSession.close().then(() => { expect(result.records.map(record => record.get(0))).toEqual([ 'Bob', 'Alice', @@ -2206,7 +2206,7 @@ describe('#stub-routing routing driver with stub server', () => { const writeSession = driver.session({ defaultAccessMode: WRITE }) writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close(() => { + writeSession.close().then(() => { expect(result.records).toEqual([]) driver.close() @@ -2249,7 +2249,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized') expect(error.message).toEqual('Some server auth error message') - session.close(() => { + session.close().then(() => { driver.close() router.exit(code => { expect(code).toEqual(0) @@ -2434,7 +2434,7 @@ describe('#stub-routing routing driver with stub server', () => { assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - session.close(() => { + session.close().then(() => { driver.close() router1.exit(code1 => { router2.exit(code2 => { @@ -2923,7 +2923,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(hasReaderInRoutingTable(driver, serverAddress)).toBeFalsy() expect(hasWriterInRoutingTable(driver, serverAddress)).toBeFalsy() - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { @@ -3258,7 +3258,7 @@ describe('#stub-routing routing driver with stub server', () => { 'Alice', 'Tina' ]) - session.close(() => { + session.close().then(() => { driver.close() router.exit(code1 => { diff --git a/test/internal/stream-observer.test.js b/test/internal/stream-observer.test.js index 432605fad..ee69bcfdc 100644 --- a/test/internal/stream-observer.test.js +++ b/test/internal/stream-observer.test.js @@ -17,19 +17,17 @@ * limitations under the License. */ -import StreamObserver from '../../src/internal/stream-observer' import FakeConnection from './fake-connection' +import { ResultStreamObserver } from '../../src/internal/stream-observers' const NO_OP = () => {} -describe('#unit StreamObserver', () => { +describe('#unit ResultStreamObserver', () => { it('remembers resolved connection', () => { - const streamObserver = newStreamObserver() const connection = new FakeConnection() + const streamObserver = newStreamObserver(connection) - streamObserver.resolveConnection(connection) - - expect(streamObserver._conn).toBe(connection) + expect(streamObserver._connection).toBe(connection) }) it('remembers subscriber', () => { @@ -38,7 +36,7 @@ describe('#unit StreamObserver', () => { streamObserver.subscribe(subscriber) - expect(streamObserver._observer).toBe(subscriber) + expect(streamObserver._observers).toContain(subscriber) }) it('passes received records to the subscriber', () => { @@ -154,7 +152,7 @@ describe('#unit StreamObserver', () => { it('invokes subscribed observer only once of error', () => { const errors = [] - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.subscribe({ onError: error => errors.push(error) }) @@ -169,7 +167,7 @@ describe('#unit StreamObserver', () => { }) it('should be able to handle a single response', done => { - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.prepareToHandleSingleResponse() streamObserver.subscribe({ @@ -183,7 +181,7 @@ describe('#unit StreamObserver', () => { }) it('should mark as completed', done => { - const streamObserver = new StreamObserver() + const streamObserver = new ResultStreamObserver() streamObserver.markCompleted() streamObserver.subscribe({ @@ -195,8 +193,10 @@ describe('#unit StreamObserver', () => { }) }) -function newStreamObserver () { - return new StreamObserver() +function newStreamObserver (connection) { + return new ResultStreamObserver({ + connection + }) } function newObserver (onNext = NO_OP, onError = NO_OP, onCompleted = NO_OP) { diff --git a/test/session.test.js b/test/session.test.js index 9716311b5..c05577004 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -55,20 +55,20 @@ describe('#integration session', () => { driver.close() }) - it('close should invoke callback ', done => { + it('close should return promise', done => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(done) + session.close().then(() => done()) }) - it('close should invoke callback even when already closed ', done => { + it('close should return promise even when already closed ', done => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(() => { - session.close(() => { - session.close(() => { + session.close().then(() => { + session.close().then(() => { + session.close().then(() => { done() }) }) @@ -79,13 +79,13 @@ describe('#integration session', () => { const connection = new FakeConnection() const session = newSessionWithConnection(connection) - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() - session.close(() => { + session.close().then(() => { expect(connection.isReleasedOnce()).toBeTruthy() done() }) @@ -104,7 +104,7 @@ describe('#integration session', () => { originalClose.call(transactionExecutor) } - session.close(() => { + session.close().then(() => { expect(closeCalledTimes).toEqual(1) done() }) @@ -116,7 +116,7 @@ describe('#integration session', () => { const tx = session.beginTransaction() tx.run('INVALID QUERY').catch(() => { tx.rollback().then(() => { - session.close(() => { + session.close().then(() => { driver.close() done() }) @@ -324,7 +324,7 @@ describe('#integration session', () => { }) }) - it('should fail when using the session when having an open transaction', done => { + it('should fail when using the session with an open transaction', done => { // When session.beginTransaction() @@ -783,7 +783,7 @@ describe('#integration session', () => { it('should interrupt query waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -817,7 +817,7 @@ describe('#integration session', () => { it('should interrupt transaction waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -850,7 +850,7 @@ describe('#integration session', () => { it('should interrupt transaction function waiting on a lock when closed', done => { session.run('CREATE ()').then(() => { - session.close(() => { + session.close().then(() => { const session1 = driver.session() const session2 = driver.session() const tx1 = session1.beginTransaction() @@ -990,7 +990,7 @@ describe('#integration session', () => { expect(numberOfAcquiredConnectionsFromPool()).toEqual(1) }, onCompleted: () => { - session.close(() => { + session.close().then(() => { done() }) }, @@ -1010,8 +1010,8 @@ describe('#integration session', () => { expect(numberOfAcquiredConnectionsFromPool()).toEqual(2) }, onCompleted: () => { - otherSession.close(() => { - session.close(() => { + otherSession.close().then(() => { + session.close().then(() => { done() }) }) @@ -1060,7 +1060,7 @@ describe('#integration session', () => { .run('RETURN $array', { array: iterable }) .then(result => { done.fail( - 'Failre expected but query returned ' + + 'Failure expected but query returned ' + JSON.stringify(result.records[0].get(0)) ) }) @@ -1205,7 +1205,7 @@ describe('#integration session', () => { function withQueryInTmpSession (driver, callback) { const tmpSession = driver.session() return tmpSession.run('RETURN 1').then(() => { - tmpSession.close(callback) + tmpSession.close().then(() => callback()) }) } @@ -1263,7 +1263,7 @@ describe('#integration session', () => { tx.commit() .then(() => { const bookmark = session.lastBookmark() - session.close(() => { + session.close().then(() => { resolve(bookmark) }) }) diff --git a/test/stress.test.js b/test/stress.test.js index fca065cbf..7268dba3b 100644 --- a/test/stress.test.js +++ b/test/stress.test.js @@ -236,7 +236,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode) context.log(commandId, `Query completed successfully`) - session.close(() => { + return session.close().then(() => { const possibleError = verifyQueryResult(result) callback(possibleError) }) @@ -277,7 +277,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode, session.lastBookmark()) context.log(commandId, `Transaction function executed successfully`) - session.close(() => { + return session.close().then(() => { const possibleError = verifyQueryResult(result) callback(possibleError) }) @@ -327,7 +327,7 @@ describe('#integration stress tests', () => { context.queryCompleted(result, accessMode, session.lastBookmark()) context.log(commandId, `Transaction committed successfully`) - session.close(() => { + return session.close().then(() => { callback(commandError) }) }) diff --git a/test/temporal-types.test.js b/test/temporal-types.test.js index 994d08467..0eddcb883 100644 --- a/test/temporal-types.test.js +++ b/test/temporal-types.test.js @@ -86,255 +86,245 @@ describe('#integration temporal-types', () => { } }) - beforeEach(done => { + beforeEach(async () => { session = driver.session() - session - .run('MATCH (n) DETACH DELETE n') - .then(() => { - done() - }) - .catch(error => { - done.fail(error) - }) + + try { + await session.run('MATCH (n) DETACH DELETE n') + } finally { + await session.close() + } }) - afterEach(() => { + afterEach(async () => { if (session) { - session.close() + await session.close() session = null } }) - it('should receive Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = duration(27, 17, 91, 999) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN duration({years: 2, months: 3, days: 17, seconds: 91, nanoseconds: 999})', - expectedValue, - done + expectedValue ) }) - it('should send and receive random Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomDuration(), done) + await testSendAndReceiveRandomTemporalValues(() => randomDuration()) }) - it('should send and receive Duration when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Duration when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.Duration(4, 15, 931, 99953), - done + await testSendReceiveTemporalValue( + new neo4j.types.Duration(4, 15, 931, 99953) ) }) - it('should send and receive array of Duration', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Duration', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomDuration(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomDuration()) }) - it('should receive LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = localTime(22, 59, 10, 999999) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN localtime({hour: 22, minute: 59, second: 10, nanosecond: 999999})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxLocalTime = localTime(23, 59, 59, MAX_NANO_OF_SECOND) - testSendReceiveTemporalValue(maxLocalTime, done) + await testSendReceiveTemporalValue(maxLocalTime) }) - it('should send and receive min LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minLocalTime = localTime(0, 0, 0, 0) - testSendReceiveTemporalValue(minLocalTime, done) + await testSendReceiveTemporalValue(minLocalTime) }) - it('should send and receive LocalTime when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive LocalTime when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.LocalTime(12, 32, 56, 12345), - done + await testSendReceiveTemporalValue( + new neo4j.types.LocalTime(12, 32, 56, 12345) ) }) - it('should send and receive random LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomLocalTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomLocalTime()) }) - it('should send and receive array of LocalTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of LocalTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomLocalTime(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomLocalTime()) }) - it('should receive Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = time(11, 42, 59, 9999, -30600) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN time({hour: 11, minute: 42, second: 59, nanosecond: 9999, timezone:"-08:30"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxTime = time(23, 59, 59, MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET) - testSendReceiveTemporalValue(maxTime, done) + await testSendReceiveTemporalValue(maxTime) }) - it('should send and receive min Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minTime = time(0, 0, 0, 0, MIN_TIME_ZONE_OFFSET) - testSendReceiveTemporalValue(minTime, done) + await testSendReceiveTemporalValue(minTime) }) - it('should send and receive Time when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Time when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.Time(22, 19, 32, 18381, MAX_TIME_ZONE_OFFSET), - done + await testSendReceiveTemporalValue( + new neo4j.types.Time(22, 19, 32, 18381, MAX_TIME_ZONE_OFFSET) ) }) - it('should send and receive random Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomTime()) }) - it('should send and receive array of Time', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Time', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomTime(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomTime()) }) - it('should receive Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = date(1995, 7, 28) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN date({year: 1995, month: 7, day: 28})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const maxDate = date(MAX_YEAR, 12, 31) - testSendReceiveTemporalValue(maxDate, done) + await testSendReceiveTemporalValue(maxDate) }) - it('should send and receive min Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minDate = date(MIN_YEAR, 1, 1) - testSendReceiveTemporalValue(minDate, done) + await testSendReceiveTemporalValue(minDate) }) - it('should send and receive Date when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive Date when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue(new neo4j.types.Date(1923, 8, 14), done) + await testSendReceiveTemporalValue(new neo4j.types.Date(1923, 8, 14)) }) - it('should send and receive random Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomDate(), done) + await testSendAndReceiveRandomTemporalValues(() => randomDate()) }) - it('should send and receive array of Date', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of Date', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues(() => randomDate(), done) + await testSendAndReceiveArrayOfRandomTemporalValues(() => randomDate()) }) - it('should receive LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const expectedValue = localDateTime(1869, 9, 23, 18, 29, 59, 12349) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN localdatetime({year: 1869, month: 9, day: 23, hour: 18, minute: 29, second: 59, nanosecond: 12349})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -347,51 +337,49 @@ describe('#integration temporal-types', () => { 59, MAX_NANO_OF_SECOND ) - testSendReceiveTemporalValue(maxLocalDateTime, done) + await testSendReceiveTemporalValue(maxLocalDateTime) }) - it('should send and receive min LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } const minLocalDateTime = localDateTime(MIN_YEAR, 1, 1, 0, 0, 0, 0) - testSendReceiveTemporalValue(minLocalDateTime, done) + await testSendReceiveTemporalValue(minLocalDateTime) }) - it('should send and receive LocalDateTime when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive LocalDateTime when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( - new neo4j.types.LocalDateTime(2045, 9, 1, 11, 25, 25, 911), - done + await testSendReceiveTemporalValue( + new neo4j.types.LocalDateTime(2045, 9, 1, 11, 25, 25, 911) ) }) - it('should send and receive random LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues(() => randomLocalDateTime(), done) + await testSendAndReceiveRandomTemporalValues(() => randomLocalDateTime()) }) - it('should send and receive random LocalDateTime', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random LocalDateTime', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomLocalDateTime(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomLocalDateTime() ) }) - it('should receive DateTime with time zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive DateTime with time zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -405,15 +393,14 @@ describe('#integration temporal-types', () => { 999, 18000 ) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "+05:00"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -427,11 +414,11 @@ describe('#integration temporal-types', () => { MAX_NANO_OF_SECOND, MAX_TIME_ZONE_OFFSET ) - testSendReceiveTemporalValue(maxDateTime, done) + await testSendReceiveTemporalValue(maxDateTime) }) - it('should send and receive min DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -445,16 +432,16 @@ describe('#integration temporal-types', () => { 0, MAX_TIME_ZONE_OFFSET ) - testSendReceiveTemporalValue(minDateTime, done) + await testSendReceiveTemporalValue(minDateTime) }) - it('should send and receive DateTime with zone offset when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive DateTime with zone offset when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( + await testSendReceiveTemporalValue( new neo4j.types.DateTime( 2022, 2, @@ -465,35 +452,32 @@ describe('#integration temporal-types', () => { 12399, MAX_TIME_ZONE_OFFSET, null - ), - done + ) ) }) - it('should send and receive random DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues( - () => randomDateTimeWithZoneOffset(), - done + await testSendAndReceiveRandomTemporalValues(() => + randomDateTimeWithZoneOffset() ) }) - it('should send and receive array of DateTime with zone offset', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of DateTime with zone offset', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomDateTimeWithZoneOffset(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomDateTimeWithZoneOffset() ) }) - it('should receive DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should receive DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -507,15 +491,14 @@ describe('#integration temporal-types', () => { 999, 'Europe/Stockholm' ) - testReceiveTemporalValue( + await testReceiveTemporalValue( 'RETURN datetime({year: 1992, month: 11, day: 24, hour: 9, minute: 55, second: 42, nanosecond: 999, timezone: "Europe/Stockholm"})', - expectedValue, - done + expectedValue ) }) - it('should send and receive max DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive max DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -529,11 +512,11 @@ describe('#integration temporal-types', () => { MAX_NANO_OF_SECOND, MAX_ZONE_ID ) - testSendReceiveTemporalValue(maxDateTime, done) + await testSendReceiveTemporalValue(maxDateTime) }) - it('should send and receive min DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive min DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -547,16 +530,16 @@ describe('#integration temporal-types', () => { 0, MIN_ZONE_ID ) - testSendReceiveTemporalValue(minDateTime, done) + await testSendReceiveTemporalValue(minDateTime) }) - it('should send and receive DateTime with zone id when disableLosslessIntegers=true', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive DateTime with zone id when disableLosslessIntegers=true', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } session = driverWithNativeNumbers.session() - testSendReceiveTemporalValue( + await testSendReceiveTemporalValue( new neo4j.types.DateTime( 2011, 11, @@ -567,30 +550,27 @@ describe('#integration temporal-types', () => { 192378, null, 'Europe/Stockholm' - ), - done + ) ) }) - it('should send and receive random DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive random DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveRandomTemporalValues( - () => randomDateTimeWithZoneId(), - done + await testSendAndReceiveRandomTemporalValues(() => + randomDateTimeWithZoneId() ) }) - it('should send and receive array of DateTime with zone id', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive array of DateTime with zone id', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testSendAndReceiveArrayOfRandomTemporalValues( - () => randomDateTimeWithZoneId(), - done + await testSendAndReceiveArrayOfRandomTemporalValues(() => + randomDateTimeWithZoneId() ) }) @@ -748,125 +728,122 @@ describe('#integration temporal-types', () => { expect(zonedDateTime.nanosecond).toEqual(neo4j.int(9346458)) }) - it('should format duration to string', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should format duration to string', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } - testDurationToString( - [ - { duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S' }, - - { duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S' }, - { duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S' }, - { duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S' }, - { duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S' }, - - { - duration: duration(0, 0, 0, 5), - expectedString: 'P0M0DT0.000000005S' - }, - { - duration: duration(0, 0, 0, -5), - expectedString: 'P0M0DT-0.000000005S' - }, - { - duration: duration(0, 0, 0, 999999999), - expectedString: 'P0M0DT0.999999999S' - }, - { - duration: duration(0, 0, 0, -999999999), - expectedString: 'P0M0DT-0.999999999S' - }, - - { - duration: duration(0, 0, 1, 5), - expectedString: 'P0M0DT1.000000005S' - }, - { - duration: duration(0, 0, -1, -5), - expectedString: 'P0M0DT-1.000000005S' - }, - { - duration: duration(0, 0, 1, -5), - expectedString: 'P0M0DT0.999999995S' - }, - { - duration: duration(0, 0, -1, 5), - expectedString: 'P0M0DT-0.999999995S' - }, - { - duration: duration(0, 0, 1, 999999999), - expectedString: 'P0M0DT1.999999999S' - }, - { - duration: duration(0, 0, -1, -999999999), - expectedString: 'P0M0DT-1.999999999S' - }, - { - duration: duration(0, 0, 1, -999999999), - expectedString: 'P0M0DT0.000000001S' - }, - { - duration: duration(0, 0, -1, 999999999), - expectedString: 'P0M0DT-0.000000001S' - }, - - { - duration: duration(0, 0, 28, 9), - expectedString: 'P0M0DT28.000000009S' - }, - { - duration: duration(0, 0, -28, 9), - expectedString: 'P0M0DT-27.999999991S' - }, - { - duration: duration(0, 0, 28, -9), - expectedString: 'P0M0DT27.999999991S' - }, - { - duration: duration(0, 0, -28, -9), - expectedString: 'P0M0DT-28.000000009S' - }, - - { - duration: duration(0, 0, -78036, -143000000), - expectedString: 'P0M0DT-78036.143000000S' - }, - - { duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S' }, - { - duration: duration(0, 0, 0, -1000000000), - expectedString: 'P0M0DT-1S' - }, - { - duration: duration(0, 0, 0, 1000000007), - expectedString: 'P0M0DT1.000000007S' - }, - { - duration: duration(0, 0, 0, -1000000007), - expectedString: 'P0M0DT-1.000000007S' - }, - - { - duration: duration(0, 0, 40, 2123456789), - expectedString: 'P0M0DT42.123456789S' - }, - { - duration: duration(0, 0, -40, 2123456789), - expectedString: 'P0M0DT-37.876543211S' - }, - { - duration: duration(0, 0, 40, -2123456789), - expectedString: 'P0M0DT37.876543211S' - }, - { - duration: duration(0, 0, -40, -2123456789), - expectedString: 'P0M0DT-42.123456789S' - } - ], - done - ) + await testDurationToString([ + { duration: duration(0, 0, 0, 0), expectedString: 'P0M0DT0S' }, + + { duration: duration(0, 0, 42, 0), expectedString: 'P0M0DT42S' }, + { duration: duration(0, 0, -42, 0), expectedString: 'P0M0DT-42S' }, + { duration: duration(0, 0, 1, 0), expectedString: 'P0M0DT1S' }, + { duration: duration(0, 0, -1, 0), expectedString: 'P0M0DT-1S' }, + + { + duration: duration(0, 0, 0, 5), + expectedString: 'P0M0DT0.000000005S' + }, + { + duration: duration(0, 0, 0, -5), + expectedString: 'P0M0DT-0.000000005S' + }, + { + duration: duration(0, 0, 0, 999999999), + expectedString: 'P0M0DT0.999999999S' + }, + { + duration: duration(0, 0, 0, -999999999), + expectedString: 'P0M0DT-0.999999999S' + }, + + { + duration: duration(0, 0, 1, 5), + expectedString: 'P0M0DT1.000000005S' + }, + { + duration: duration(0, 0, -1, -5), + expectedString: 'P0M0DT-1.000000005S' + }, + { + duration: duration(0, 0, 1, -5), + expectedString: 'P0M0DT0.999999995S' + }, + { + duration: duration(0, 0, -1, 5), + expectedString: 'P0M0DT-0.999999995S' + }, + { + duration: duration(0, 0, 1, 999999999), + expectedString: 'P0M0DT1.999999999S' + }, + { + duration: duration(0, 0, -1, -999999999), + expectedString: 'P0M0DT-1.999999999S' + }, + { + duration: duration(0, 0, 1, -999999999), + expectedString: 'P0M0DT0.000000001S' + }, + { + duration: duration(0, 0, -1, 999999999), + expectedString: 'P0M0DT-0.000000001S' + }, + + { + duration: duration(0, 0, 28, 9), + expectedString: 'P0M0DT28.000000009S' + }, + { + duration: duration(0, 0, -28, 9), + expectedString: 'P0M0DT-27.999999991S' + }, + { + duration: duration(0, 0, 28, -9), + expectedString: 'P0M0DT27.999999991S' + }, + { + duration: duration(0, 0, -28, -9), + expectedString: 'P0M0DT-28.000000009S' + }, + + { + duration: duration(0, 0, -78036, -143000000), + expectedString: 'P0M0DT-78036.143000000S' + }, + + { duration: duration(0, 0, 0, 1000000000), expectedString: 'P0M0DT1S' }, + { + duration: duration(0, 0, 0, -1000000000), + expectedString: 'P0M0DT-1S' + }, + { + duration: duration(0, 0, 0, 1000000007), + expectedString: 'P0M0DT1.000000007S' + }, + { + duration: duration(0, 0, 0, -1000000007), + expectedString: 'P0M0DT-1.000000007S' + }, + + { + duration: duration(0, 0, 40, 2123456789), + expectedString: 'P0M0DT42.123456789S' + }, + { + duration: duration(0, 0, -40, 2123456789), + expectedString: 'P0M0DT-37.876543211S' + }, + { + duration: duration(0, 0, 40, -2123456789), + expectedString: 'P0M0DT37.876543211S' + }, + { + duration: duration(0, 0, -40, -2123456789), + expectedString: 'P0M0DT-42.123456789S' + } + ]) }) it('should normalize created duration', () => { @@ -1284,8 +1261,8 @@ describe('#integration temporal-types', () => { ) }) - it('should send and receive neo4j Date created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j Date created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1294,11 +1271,11 @@ describe('#integration temporal-types', () => { const standardDate = new Date(2000, 0, 1) const neo4jDate = neo4j.types.Date.fromStandardDate(standardDate) - testSendReceiveTemporalValue(neo4jDate, done) + await testSendReceiveTemporalValue(neo4jDate) }) - it('should send and receive neo4j LocalDateTime created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j LocalDateTime created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1309,11 +1286,11 @@ describe('#integration temporal-types', () => { const neo4jLocalDateTime = neo4j.types.LocalDateTime.fromStandardDate( standardDate ) - testSendReceiveTemporalValue(neo4jLocalDateTime, done) + await testSendReceiveTemporalValue(neo4jLocalDateTime) }) - it('should send and receive neo4j DateTime created from standard Date with zero month', done => { - if (neo4jDoesNotSupportTemporalTypes(done)) { + it('should send and receive neo4j DateTime created from standard Date with zero month', async () => { + if (neo4jDoesNotSupportTemporalTypes()) { return } @@ -1322,7 +1299,7 @@ describe('#integration temporal-types', () => { const standardDate = new Date(1756, 0, 29, 23, 15, 59, 12) const neo4jDateTime = neo4j.types.DateTime.fromStandardDate(standardDate) - testSendReceiveTemporalValue(neo4jDateTime, done) + await testSendReceiveTemporalValue(neo4jDateTime) }) it('should fail to create LocalTime with out of range values', () => { @@ -1440,91 +1417,84 @@ describe('#integration temporal-types', () => { verifyTimeZoneOffset(neo4jDateTime5, -1 * 150 * 60, '-02:30') }) - function testSendAndReceiveRandomTemporalValues (valueGenerator, done) { + function testSendAndReceiveRandomTemporalValues (valueGenerator) { const asyncFunction = (index, callback) => { - const next = () => callback() - next.fail = error => callback(error) - testSendReceiveTemporalValue(valueGenerator(), next) - } - - const doneFunction = error => { - if (error) { - done.fail(error) - } else { - done() - } + testSendReceiveTemporalValue(valueGenerator()) + .then(() => callback()) + .catch(error => callback(error)) } - timesSeries(RANDOM_VALUES_TO_TEST, asyncFunction, doneFunction) + return new Promise((resolve, reject) => { + timesSeries(RANDOM_VALUES_TO_TEST, asyncFunction, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result) + } + }) + }) } - function testSendAndReceiveArrayOfRandomTemporalValues (valueGenerator, done) { + async function testSendAndReceiveArrayOfRandomTemporalValues (valueGenerator) { const arrayLength = _.random( MIN_TEMPORAL_ARRAY_LENGTH, MAX_TEMPORAL_ARRAY_LENGTH ) const values = _.range(arrayLength).map(() => valueGenerator()) - testSendReceiveTemporalValue(values, done) + + await testSendReceiveTemporalValue(values) } - function testReceiveTemporalValue (query, expectedValue, done) { - session - .run(query) - .then(result => { - const records = result.records - expect(records.length).toEqual(1) + async function testReceiveTemporalValue (query, expectedValue) { + try { + const result = await session.run(query) - const value = records[0].get(0) - expect(value).toEqual(expectedValue) + const records = result.records + expect(records.length).toEqual(1) - session.close() - done() - }) - .catch(error => { - done.fail(error) - }) + const value = records[0].get(0) + expect(value).toEqual(expectedValue) + } finally { + await session.close() + } } - function testSendReceiveTemporalValue (value, done) { - session - .run('CREATE (n:Node {value: $value}) RETURN n.value', { value: value }) - .then(result => { - const records = result.records - expect(records.length).toEqual(1) + async function testSendReceiveTemporalValue (value) { + const result = await session.run( + 'CREATE (n:Node {value: $value}) RETURN n.value', + { value: value } + ) - const receivedValue = records[0].get(0) - expect(receivedValue).toEqual(value) + const records = result.records + expect(records.length).toEqual(1) - session.close() - done() - }) - .catch(error => { - done.fail(error) - }) + const receivedValue = records[0].get(0) + expect(receivedValue).toEqual(value) + + await session.close() } - function testDurationToString (values, done) { + async function testDurationToString (values) { const durations = values.map(value => value.duration) const expectedDurationStrings = values.map(value => value.expectedString) - session - .run('UNWIND $durations AS d RETURN d', { durations: durations }) - .then(result => { - const receivedDurationStrings = result.records - .map(record => record.get(0)) - .map(duration => duration.toString()) - - expect(expectedDurationStrings).toEqual(receivedDurationStrings) - done() - }) - .catch(error => { - done.fail(error) + try { + const result = await session.run('UNWIND $durations AS d RETURN d', { + durations: durations }) + + const receivedDurationStrings = result.records + .map(record => record.get(0)) + .map(duration => duration.toString()) + + expect(expectedDurationStrings).toEqual(receivedDurationStrings) + } finally { + await session.close() + } } - function neo4jDoesNotSupportTemporalTypes (done) { + function neo4jDoesNotSupportTemporalTypes () { if (serverVersion.compareTo(VERSION_3_4_0) < 0) { - done() return true } return false diff --git a/test/types/session.test.ts b/test/types/session.test.ts index 79d9dea98..162246e91 100644 --- a/test/types/session.test.ts +++ b/test/types/session.test.ts @@ -73,8 +73,8 @@ const promise4: Promise = session.writeTransaction( } ) -const close1: void = session.close() -const close2: void = session.close(() => { +const close1: Promise = session.close() +const close2: Promise = session.close().then(() => { console.log('Session closed') }) diff --git a/types/session.d.ts b/types/session.d.ts index 457329869..14b3d442e 100644 --- a/types/session.d.ts +++ b/types/session.d.ts @@ -50,7 +50,7 @@ declare interface Session extends StatementRunner { config?: TransactionConfig ): Promise - close(callback?: () => void): void + close(): Promise } export { TransactionConfig } From cd47b7ca6187b9a23f36ae94666a87d22391ea46 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Thu, 8 Aug 2019 12:47:21 +0100 Subject: [PATCH 03/14] Initial reactive session and transaction --- src/driver.js | 47 +- src/internal/bolt-protocol-v4.js | 24 +- src/internal/request-message.js | 6 +- src/internal/stream-observers.js | 90 ++-- src/result-rx.js | 142 ++++++ src/result.js | 4 + src/session-rx.js | 88 ++++ src/session.js | 34 +- src/transaction-rx.js | 69 +++ src/transaction.js | 34 +- test/rx/navigation.test.js | 642 ++++++++++++++++++++++++++ test/rx/summary.test.js | 683 ++++++++++++++++++++++++++++ test/rx/transaction.test.js | 752 +++++++++++++++++++++++++++++++ 13 files changed, 2551 insertions(+), 64 deletions(-) create mode 100644 src/result-rx.js create mode 100644 src/session-rx.js create mode 100644 src/transaction-rx.js create mode 100644 test/rx/navigation.test.js create mode 100644 test/rx/summary.test.js create mode 100644 test/rx/transaction.test.js diff --git a/src/driver.js b/src/driver.js index 9b555b891..9bfea77ed 100644 --- a/src/driver.js +++ b/src/driver.js @@ -17,20 +17,19 @@ * limitations under the License. */ -import Session from './session' -import Pool from './internal/pool' -import ChannelConnection from './internal/connection-channel' -import { newError, SERVICE_UNAVAILABLE } from './error' -import DirectConnectionProvider from './internal/connection-provider-direct' +import { newError } from './error' +import ConnectionProvider from './internal/connection-provider' import Bookmark from './internal/bookmark' +import DirectConnectionProvider from './internal/connection-provider-direct' import ConnectivityVerifier from './internal/connectivity-verifier' -import PoolConfig, { +import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import Logger from './internal/logger' +import { DEFAULT_ACQUISITION_TIMEOUT, DEFAULT_MAX_SIZE } from './internal/pool-config' -import Logger from './internal/logger' -import ConnectionErrorHandler from './internal/connection-error-handler' -import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import Session from './session' +import RxSession from './session-rx' const DEFAULT_MAX_CONNECTION_LIFETIME = 60 * 60 * 1000 // 1 hour @@ -121,10 +120,11 @@ class Driver { * it is closed, the underlying connection will be released to the connection * pool and made available for others to use. * - * @param {string} [defaultAccessMode=WRITE] the access mode of this session, allowed values are {@link READ} and {@link WRITE}. - * @param {string|string[]} [bookmarks=null] the initial reference or references to some previous + * @param {Object} args - + * @param {string} args.defaultAccessMode='WRITE' - the access mode of this session, allowed values are {@link READ} and {@link WRITE}. + * @param {string|string[]} args.bookmarks - 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. - * @param {string} [database=''] the database this session will connect to. + * @param {string} args.database='' - the database this session will connect to. * @return {Session} new session. */ session ({ @@ -132,6 +132,26 @@ class Driver { bookmarks: bookmarkOrBookmarks, database = '' } = {}) { + return this._newSession({ + defaultAccessMode, + bookmarkOrBookmarks, + database, + reactive: false + }) + } + + rxSession ({ defaultAccessMode = WRITE, bookmarks, database = '' } = {}) { + return new RxSession( + this._newSession({ + defaultAccessMode, + bookmarks, + database, + reactive: true + }) + ) + } + + _newSession ({ defaultAccessMode, bookmarkOrBookmarks, database, reactive }) { const sessionMode = Driver._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() const bookmark = bookmarkOrBookmarks @@ -142,7 +162,8 @@ class Driver { database, connectionProvider, bookmark, - config: this._config + config: this._config, + reactive }) } diff --git a/src/internal/bolt-protocol-v4.js b/src/internal/bolt-protocol-v4.js index 766321b23..4a48476a5 100644 --- a/src/internal/bolt-protocol-v4.js +++ b/src/internal/bolt-protocol-v4.js @@ -63,11 +63,15 @@ export default class BoltProtocol extends BoltProtocolV3 { afterError, beforeComplete, afterComplete, - flush = true + flush = true, + reactive = false } = {} ) { const observer = new ResultStreamObserver({ connection: this._connection, + reactive: reactive, + moreFunction: reactive ? this._requestMore : this._noOp, + discardFunction: reactive ? this._requestDiscard : this._noOp, beforeKeys, afterKeys, beforeError, @@ -76,6 +80,7 @@ export default class BoltProtocol extends BoltProtocolV3 { afterComplete }) + const flushRun = reactive this._connection.write( RequestMessage.runWithMetadata(statement, parameters, { bookmark, @@ -84,10 +89,23 @@ export default class BoltProtocol extends BoltProtocolV3 { mode }), observer, - false + flushRun && flush ) - this._connection.write(RequestMessage.pull(), observer, flush) + + if (!reactive) { + this._connection.write(RequestMessage.pull(), observer, flush) + } return observer } + + _requestMore (connection, stmtId, n, observer) { + connection.write(RequestMessage.pull({ stmtId, n }), observer, true) + } + + _requestDiscard (connection, stmtId, observer) { + connection.write(RequestMessage.discard({ stmtId }), observer, true) + } + + _noOp () {} } diff --git a/src/internal/request-message.js b/src/internal/request-message.js index 4faa9783f..5e687b687 100644 --- a/src/internal/request-message.js +++ b/src/internal/request-message.js @@ -185,7 +185,7 @@ export default class RequestMessage { * @return {RequestMessage} the PULL message. */ static pull ({ stmtId = NO_STATEMENT_ID, n = ALL } = {}) { - const metadata = buildStreamMetadata(stmtId, n) + const metadata = buildStreamMetadata(stmtId || NO_STATEMENT_ID, n || ALL) return new RequestMessage( PULL, [metadata], @@ -200,7 +200,7 @@ export default class RequestMessage { * @return {RequestMessage} the PULL message. */ static discard ({ stmtId = NO_STATEMENT_ID, n = ALL } = {}) { - const metadata = buildStreamMetadata(stmtId, n) + const metadata = buildStreamMetadata(stmtId || NO_STATEMENT_ID, n || ALL) return new RequestMessage( DISCARD, [metadata], @@ -246,7 +246,7 @@ function buildTxMetadata (bookmark, txConfig, database, mode) { function buildStreamMetadata (stmtId, n) { const metadata = { n: int(n) } if (stmtId !== NO_STATEMENT_ID) { - metadata['stmt_id'] = int(stmtId) + metadata['qid'] = int(stmtId) } return metadata } diff --git a/src/internal/stream-observers.js b/src/internal/stream-observers.js index 06267ff03..b472585ed 100644 --- a/src/internal/stream-observers.js +++ b/src/internal/stream-observers.js @@ -20,8 +20,9 @@ import Record from '../record' import Connection from './connection' import { newError, PROTOCOL_ERROR } from '../error' import { isString } from './util' +import Integer from '../integer' -const DefaultBatchSize = 100 +const DefaultBatchSize = 50 class StreamObserver { onNext (rawRecord) {} @@ -45,10 +46,11 @@ class ResultStreamObserver extends StreamObserver { /** * * @param {Object} param - * @param {Connection} connection - * @param {} param.moreFunction - - * @param {} param.discardFunction - - * @param {} param.batchSize - + * @param {Connection} param.connection + * @param {boolean} param.reactive + * @param {function(connection: Connection, stmtId: number|Integer, n: number|Integer, observer: StreamObserver)} param.moreFunction - + * @param {function(connection: Connection, stmtId: number|Integer, observer: StreamObserver)} param.discardFunction - + * @param {number|Integer} param.batchSize - * @param {function(err: Error): Promise|void} param.beforeError - * @param {function(err: Error): Promise|void} param.afterError - * @param {function(keys: string[]): Promise|void} param.beforeKeys - @@ -58,6 +60,7 @@ class ResultStreamObserver extends StreamObserver { */ constructor ({ connection, + reactive = false, moreFunction, discardFunction, batchSize = DefaultBatchSize, @@ -71,6 +74,8 @@ class ResultStreamObserver extends StreamObserver { super() this._connection = connection + this._reactive = reactive + this._streaming = false this._fieldKeys = null this._fieldLookup = null @@ -105,7 +110,11 @@ class ResultStreamObserver extends StreamObserver { onNext (rawRecord) { let record = new Record(this._fieldKeys, rawRecord, this._fieldLookup) if (this._observers.some(o => o.onNext)) { - this._observers.forEach(o => (o.onNext ? o.onNext(record) : {})) + this._observers.forEach(o => { + if (o.onNext) { + o.onNext(record) + } + }) } else { this._queuedRecords.push(record) } @@ -149,14 +158,20 @@ class ResultStreamObserver extends StreamObserver { this._head = this._fieldKeys if (this._observers.some(o => o.onKeys)) { - this._observers.forEach(o => - o.onKeys ? o.onKeys(this._fieldKeys) : {} - ) + this._observers.forEach(o => { + if (o.onKeys) { + o.onKeys(this._fieldKeys) + } + }) } if (this._afterKeys) { this._afterKeys(this._fieldKeys) } + + if (this._reactive) { + this._handleStreaming() + } } if (beforeHandlerResult) { @@ -165,22 +180,13 @@ class ResultStreamObserver extends StreamObserver { continuation() } } else { + this._streaming = false + if (meta.has_more) { // 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 - if (this._discard) { - this._discardFunction({ - connection: this._connection, - statementId: this._statementId - }) - } else { - this._moreFunction({ - connection: this._connection, - statementId: this._statementId, - n: this._batchSize - }) - } + this._handleStreaming() delete meta.has_more } else { @@ -200,9 +206,11 @@ class ResultStreamObserver extends StreamObserver { this._tail = completionMetadata if (this._observers.some(o => o.onCompleted)) { - this._observers.forEach(o => - o.onCompleted ? o.onCompleted(completionMetadata) : {} - ) + this._observers.forEach(o => { + if (o.onCompleted) { + o.onCompleted(completionMetadata) + } + }) } if (this._afterComplete) { @@ -219,6 +227,28 @@ class ResultStreamObserver extends StreamObserver { } } + _handleStreaming () { + if ( + this._reactive && + this._head && + this._observers.some(o => o.onNext || o.onCompleted) && + !this._streaming + ) { + this._streaming = true + + if (this._discard) { + this._discardFunction(this._connection, this._statementId, this) + } else { + this._moreFunction( + this._connection, + this._statementId, + this._batchSize, + this + ) + } + } + } + _storeMetadataForCompletion (meta) { const keys = Object.keys(meta) let index = keys.length @@ -282,9 +312,11 @@ class ResultStreamObserver extends StreamObserver { const continuation = () => { if (this._observers.some(o => o.onError)) { - this._observers.forEach(o => - o.onError ? o.onError(error) : console.log(error) - ) + this._observers.forEach(o => { + if (o.onError) { + o.onError(error) + } + }) } if (this._afterError) { @@ -324,6 +356,10 @@ class ResultStreamObserver extends StreamObserver { observer.onCompleted(this._tail) } this._observers.push(observer) + + if (this._reactive) { + this._handleStreaming() + } } hasFailed () { diff --git a/src/result-rx.js b/src/result-rx.js new file mode 100644 index 000000000..03e0d2970 --- /dev/null +++ b/src/result-rx.js @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2002-2019 "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' +import ResultSummary from './result-summary' +import { Observable, Subject, ReplaySubject, from } from 'rxjs' +import { flatMap, publishReplay, refCount, shareReplay } from 'rxjs/operators' + +const States = { + READY: 0, + STREAMING: 1, + COMPLETED: 2 +} + +export default class RxResult { + /** + * + * @param {Observable} result + */ + constructor (result) { + const replayedResult = result.pipe( + publishReplay(1), + refCount() + ) + + this._result = replayedResult + this._keys = replayedResult.pipe( + flatMap(r => from(r.keys())), + publishReplay(1), + refCount() + ) + this._records = new Subject() + this._summary = new ReplaySubject() + this._state = States.READY + } + + keys () { + return this._keys + } + + records () { + return this._result.pipe( + flatMap( + result => + new Observable(recordsObserver => + this._startStreaming({ result, recordsObserver }) + ) + ) + ) + } + + /** + * return {Observable} + */ + summary () { + return this._result.pipe( + flatMap( + result => + new Observable(summaryObserver => + this._startStreaming({ result, summaryObserver }) + ) + ) + ) + } + + _startStreaming ({ + result, + recordsObserver = null, + summaryObserver = null + } = {}) { + const subscriptions = [] + + if (recordsObserver) { + subscriptions.push(this._records.subscribe(recordsObserver)) + } + + if (summaryObserver) { + subscriptions.push(this._summary.subscribe(summaryObserver)) + } + + if (this._state < States.STREAMING) { + this._state = States.STREAMING + + subscriptions.push({ + unsubscribe: () => { + if (result.discard) { + result.discard() + } + } + }) + + if (this._records.observers.length === 0) { + result._discard() + } + + result.subscribe({ + onNext: record => { + this._records.next(record) + }, + onCompleted: summary => { + this._records.complete() + + this._summary.next(summary) + this._summary.complete() + + this._state = States.COMPLETED + }, + onError: err => { + this._records.error(err) + this._summary.error(err) + + this._state = States.COMPLETED + } + }) + } else if (this._state === States.STREAMING && recordsObserver) { + recordsObserver.error( + newError( + 'Streaming has already started with a previous records or summary subscription.' + ) + ) + } + + return () => { + subscriptions.forEach(s => s.unsubscribe()) + } + } +} diff --git a/src/result.js b/src/result.js index 766cb9268..0f3a55abc 100644 --- a/src/result.js +++ b/src/result.js @@ -162,6 +162,10 @@ class Result { this._streamObserverPromise.then(o => o.subscribe(observer)) } + + _discard () { + this._streamObserverPromise.then(o => o.discard()) + } } function captureStacktrace () { diff --git a/src/session-rx.js b/src/session-rx.js new file mode 100644 index 000000000..6dfc4b60a --- /dev/null +++ b/src/session-rx.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2002-2019 "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 { defer, Observable } from 'rxjs' +import { map } from 'rxjs/operators' +import { newError } from './error' +import RxResult from './result-rx' +import Session from './session' +import RxTransaction from './transaction-rx' + +export default class RxSession { + /** + * @param {Session} session + */ + constructor (session) { + this._session = session + } + + run (statement, parameters, transactionConfig) { + return new RxResult( + new Observable(observer => { + try { + observer.next( + this._session.run(statement, parameters, transactionConfig) + ) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + ) + } + + beginTransaction (transactionConfig) { + return new Observable(observer => { + try { + observer.next( + new RxTransaction(this._session.beginTransaction(transactionConfig)) + ) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + } + + readTransaction (transactionWork, transactionConfig) { + throw newError('not implemented') + } + + writeTransaction (transactionWork, transactionConfig) { + throw newError('not implemented') + } + + close () { + return new Observable(observer => { + this._session + .close() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } + + lastBookmark () { + return this._session.lastBookmark() + } +} diff --git a/src/session.js b/src/session.js index 48e31fc09..a5b4b0ecf 100644 --- a/src/session.js +++ b/src/session.js @@ -60,15 +60,25 @@ import TxConfig from './internal/tx-config' class Session { /** * @constructor - * @param {string} mode the default access mode for this session. - * @param {ConnectionProvider} connectionProvider - the connection provider to acquire connections from. - * @param {Bookmark} bookmark - the initial bookmark for this session. - * @param {string} database the database name - * @param {Object} [config={}] - this driver configuration. + * @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 {Bookmark} args.bookmark - the initial bookmark 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 */ - constructor ({ mode, connectionProvider, bookmark, database, config }) { + constructor ({ + mode, + connectionProvider, + bookmark, + database, + config, + reactive + }) { this._mode = mode this._database = database + this._reactive = reactive this._readConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_READ, database, @@ -110,7 +120,8 @@ class Session { txConfig: autoCommitTxConfig, mode: this._mode, database: this._database, - afterComplete: this._onComplete + afterComplete: this._onComplete, + reactive: this._reactive }) ) } @@ -175,11 +186,12 @@ class Session { connectionHolder.initializeConnection() this._hasTx = true - const tx = new Transaction( + const tx = new Transaction({ connectionHolder, - this._transactionClosed.bind(this), - this._updateBookmark.bind(this) - ) + onClose: this._transactionClosed.bind(this), + onBookmark: this._updateBookmark.bind(this), + reactive: this._reactive + }) tx._begin(this._lastBookmark, txConfig) return tx } diff --git a/src/transaction-rx.js b/src/transaction-rx.js new file mode 100644 index 000000000..66f315743 --- /dev/null +++ b/src/transaction-rx.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2002-2019 "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' +import { Observable, from } from 'rxjs' +import Transaction from './transaction' +import RxResult from './result-rx' + +export default class RxTransaction { + /** + * + * @param {Transaction} txc + */ + constructor (txc) { + this._txc = txc + } + + run (statement, parameters) { + return new RxResult( + new Observable(observer => { + try { + observer.next(this._txc.run(statement, parameters)) + observer.complete() + } catch (err) { + observer.error(err) + } + + return () => {} + }) + ) + } + + commit () { + return new Observable(observer => { + this._txc + .commit() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } + + rollback () { + return new Observable(observer => { + this._txc + .rollback() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } +} diff --git a/src/transaction.js b/src/transaction.js index edc9a103c..d459fb055 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -41,9 +41,11 @@ class Transaction { * @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(bookmark: Bookmark)} onBookmark callback invoked when new bookmark is produced. + * @param {boolean} reactive whether this transaction generates reactive streams */ - constructor (connectionHolder, onClose, onBookmark) { + constructor ({ connectionHolder, onClose, onBookmark, reactive }) { this._connectionHolder = connectionHolder + this._reactive = reactive this._state = _states.ACTIVE this._onClose = onClose this._onBookmark = onBookmark @@ -84,7 +86,8 @@ class Transaction { return this._state.run(query, params, { connectionHolder: this._connectionHolder, onError: this._onError, - onComplete: this._onComplete + onComplete: this._onComplete, + reactive: this._reactive }) } @@ -165,7 +168,11 @@ let _states = { state: _states.ROLLED_BACK } }, - run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { // RUN in explicit transaction can't contain bookmarks and transaction configuration const observerPromise = connectionHolder .getConnection() @@ -176,7 +183,8 @@ let _states = { mode: connectionHolder.mode(), database: connectionHolder.database(), beforeError: onError, - afterComplete: onComplete + afterComplete: onComplete, + reactive: reactive }) ) .catch(error => new FailedObserver({ error, onError })) @@ -210,7 +218,11 @@ let _states = { state: _states.FAILED } }, - run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { return newCompletedResult( new FailedObserver({ error: @@ -256,7 +268,11 @@ let _states = { state: _states.SUCCEEDED } }, - run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { return newCompletedResult( new FailedObserver({ error: @@ -298,7 +314,11 @@ let _states = { state: _states.ROLLED_BACK } }, - run: (statement, parameters, { connectionHolder, onError, onComplete }) => { + run: ( + statement, + parameters, + { connectionHolder, onError, onComplete, reactive } + ) => { return newCompletedResult( new FailedObserver({ error: diff --git a/test/rx/navigation.test.js b/test/rx/navigation.test.js new file mode 100644 index 000000000..33f15b8e8 --- /dev/null +++ b/test/rx/navigation.test.js @@ -0,0 +1,642 @@ +/** + * Copyright (c) 2002-2019 "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 neo4j from '../../src' +import sharedNeo4j from '../internal/shared-neo4j' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import { Notification, Observable } from 'rxjs' +import { materialize, toArray, map } from 'rxjs/operators' +import RxTransaction from '../../src/transaction-rx' + +describe('#integration-rx navigation', () => { + describe('session', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + let originalTimeout + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return keys', () => shouldReturnKeys(serverVersion, session)) + + it('should return summary', () => + shouldReturnSummary(serverVersion, session)) + + it('should return keys and records', () => + shouldReturnKeysAndRecords(serverVersion, session)) + + it('should return records and summary', () => + shouldReturnRecordsAndSummary(serverVersion, session)) + + it('should return keys, records and summary', () => + shouldReturnKeysRecordsAndSummary(serverVersion, session)) + + it('should return keys and summary but no records', () => + shouldReturnKeysAndSummaryButRecords(serverVersion, session)) + + it('should return keys even after records are complete', () => + shouldReturnKeysEvenAfterRecordsAreComplete(serverVersion, session)) + + it('should return keys even after summary is complete', () => + shouldReturnKeysEvenAfterSummaryIsComplete(serverVersion, session)) + + it('should return keys multiple times', () => + shouldReturnKeysMultipleTimes(serverVersion, session)) + + it('should return summary multiple times', () => + shouldReturnSummaryMultipleTimes(serverVersion, session)) + + it('should return records only once', () => + shouldReturnRecordsOnlyOnce(serverVersion, session)) + + it('should return empty keys for statement without return', () => + shouldReturnEmptyKeysForStatementWithNoReturn(serverVersion, session)) + + it('should return no records for statement without return', () => + shouldReturnNoRecordsForStatementWithNoReturn(serverVersion, session)) + + it('should return summary for statement without return', () => + shouldReturnSummaryForStatementWithNoReturn(serverVersion, session)) + + it('should fail on keys when run fails', () => + shouldFailOnKeysWhenRunFails(serverVersion, session)) + + it('should fail on subsequent keys when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, session)) + + it('should fail on records when run fails', () => + shouldFailOnRecordsWhenRunFails(serverVersion, session)) + + it('should fail on summary when run fails', () => + shouldFailOnSummaryWhenRunFails(serverVersion, session)) + + it('should fail on subsequent summary when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, session)) + }) + + describe('transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {RxTransaction} */ + let txc + /** @type {ServerVersion} */ + let serverVersion + let originalTimeout + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + txc = await session.beginTransaction().toPromise() + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + if (txc) { + try { + await txc.commit().toPromise() + } catch (err) { + // ignore + } + } + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return keys', () => shouldReturnKeys(serverVersion, txc)) + + it('should return summary', () => shouldReturnSummary(serverVersion, txc)) + + it('should return keys and records', () => + shouldReturnKeysAndRecords(serverVersion, txc)) + + it('should return records and summary', () => + shouldReturnRecordsAndSummary(serverVersion, txc)) + + it('should return keys, records and summary', () => + shouldReturnKeysRecordsAndSummary(serverVersion, txc)) + + it('should return keys and summary but no records', () => + shouldReturnKeysAndSummaryButRecords(serverVersion, txc)) + + it('should return keys even after records are complete', () => + shouldReturnKeysEvenAfterRecordsAreComplete(serverVersion, txc)) + + it('should return keys even after summary is complete', () => + shouldReturnKeysEvenAfterSummaryIsComplete(serverVersion, txc)) + + it('should return keys multiple times', () => + shouldReturnKeysMultipleTimes(serverVersion, txc)) + + it('should return summary multiple times', () => + shouldReturnSummaryMultipleTimes(serverVersion, txc)) + + it('should return records only once', () => + shouldReturnRecordsOnlyOnce(serverVersion, txc)) + + it('should return empty keys for statement without return', () => + shouldReturnEmptyKeysForStatementWithNoReturn(serverVersion, txc)) + + it('should return no records for statement without return', () => + shouldReturnNoRecordsForStatementWithNoReturn(serverVersion, txc)) + + it('should return summary for statement without return', () => + shouldReturnSummaryForStatementWithNoReturn(serverVersion, txc)) + + it('should fail on keys when run fails', () => + shouldFailOnKeysWhenRunFails(serverVersion, txc)) + + it('should fail on subsequent keys when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, txc)) + + it('should fail on records when run fails', () => + shouldFailOnRecordsWhenRunFails(serverVersion, txc)) + + it('should fail on summary when run fails', () => + shouldFailOnSummaryWhenRunFails(serverVersion, txc)) + + it('should fail on subsequent summary when run fails', () => + shouldFailOnSubsequentKeysWhenRunFails(serverVersion, txc)) + }) + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeys (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await runnable + .run("RETURN 1 as f1, true as f2, 'string' as f3") + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([ + Notification.createNext(['f1', 'f2', 'f3']), + Notification.createComplete() + ]) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertSummary( + runnable.run("RETURN 1 as f1, true as f2, 'string' as f3") + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysAndRecords (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertRecords(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnRecordsAndSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysRecordsAndSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertRecords(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysAndSummaryButRecords (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertSummary(result) + + await collectAndAssertEmpty(result.records()) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysEvenAfterRecordsAreComplete ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysEvenAfterSummaryIsComplete (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertSummary(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnKeysMultipleTimes (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + await collectAndAssertKeys(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryMultipleTimes (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + await collectAndAssertSummary(result) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnRecordsOnlyOnce (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run( + "UNWIND RANGE(1,5) AS n RETURN n as number, 't'+n as text" + ) + + await collectAndAssertRecords(result) + await collectAndAssertEmpty(result.records()) + await collectAndAssertEmpty(result.records()) + await collectAndAssertEmpty(result.records()) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnEmptyKeysForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const keys = await runnable + .run('CREATE ({id : $id})', { id: 5 }) + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(keys).toEqual([ + Notification.createNext([]), + Notification.createComplete() + ]) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNoRecordsForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertEmpty( + runnable.run('CREATE ({id : $id})', { id: 5 }).records() + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryForStatementWithNoReturn ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await collectAndAssertSummary( + runnable.run('CREATE ({id : $id})', { id: 5 }), + 'w' + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnKeysWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError(result.keys(), error => { + expect(error.message).toContain('Invalid input') + expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSubsequentKeysWhenRunFails (version, runnable) { + function expectations (error) { + expect(error.message).toContain('Invalid input') + expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') + } + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError(result.keys(), expectations) + await collectAndAssertError(result.keys(), expectations) + await collectAndAssertError(result.keys(), expectations) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnRecordsWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError(result.records(), error => { + expect(error.message).toContain('Invalid input') + expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSummaryWhenRunFails (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError(result.summary(), error => { + expect(error.message).toContain('Invalid input') + expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldFailOnSubsequentSummaryWhenRunFails (version, runnable) { + function expectations (error) { + expect(error.message).toContain('Invalid input') + expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') + } + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = runnable.run('THIS IS NOT A CYPHER') + + await collectAndAssertError(result.summary(), expectations) + await collectAndAssertError(result.summary(), expectations) + await collectAndAssertError(result.summary(), expectations) + } + + async function collectAndAssertKeys (result) { + const keys = await result + .keys() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(keys).toEqual([ + Notification.createNext(['number', 'text']), + Notification.createComplete() + ]) + } + + async function collectAndAssertRecords (result) { + const records = await result + .records() + .pipe( + map(r => [r.get(0), r.get(1)]), + materialize(), + toArray() + ) + .toPromise() + expect(records).toEqual([ + Notification.createNext([neo4j.int(1), 't1']), + Notification.createNext([neo4j.int(2), 't2']), + Notification.createNext([neo4j.int(3), 't3']), + Notification.createNext([neo4j.int(4), 't4']), + Notification.createNext([neo4j.int(5), 't5']), + Notification.createComplete() + ]) + } + + async function collectAndAssertSummary (result, expectedStatementType = 'r') { + const summary = await result + .summary() + .pipe( + map(s => s.statementType), + materialize(), + toArray() + ) + .toPromise() + expect(summary).toEqual([ + Notification.createNext(expectedStatementType), + Notification.createComplete() + ]) + } + + async function collectAndAssertEmpty (stream) { + const result = await stream + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + /** + * + * @param {Observable} stream + * @param {function(err: Error): void} expectationFunc + */ + async function collectAndAssertError (stream, expectationFunc) { + const error = await stream + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + + expect(error).toBeTruthy() + expectationFunc(error) + } +}) diff --git a/test/rx/summary.test.js b/test/rx/summary.test.js new file mode 100644 index 000000000..f58d432b0 --- /dev/null +++ b/test/rx/summary.test.js @@ -0,0 +1,683 @@ +/** + * Copyright (c) 2002-2019 "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 neo4j from '../../src' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import RxTransaction from '../../src/transaction-rx' +import sharedNeo4j from '../internal/shared-neo4j' + +describe('#integration-rx summary', () => { + describe('session', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + await dropConstraintsAndIndices(driver) + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return non-null summary', () => + shouldReturnNonNullSummary(serverVersion, session)) + + it('should return summary with statement text', () => + shouldReturnSummaryWithStatementText(serverVersion, session)) + + it('should return summary with statement text and parameters', () => + shouldReturnSummaryWithStatementTextAndParams(serverVersion, session)) + + it('should return summary with statement type', () => + shouldReturnSummaryWithCorrectStatementType(serverVersion, session)) + + it('should return summary with correct counters for create', () => + shouldReturnSummaryWithUpdateStatisticsForCreate(serverVersion, session)) + + it('should return summary with correct counters for delete', () => + shouldReturnSummaryWithUpdateStatisticsForDelete(serverVersion, session)) + + it('should return summary with correct counters for index create', () => + shouldReturnSummaryWithUpdateStatisticsForIndexCreate( + serverVersion, + session + )) + + it('should return summary with correct counters for index drop', () => + shouldReturnSummaryWithUpdateStatisticsForIndexDrop( + serverVersion, + driver, + session + )) + + it('should return summary with correct counters for constraint create', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintCreate( + serverVersion, + session + )) + + it('should return summary with correct counters for constraint drop', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintDrop( + serverVersion, + driver, + session + )) + + it('should not return plan or profile', () => + shouldNotReturnPlanAndProfile(serverVersion, session)) + + it('should return plan but no profile', () => + shouldReturnPlanButNoProfile(serverVersion, session)) + + it('should return plan and profile', () => + shouldReturnPlanAndProfile(serverVersion, session)) + + it('should not return notification', () => + shouldNotReturnNotification(serverVersion, session)) + + it('should return notification', () => + shouldReturnNotification(serverVersion, session)) + }) + + describe('transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {RxTransaction} */ + let txc + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + txc = await session.beginTransaction().toPromise() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + await dropConstraintsAndIndices(driver) + }) + + afterEach(async () => { + if (txc) { + try { + await txc.commit().toPromise() + } catch (err) { + // ignore + } + } + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should return non-null summary', () => + shouldReturnNonNullSummary(serverVersion, txc)) + + it('should return summary with statement text', () => + shouldReturnSummaryWithStatementText(serverVersion, txc)) + + it('should return summary with statement text and parameters', () => + shouldReturnSummaryWithStatementTextAndParams(serverVersion, txc)) + + it('should return summary with statement type', () => + shouldReturnSummaryWithCorrectStatementType(serverVersion, txc)) + + it('should return summary with correct counters for create', () => + shouldReturnSummaryWithUpdateStatisticsForCreate(serverVersion, txc)) + + it('should return summary with correct counters for delete', () => + shouldReturnSummaryWithUpdateStatisticsForDelete(serverVersion, txc)) + + it('should return summary with correct counters for index create', () => + shouldReturnSummaryWithUpdateStatisticsForIndexCreate(serverVersion, txc)) + + it('should return summary with correct counters for index drop', () => + shouldReturnSummaryWithUpdateStatisticsForIndexDrop( + serverVersion, + driver, + txc + )) + + it('should return summary with correct counters for constraint create', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintCreate( + serverVersion, + txc + )) + + it('should return summary with correct counters for constraint drop', () => + shouldReturnSummaryWithUpdateStatisticsForConstraintDrop( + serverVersion, + driver, + txc + )) + + it('should not return plan or profile', () => + shouldNotReturnPlanAndProfile(serverVersion, txc)) + + it('should return plan but no profile', () => + shouldReturnPlanButNoProfile(serverVersion, txc)) + + it('should return plan and profile', () => + shouldReturnPlanAndProfile(serverVersion, txc)) + + it('should not return notification', () => + shouldNotReturnNotification(serverVersion, txc)) + + it('should return notification', () => + shouldReturnNotification(serverVersion, txc)) + }) + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNonNullSummary (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('UNWIND RANGE(1,10) AS n RETURN n') + .summary() + .toPromise() + + expect(summary).toBeDefined() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithStatementText (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementTextAndParameters( + runnable, + 'UNWIND RANGE(1, 10) AS n RETURN n' + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithStatementTextAndParams ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementTextAndParameters( + runnable, + 'UNWIND RANGE(1, $x) AS n RETURN n', + { x: 100 } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithCorrectStatementType ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyStatementType(runnable, 'CREATE (n)', 'w') + await verifyStatementType(runnable, 'MATCH (n) RETURN n LIMIT 1', 'r') + await verifyStatementType(runnable, 'CREATE (n) RETURN n', 'rw') + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters( + runnable, + 'CREATE (n:Label1 {id: $id1})-[:KNOWS]->(m:Label2 {id: $id2}) RETURN n, m', + { id1: 10, id2: 20 }, + { + nodesCreated: 2, + nodesDeleted: 0, + relationshipsCreated: 1, + relationshipsDeleted: 0, + propertiesSet: 2, + labelsAdded: 2, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForDelete ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-deleted nodes + await shouldReturnSummaryWithUpdateStatisticsForCreate(version, runnable) + + await verifyCounters( + runnable, + 'MATCH (n:Label1)-[r:KNOWS]->(m:Label2) DELETE n, r', + null, + { + nodesCreated: 0, + nodesDeleted: 1, + relationshipsCreated: 0, + relationshipsDeleted: 1, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForIndexCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters(runnable, 'CREATE INDEX on :Label(prop)', null, { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 1, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 0 + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForIndexDrop ( + version, + driver, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-dropped index + const session = driver.session() + try { + await session.run('CREATE INDEX on :Label(prop)') + } finally { + await session.close() + } + + await verifyCounters(runnable, 'DROP INDEX on :Label(prop)', null, { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 1, + constraintsAdded: 0, + constraintsRemoved: 0 + }) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForConstraintCreate ( + version, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + await verifyCounters( + runnable, + 'CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE', + null, + { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 1, + constraintsRemoved: 0 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnSummaryWithUpdateStatisticsForConstraintDrop ( + version, + driver, + runnable + ) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + // first create the to-be-dropped index + const session = driver.session() + try { + await session.run( + 'CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE' + ) + } finally { + await session.close() + } + + await verifyCounters( + runnable, + 'DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE', + null, + { + nodesCreated: 0, + nodesDeleted: 0, + relationshipsCreated: 0, + relationshipsDeleted: 0, + propertiesSet: 0, + labelsAdded: 0, + labelsRemoved: 0, + indexesAdded: 0, + indexesRemoved: 0, + constraintsAdded: 0, + constraintsRemoved: 1 + } + ) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldNotReturnPlanAndProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeFalsy() + expect(summary.plan).toBeFalsy() + expect(summary.hasProfile()).toBeFalsy() + expect(summary.profile).toBeFalsy() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnPlanButNoProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('EXPLAIN CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeTruthy() + expect(summary.plan.operatorType).toBe('ProduceResults') + expect(summary.plan.identifiers).toEqual(['n']) + expect(summary.hasProfile()).toBeFalsy() + expect(summary.profile).toBeFalsy() + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnPlanAndProfile (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('PROFILE CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.hasPlan()).toBeTruthy() + expect(summary.plan.operatorType).toBe('ProduceResults') + expect(summary.plan.identifiers).toEqual(['n']) + expect(summary.hasProfile()).toBeTruthy() + expect(summary.profile.operatorType).toBe('ProduceResults') + expect(summary.profile.identifiers).toEqual(['n']) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldNotReturnNotification (version, runnable) { + if (version.compareTo(VERSION_4_0_0) < 0) { + return + } + + const summary = await runnable + .run('CREATE (n) RETURN n') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.notifications).toBeTruthy() + expect(summary.notifications.length).toBe(0) + } + + /** + * @param {ServerVersion} version + * @param {RxSession|RxTransaction} runnable + */ + async function shouldReturnNotification (version, runnable) { + // TODO: seems to be flaky + return + + if (version.compareTo(VERSION_4_0_0) < 0) { + } + + const summary = await runnable + .run('CYPHER runtime=interpreted EXPLAIN MATCH (n),(m) RETURN n,m') + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.notifications).toBeTruthy() + expect(summary.notifications.length).toBeGreaterThan(0) + expect(summary.notifications[0].code).toBe( + 'Neo.ClientNotification.Statement.CartesianProductWarning' + ) + expect(summary.notifications[0].title).toBe( + 'This query builds a cartesian product between disconnected patterns.' + ) + expect(summary.notifications[0].description).toBe( + 'If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifier is: (m))' + ) + expect(summary.notifications[0].severity).toBe('WARNING') + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {*} parameters + */ + async function verifyStatementTextAndParameters ( + runnable, + statement, + parameters = null + ) { + const summary = await runnable + .run(statement, parameters) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.statement).toBeDefined() + expect(summary.statement.text).toBe(statement) + expect(summary.statement.parameters).toEqual(parameters || {}) + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {string} expectedStatementType + */ + async function verifyStatementType ( + runnable, + statement, + expectedStatementType + ) { + const summary = await runnable + .run(statement) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect(summary.statementType).toBe(expectedStatementType) + } + + /** + * + * @param {RxSession|RxTransaction} runnable + * @param {string} statement + * @param {*} parameters + * @param {*} counters + */ + async function verifyCounters (runnable, statement, parameters, counters) { + const summary = await runnable + .run(statement, parameters) + .summary() + .toPromise() + expect(summary).toBeDefined() + expect({ + nodesCreated: summary.counters.nodesCreated(), + nodesDeleted: summary.counters.nodesDeleted(), + relationshipsCreated: summary.counters.relationshipsCreated(), + relationshipsDeleted: summary.counters.relationshipsDeleted(), + propertiesSet: summary.counters.propertiesSet(), + labelsAdded: summary.counters.labelsAdded(), + labelsRemoved: summary.counters.labelsRemoved(), + indexesAdded: summary.counters.indexesAdded(), + indexesRemoved: summary.counters.indexesRemoved(), + constraintsAdded: summary.counters.constraintsAdded(), + constraintsRemoved: summary.counters.constraintsRemoved() + }).toEqual(counters) + } + + async function dropConstraintsAndIndices (driver) { + const session = driver.session() + try { + const constraints = await session.run( + "CALL db.constraints() yield description RETURN 'DROP ' + description" + ) + for (let i = 0; i < constraints.records.length; i++) { + await session.run(constraints.records[0].get(0)) + } + + const indices = await session.run( + "CALL db.indexes() yield description RETURN 'DROP ' + description" + ) + for (let i = 0; i < indices.records.length; i++) { + await session.run(indices.records[0].get(0)) + } + } finally { + await session.close() + } + } +}) diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js new file mode 100644 index 000000000..0ddd0c258 --- /dev/null +++ b/test/rx/transaction.test.js @@ -0,0 +1,752 @@ +/** + * Copyright (c) 2002-2019 "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 { Notification, throwError } from 'rxjs' +import { + flatMap, + materialize, + toArray, + concat, + map, + bufferCount, + catchError +} from 'rxjs/operators' +import neo4j from '../../src' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' +import RxSession from '../../src/session-rx' +import RxTransaction from '../../src/transaction-rx' +import sharedNeo4j from '../internal/shared-neo4j' +import { newError } from '../../src/error' + +describe('#integration-rx transaction', () => { + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + }) + + it('should commit an empty transaction', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => txc.commit()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should rollback an empty transaction', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => txc.rollback()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should run statement and commit', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('CREATE (n:Node {id: 42}) RETURN n') + .records() + .pipe( + map(r => r.get('n').properties['id']), + concat(txc.commit()) + ) + ), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(42)), + Notification.createComplete() + ]) + + expect(await countNodes(42)).toBe(1) + }) + + it('should run statement and rollback', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const result = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('CREATE (n:Node {id: 42}) RETURN n') + .records() + .pipe( + map(r => r.get('n').properties['id']), + concat(txc.rollback()) + ) + ), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(42)), + Notification.createComplete() + ]) + + expect(await countNodes(42)).toBe(0) + }) + + it('should run multiple statements and commit', async () => { + await verifyCanRunMultipleStatements(true) + }) + + it('should run multiple statements and rollback', async () => { + await verifyCanRunMultipleStatements(false) + }) + + it('should run multiple statements without waiting and commit', async () => { + await verifyCanRunMultipleStatementsWithoutWaiting(true) + }) + + it('should run multiple statements without waiting and rollback', async () => { + await verifyCanRunMultipleStatementsWithoutWaiting(false) + }) + + it('should run multiple statements without streaming and commit', async () => { + await verifyCanRunMultipleStatementsWithoutStreaming(true) + }) + + it('should run multiple statements without streaming and rollback', async () => { + await verifyCanRunMultipleStatementsWithoutStreaming(false) + }) + + it('should fail to commit after a failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const error = await txc + .commit() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error).toBeTruthy() + expect(error.error).toContain( + 'Cannot commit statements in this transaction' + ) + }) + + it('should succeed to rollback after a failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to commit after successful and failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 5) + await verifyCanReturnOne(txc) + await verifyFailsWithWrongStatement(txc) + + const error = await txc + .commit() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error).toBeTruthy() + expect(error.error).toContain( + 'Cannot commit statements in this transaction' + ) + }) + + it('should succeed to rollback after successful and failed statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 5) + await verifyCanReturnOne(txc) + await verifyFailsWithWrongStatement(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to run another statement after a failed one', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyFailsWithWrongStatement(txc) + + const error = await txc + .run('CREATE ()') + .records() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error).toBeTruthy() + expect(error.error).toContain( + 'Cannot run statement, because previous statements in the transaction has failed' + ) + }) + + it('should allow commit after commit', async () => { + // TODO: behaviour difference across drivers + return + + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanCommit(txc) + + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should allow rollback after rollback', async () => { + // TODO: behaviour difference across drivers + return + + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanRollback(txc) + + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + }) + + it('should fail to rollback after commit', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanCommit(txc) + + const error = await txc + .rollback() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error.error).toContain( + 'Cannot rollback transaction, because transaction has already been successfully closed' + ) + }) + + it('should fail to commit after rollback', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await verifyCanCreateNode(txc, 6) + await verifyCanRollback(txc) + + const error = await txc + .commit() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error.error).toContain( + 'Cannot commit this transaction, because it has already been rolled back' + ) + }) + + it('should fail to run statement after committed transaction', async () => { + await verifyFailToRunStatementAfterTxcIsComplete(true) + }) + + it('should fail to run statement after rollbacked transaction', async () => { + await verifyFailToRunStatementAfterTxcIsComplete(false) + }) + + it('should update bookmark', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const bookmark0 = session.lastBookmark() + + const txc1 = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc1, 20) + await verifyCanCommit(txc1) + const bookmark1 = session.lastBookmark() + + const txc2 = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc2, 21) + await verifyCanCommit(txc2) + const bookmark2 = session.lastBookmark() + + expect(bookmark0).toBeFalsy() + expect(bookmark1).toBeTruthy() + expect(bookmark1).not.toEqual(bookmark0) + expect(bookmark2).toBeTruthy() + expect(bookmark2).not.toEqual(bookmark1) + }) + + it('should propagate failures from statements', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (:TestNode) RETURN 1 AS n') + const result2 = txc.run('CREATE (:TestNode) RETURN 2 AS n') + const result3 = txc.run('RETURN 10 / 0 AS n') + const result4 = txc.run('CREATE (:TestNode) RETURN 3 AS n') + + const result = await result1 + .records() + .pipe( + concat(result2.records()), + concat(result3.records()), + concat(result4.records()), + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createError(newError('/ by zero')) + ]) + + await verifyCanRollback(txc) + }) + + it('should not run until subscribed', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('RETURN 1') + const result2 = txc.run('RETURN 2') + const result3 = txc.run('RETURN 3') + const result4 = txc.run('RETURN 4') + + const result = await result4 + .records() + .pipe( + concat(result3.records()), + concat(result2.records()), + concat(result1.records()), + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(4), + Notification.createNext(3), + Notification.createNext(2), + Notification.createNext(1), + Notification.createComplete() + ]) + + await verifyCanCommit(txc) + }) + + it('should not propagate failure on commit if not executed', async () => { + await verifyNoFailureIfNotExecuted(true) + }) + + it('should not propagate failure on rollback if not executed', async () => { + await verifyNoFailureIfNotExecuted(false) + }) + + it('should not propagate run failure from summary', async () => { + // TODO: behaviour difference across drivers + return + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + } + + const txc = await session.beginTransaction().toPromise() + const result = txc.run('RETURN Wrong') + + const error = await result + .records() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error.message).toContain('Variable `Wrong` not defined') + + const summary = await result.summary().toPromise() + expect(summary).toBeTruthy() + }) + + it('should handle nested queries', async () => { + const size = 1024 + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const messages = await session + .beginTransaction() + .pipe( + flatMap(txc => + txc + .run('UNWIND RANGE(1, $size) AS x RETURN x', { size }) + .records() + .pipe( + map(r => r.get(0)), + bufferCount(50), + flatMap(x => + txc + .run('UNWIND $x AS id CREATE (n:Node {id: id}) RETURN n.id', { + x + }) + .records() + ), + map(r => r.get(0)), + concat(txc.commit()), + catchError(err => txc.rollback().pipe(concat(throwError(err)))), + materialize(), + toArray() + ) + ) + ) + .toPromise() + + expect(messages.length).toBe(size + 1) + expect(messages[size]).toEqual(Notification.createComplete()) + }) + + async function verifyNoFailureIfNotExecuted (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + txc.run('RETURN ILLEGAL') + + await verifyCanCommitOrRollback(txc, commit) + } + + async function verifyFailToRunStatementAfterTxcIsComplete (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + await verifyCanCreateNode(txc, 15) + await verifyCanCommitOrRollback(txc, commit) + + const error = await txc + .run('CREATE ()') + .records() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + expect(error.error).toContain('Cannot run statement, because transaction') + } + + async function verifyCanRunMultipleStatements (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + await txc + .run('CREATE (n:Node {id: 1})') + .summary() + .toPromise() + await txc + .run('CREATE (n:Node {id: 2})') + .summary() + .toPromise() + await txc + .run('CREATE (n:Node {id: 1})') + .summary() + .toPromise() + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanRunMultipleStatementsWithoutWaiting (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (n:Node {id: 1})') + const result2 = txc.run('CREATE (n:Node {id: 2})') + const result3 = txc.run('CREATE (n:Node {id: 1})') + + const results = await result1 + .records() + .pipe( + concat(result2.records()), + concat(result3.records()), + materialize(), + toArray() + ) + .toPromise() + expect(results).toEqual([Notification.createComplete()]) + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanRunMultipleStatementsWithoutStreaming (commit) { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + + const txc = await session.beginTransaction().toPromise() + + const result1 = txc.run('CREATE (n:Node {id: 1})') + const result2 = txc.run('CREATE (n:Node {id: 2})') + const result3 = txc.run('CREATE (n:Node {id: 1})') + + const results = await result1 + .keys() + .pipe( + concat(result2.keys()), + concat(result3.keys()), + materialize(), + toArray() + ) + .toPromise() + expect(results).toEqual([ + Notification.createNext([]), + Notification.createNext([]), + Notification.createNext([]), + Notification.createComplete() + ]) + + await verifyCanCommitOrRollback(txc, commit) + await verifyCommittedOrRollbacked(commit) + } + + async function verifyCanCommit (txc) { + const result = await txc + .commit() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + async function verifyCanRollback (txc) { + const result = await txc + .rollback() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([Notification.createComplete()]) + } + + async function verifyCanCommitOrRollback (txc, commit) { + if (commit) { + await verifyCanCommit(txc) + } else { + await verifyCanRollback(txc) + } + } + + async function verifyCanCreateNode (txc, id) { + const result = await txc + .run('CREATE (n:Node {id: $id}) RETURN n', { id: neo4j.int(id) }) + .records() + .pipe( + map(r => r.get('n').properties['id']), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(id)), + Notification.createComplete() + ]) + } + + async function verifyCanReturnOne (txc) { + const result = await txc + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0)), + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(neo4j.int(1)), + Notification.createComplete() + ]) + } + + async function verifyFailsWithWrongStatement (txc) { + const error = await txc + .run('RETURN') + .records() + .pipe( + materialize(), + map(n => n.error) + ) + .toPromise() + + expect(error).toBeTruthy() + expect(error.code).toContain('SyntaxError') + } + + async function verifyCommittedOrRollbacked (commit) { + if (commit) { + expect(await countNodes(1)).toBe(2) + expect(await countNodes(2)).toBe(1) + } else { + expect(await countNodes(1)).toBe(0) + expect(await countNodes(2)).toBe(0) + } + } + + async function countNodes (id) { + const session = driver.rxSession() + return await session + .run('MATCH (n:Node {id: $id}) RETURN count(n)', { id: id }) + .records() + .pipe( + map(r => r.get(0).toInt()), + concat(session.close()) + ) + .toPromise() + } +}) From 0c2b7b931d57502c7faf20a5ce62c8449561a01e Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Fri, 9 Aug 2019 12:11:44 +0100 Subject: [PATCH 04/14] Added support for transaction functions --- src/driver.js | 9 +- src/internal/retry-logic-rx.js | 153 +++++++++++++++ src/session-rx.js | 67 ++++++- test/internal/request-message.test.js | 8 +- test/internal/retry-logic-rx.test.js | 257 ++++++++++++++++++++++++ test/rx/session.test.js | 273 ++++++++++++++++++++++++++ test/rx/summary.test.js | 4 +- test/rx/transaction.test.js | 13 +- 8 files changed, 762 insertions(+), 22 deletions(-) create mode 100644 src/internal/retry-logic-rx.js create mode 100644 test/internal/retry-logic-rx.test.js create mode 100644 test/rx/session.test.js diff --git a/src/driver.js b/src/driver.js index 9bfea77ed..2f61a544c 100644 --- a/src/driver.js +++ b/src/driver.js @@ -141,14 +141,15 @@ class Driver { } rxSession ({ defaultAccessMode = WRITE, bookmarks, database = '' } = {}) { - return new RxSession( - this._newSession({ + return new RxSession({ + session: this._newSession({ defaultAccessMode, bookmarks, database, reactive: true - }) - ) + }), + config: this._config + }) } _newSession ({ defaultAccessMode, bookmarkOrBookmarks, database, reactive }) { diff --git a/src/internal/retry-logic-rx.js b/src/internal/retry-logic-rx.js new file mode 100644 index 000000000..46aa98e29 --- /dev/null +++ b/src/internal/retry-logic-rx.js @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2002-2019 "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, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../error' +import { Observable, throwError, of } from 'rxjs' +import { retryWhen, flatMap, delay } from 'rxjs/operators' +import Logger from './logger' + +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 + +export default class RxRetryLogic { + /** + * + * @param {Object} args + * @param {Logger} args.logger + */ + constructor ({ + maxRetryTimeout = DEFAULT_MAX_RETRY_TIME_MS, + initialDelay = DEFAULT_INITIAL_RETRY_DELAY_MS, + delayMultiplier = DEFAULT_RETRY_DELAY_MULTIPLIER, + delayJitter = DEFAULT_RETRY_DELAY_JITTER_FACTOR, + logger = null + } = {}) { + this._maxRetryTimeout = valueOrDefault( + maxRetryTimeout, + DEFAULT_MAX_RETRY_TIME_MS + ) + this._initialDelay = valueOrDefault( + initialDelay, + DEFAULT_INITIAL_RETRY_DELAY_MS + ) + this._delayMultiplier = valueOrDefault( + delayMultiplier, + DEFAULT_RETRY_DELAY_MULTIPLIER + ) + this._delayJitter = valueOrDefault( + delayJitter, + DEFAULT_RETRY_DELAY_JITTER_FACTOR + ) + this._logger = logger + } + + /** + * + * @param {Observable} work + */ + retry (work) { + return work.pipe( + retryWhen(failedWork => { + const handledExceptions = [] + const startTime = Date.now() + let retryCount = 1 + let delayDuration = this._initialDelay + + return failedWork.pipe( + flatMap(err => { + if (!RxRetryLogic._canRetryOn(err)) { + return throwError(err) + } + + handledExceptions.push(err) + + if ( + retryCount >= 2 && + Date.now() - startTime >= this._maxRetryTimeout + ) { + const error = newError( + `Failed after retried for ${retryCount} times in ${ + this._maxRetryTimeout + } ms. Make sure that your database is online and retry again.`, + SERVICE_UNAVAILABLE + ) + + error.seenErrors = handledExceptions + + return throwError(error) + } + + const nextDelayDuration = this._computeNextDelay(delayDuration) + delayDuration = delayDuration * this._delayMultiplier + retryCount++ + if (this._logger) { + this._logger.warn( + `Transaction failed and will be retried in ${nextDelayDuration}` + ) + } + return of(1).pipe(delay(nextDelayDuration)) + }) + ) + }) + ) + } + + _computeNextDelay (delay) { + const jitter = delay * this._delayJitter + return delay - jitter + 2 * jitter * Math.random() + } + + static _canRetryOn (error) { + return ( + error && + error.code && + (error.code === SERVICE_UNAVAILABLE || + error.code === SESSION_EXPIRED || + this._isTransientError(error)) + ) + } + + static _isTransientError (error) { + // Retries should not happen when transaction was explicitly terminated by the user. + // Termination of transaction might result in two different error codes depending on where it was + // terminated. These are really client errors but classification on the server is not entirely correct and + // they are classified as transient. + + const code = error.code + if (code.indexOf('TransientError') >= 0) { + if ( + code === 'Neo.TransientError.Transaction.Terminated' || + code === 'Neo.TransientError.Transaction.LockClientStopped' + ) { + return false + } + return true + } + return false + } +} + +function valueOrDefault (value, defaultValue) { + if (value || value === 0) { + return value + } + return defaultValue +} diff --git a/src/session-rx.js b/src/session-rx.js index 6dfc4b60a..0597a83f0 100644 --- a/src/session-rx.js +++ b/src/session-rx.js @@ -16,19 +16,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { defer, Observable } from 'rxjs' -import { map } from 'rxjs/operators' +import { defer, Observable, throwError } from 'rxjs' +import { map, flatMap, catchError, concat } from 'rxjs/operators' import { newError } from './error' import RxResult from './result-rx' import Session from './session' import RxTransaction from './transaction-rx' +import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' +import TxConfig from './internal/tx-config' +import RxRetryLogic from './internal/retry-logic-rx' export default class RxSession { /** * @param {Session} session */ - constructor (session) { + constructor ({ session, config } = {}) { this._session = session + this._retryLogic = _createRetryLogic(config) } run (statement, parameters, transactionConfig) { @@ -49,10 +53,21 @@ export default class RxSession { } beginTransaction (transactionConfig) { + return this._beginTransaction(this._session._mode, transactionConfig) + } + + _beginTransaction (accessMode, transactionConfig) { + let txConfig = TxConfig.empty() + if (transactionConfig) { + txConfig = new TxConfig(transactionConfig) + } + return new Observable(observer => { try { observer.next( - new RxTransaction(this._session.beginTransaction(transactionConfig)) + new RxTransaction( + this._session._beginTransaction(accessMode, txConfig) + ) ) observer.complete() } catch (err) { @@ -64,11 +79,43 @@ export default class RxSession { } readTransaction (transactionWork, transactionConfig) { - throw newError('not implemented') + return this._runTransaction( + ACCESS_MODE_READ, + transactionWork, + transactionConfig + ) } writeTransaction (transactionWork, transactionConfig) { - throw newError('not implemented') + return this._runTransaction( + ACCESS_MODE_WRITE, + transactionWork, + transactionConfig + ) + } + + _runTransaction (accessMode, transactionWork, transactionConfig) { + let txConfig = TxConfig.empty() + if (transactionConfig) { + txConfig = new TxConfig(transactionConfig) + } + + return this._retryLogic.retry( + this._beginTransaction(accessMode, transactionConfig).pipe( + flatMap(txc => + defer(() => { + try { + return transactionWork(txc) + } catch (err) { + return throwError(err) + } + }).pipe( + catchError(err => txc.rollback().pipe(concat(throwError(err)))), + concat(txc.commit()) + ) + ) + ) + ) } close () { @@ -86,3 +133,11 @@ export default class RxSession { return this._session.lastBookmark() } } + +function _createRetryLogic (config) { + const maxRetryTimeout = + config && config.maxTransactionRetryTime + ? config.maxTransactionRetryTime + : null + return new RxRetryLogic({ maxRetryTimeout }) +} diff --git a/test/internal/request-message.test.js b/test/internal/request-message.test.js index d46ee0410..47b565249 100644 --- a/test/internal/request-message.test.js +++ b/test/internal/request-message.test.js @@ -184,11 +184,11 @@ describe('#unit RequestMessage', () => { verify(RequestMessage.pull({ n: 501 }), 0x3f, { n: int(501) }, 'PULL') }) - it('should create PULL message with stmt_id and n', () => { + it('should create PULL message with qid and n', () => { verify( RequestMessage.pull({ stmtId: 27, n: 1023 }), 0x3f, - { n: int(1023), stmt_id: int(27) }, + { n: int(1023), qid: int(27) }, 'PULL' ) }) @@ -206,11 +206,11 @@ describe('#unit RequestMessage', () => { ) }) - it('should create DISCARD message with stmt_id and n', () => { + it('should create DISCARD message with qid and n', () => { verify( RequestMessage.discard({ stmtId: 27, n: 1023 }), 0x2f, - { n: int(1023), stmt_id: int(27) }, + { n: int(1023), qid: int(27) }, 'DISCARD' ) }) diff --git a/test/internal/retry-logic-rx.test.js b/test/internal/retry-logic-rx.test.js new file mode 100644 index 000000000..189aebd35 --- /dev/null +++ b/test/internal/retry-logic-rx.test.js @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2002-2019 "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, SESSION_EXPIRED, SERVICE_UNAVAILABLE } from '../../src/error' +import RxRetryLogic from '../../src/internal/retry-logic-rx' +import { defer, throwError, of } from 'rxjs' +import { TestScheduler } from 'rxjs/testing' +import Logger from '../../src/internal/logger' + +describe('#unit-rx retrylogic', () => { + let scheduler + let loggerFunc + let logger + let clock + + beforeEach(() => { + scheduler = new TestScheduler(assertDeepEqualSkipFrame) + loggerFunc = jasmine.createSpy() + logger = new Logger('debug', loggerFunc) + + clock = jasmine.clock() + clock.install() + clock.mockDate(new Date()) + }) + + afterEach(() => clock.uninstall()) + + describe('should not retry on non-transient errors', () => { + let scheduler + + beforeEach(() => { + scheduler = new TestScheduler(assertDeepEqual) + }) + + it('a js error', () => { + verifyNoRetry(new Error('a random error')) + }) + + it('a neo4j error', () => { + verifyNoRetry(newError('a neo4j error')) + }) + + it('a transaction terminated error', () => { + verifyNoRetry( + newError( + 'transaction terminated', + 'Neo.TransientError.Transaction.Terminated' + ) + ) + }) + + it('a lock client stopped error', () => { + verifyNoRetry( + newError( + 'lock client stopped', + 'Neo.TransientError.Transaction.LockClientStopped' + ) + ) + }) + + function verifyNoRetry (error) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = helpers.cold('-a-b-c-#', { a: 1, b: 2, c: 3 }, error) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-b-c-#', { a: 1, b: 2, c: 3 }, error) + }) + } + }) + + describe('should retry on transient errors', () => { + it('a database unavailable error', () => { + verifyRetry( + newError( + 'database unavailable', + 'Neo.TransientError.Database.Unavailable' + ) + ) + }) + + it('a session expired error', () => { + verifyRetry(newError('session expired', SESSION_EXPIRED)) + }) + + it('a service unavailable error', () => { + verifyRetry(newError('service unavailable', SERVICE_UNAVAILABLE)) + }) + + function verifyRetry (error) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = newFailingObserver({ value: 1, errors: [error] }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 1 }) + }) + } + }) + + describe('should log retries', () => { + it('with 1 retry', () => { + verifyLogging(1) + }) + + it('with 2 retries', () => { + verifyLogging(2) + }) + + it('with 5 retries', () => { + verifyLogging(5) + }) + + function verifyLogging (errorCount) { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 60000, logger }) + const observable = newFailingObserver({ + errors: sequenceOf( + newError('session expired', SESSION_EXPIRED), + errorCount + ), + value: 10 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 10 }) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(errorCount) + expect(loggerFunc.calls.allArgs()).toEqual( + sequenceOf( + [ + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ], + errorCount + ) + ) + } + }) + + it('should not retry on success', () => { + scheduler = new TestScheduler(assertDeepEqual) + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 }) + const observable = helpers.cold('-a-|', { a: 5 }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 5 }) + }) + }) + + it('should retry at least twice', () => { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 2000, logger }) + const observable = newFailingObserver({ + delayBy: 2000, + errors: [newError('session expired', SESSION_EXPIRED)], + value: 10 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe('-a-|', { a: 10 }) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(1) + expect(loggerFunc).toHaveBeenCalledWith( + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ) + }) + + it('should fail with service unavailable', () => { + scheduler.run(helpers => { + const retryLogic = new RxRetryLogic({ maxRetryTimeout: 2000, logger }) + const observable = newFailingObserver({ + delayBy: 1000, + errors: sequenceOf(newError('session expired', SESSION_EXPIRED), 3), + value: 15 + }) + + helpers + .expectObservable(retryLogic.retry(observable)) + .toBe( + '-#', + null, + newError( + 'Failed after retried for 3 times in 2000 ms. Make sure that your database is online and retry again.', + SERVICE_UNAVAILABLE + ) + ) + }) + + expect(loggerFunc).toHaveBeenCalledTimes(2) + expect(loggerFunc.calls.allArgs()).toEqual( + sequenceOf( + [ + 'warn', + jasmine.stringMatching(/^Transaction failed and will be retried in/) + ], + 2 + ) + ) + }) + + function newFailingObserver ({ delayBy = 0, value, errors = [] } = {}) { + let index = 0 + return defer(() => { + if (delayBy) { + clock.tick(delayBy) + } + if (index < errors.length) { + return throwError(errors[index++]) + } else { + return of(value) + } + }) + } + + function sequenceOf (obj, n) { + return Array.from({ length: n }, _ => obj) + } + + function assertDeepEqual (actual, expected) { + expect(actual).toEqual(expected) + } + + function assertDeepEqualSkipFrame (actual, expected) { + expect(actual.length).toBeDefined() + expect(expected.length).toBeDefined() + + expect(actual.map(m => m.notification)).toEqual( + expected.map(m => m.notification) + ) + } +}) diff --git a/test/rx/session.test.js b/test/rx/session.test.js new file mode 100644 index 000000000..0430a5ac5 --- /dev/null +++ b/test/rx/session.test.js @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2002-2019 "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 { Notification, throwError } from 'rxjs' +import { map, materialize, toArray, concat } from 'rxjs/operators' +import neo4j from '../../src' +import { ServerVersion } from '../../src/internal/server-version' +import sharedNeo4j from '../internal/shared-neo4j' +import { newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../../src/error' + +describe('#integration rx-session', () => { + let originalTimeout + let driver + /** @type {RxSession} */ + let session + /** @type {ServerVersion} */ + let serverVersion + + beforeEach(async () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + session = driver.rxSession() + + const normalSession = driver.session() + try { + const result = await normalSession.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) + } finally { + await normalSession.close() + } + + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000 + }) + + afterEach(async () => { + if (session) { + await session.close().toPromise() + } + driver.close() + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + }) + + it('should be able to run a simple statement', async () => { + const result = await session + .run('UNWIND [1,2,3,4] AS n RETURN n') + .records() + .pipe( + map(r => r.get('n').toInt()), + materialize(), + toArray() + ) + .toPromise() + + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createNext(3), + Notification.createNext(4), + Notification.createComplete() + ]) + }) + + it('should be able to reuse session after failure', async () => { + const result1 = await session + .run('INVALID STATEMENT') + .records() + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result1).toEqual([ + Notification.createError(jasmine.stringMatching(/Invalid input/)) + ]) + + const result2 = await session + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0).toInt()), + materialize(), + toArray() + ) + .toPromise() + expect(result2).toEqual([ + Notification.createNext(1), + Notification.createComplete() + ]) + }) + + it('should run transactions without retries', async () => { + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithoutRetry) RETURN 5' + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(5), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(1) + expect(await countNodes('WithoutRetry')).toBe(1) + }) + + it('should run transaction with retries on reactive failures', async () => { + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithReactiveFailure) RETURN 7', + reactiveFailures: [ + newError('service unavailable', SERVICE_UNAVAILABLE), + newError('session expired', SESSION_EXPIRED), + newError('transient error', 'Neo.TransientError.Transaction.NotStarted') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(7), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(4) + expect(await countNodes('WithReactiveFailure')).toBe(1) + }) + + it('should run transaction with retries on synchronous failures', async () => { + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:WithSyncFailure) RETURN 9', + syncFailures: [ + newError('service unavailable', SERVICE_UNAVAILABLE), + newError('session expired', SESSION_EXPIRED), + newError('transient error', 'Neo.TransientError.Transaction.NotStarted') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(9), + Notification.createComplete() + ]) + + expect(txcWork.invocations).toBe(4) + expect(await countNodes('WithSyncFailure')).toBe(1) + }) + + it('should fail on transactions that cannot be retried', async () => { + const txcWork = new ConfigurableTransactionWork({ + statement: 'UNWIND [10, 5, 0] AS x CREATE (:Hi) RETURN 10/x' + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createNext(1), + Notification.createNext(2), + Notification.createError(jasmine.stringMatching(/\/ by zero/)) + ]) + + expect(txcWork.invocations).toBe(1) + expect(await countNodes('Hi')).toBe(0) + }) + + it('should fail even after a transient error', async () => { + const txcWork = new ConfigurableTransactionWork({ + statement: 'CREATE (:Person) RETURN 1', + syncFailures: [ + newError( + 'a transient error', + 'Neo.TransientError.Transaction.NotStarted' + ) + ], + reactiveFailures: [ + newError('a database error', 'Neo.Database.Not.Started') + ] + }) + + const result = await session + .writeTransaction(txc => txcWork.work(txc)) + .pipe( + materialize(), + toArray() + ) + .toPromise() + expect(result).toEqual([ + Notification.createError(jasmine.stringMatching(/a database error/)) + ]) + + expect(txcWork.invocations).toBe(2) + expect(await countNodes('Person')).toBe(0) + }) + + async function countNodes (label) { + const session = driver.rxSession() + return await session + .run(`MATCH (n:${label}) RETURN count(n)`) + .records() + .pipe( + map(r => r.get(0).toInt()), + concat(session.close()) + ) + .toPromise() + } + class ConfigurableTransactionWork { + constructor ({ statement, syncFailures = [], reactiveFailures = [] } = {}) { + this._statement = statement + this._syncFailures = syncFailures + this._syncFailuresIndex = 0 + this._reactiveFailures = reactiveFailures + this._reactiveFailuresIndex = 0 + this._invocations = 0 + } + + get invocations () { + return this._invocations + } + + work (txc) { + this._invocations++ + + if (this._syncFailuresIndex < this._syncFailures.length) { + throw this._syncFailures[this._syncFailuresIndex++] + } + + if (this._reactiveFailuresIndex < this._reactiveFailures.length) { + return throwError(this._reactiveFailures[this._reactiveFailuresIndex++]) + } + + return txc + .run(this._statement) + .records() + .pipe(map(r => r.get(0).toInt())) + } + } +}) diff --git a/test/rx/summary.test.js b/test/rx/summary.test.js index f58d432b0..ad1c69a33 100644 --- a/test/rx/summary.test.js +++ b/test/rx/summary.test.js @@ -567,10 +567,10 @@ describe('#integration-rx summary', () => { * @param {RxSession|RxTransaction} runnable */ async function shouldReturnNotification (version, runnable) { - // TODO: seems to be flaky - return + pending('seems to be flaky') if (version.compareTo(VERSION_4_0_0) < 0) { + return } const summary = await runnable diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js index 0ddd0c258..83d865b18 100644 --- a/test/rx/transaction.test.js +++ b/test/rx/transaction.test.js @@ -287,10 +287,10 @@ describe('#integration-rx transaction', () => { }) it('should allow commit after commit', async () => { - // TODO: behaviour difference across drivers - return + pending('behaviour difference across drivers') if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return } const txc = await session.beginTransaction().toPromise() @@ -309,10 +309,10 @@ describe('#integration-rx transaction', () => { }) it('should allow rollback after rollback', async () => { - // TODO: behaviour difference across drivers - return + pending('behaviour difference across drivers') if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return } const txc = await session.beginTransaction().toPromise() @@ -481,9 +481,10 @@ describe('#integration-rx transaction', () => { }) it('should not propagate run failure from summary', async () => { - // TODO: behaviour difference across drivers - return + pending('behaviour difference across drivers') + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return } const txc = await session.beginTransaction().toPromise() From 3cc34e3dc920bce9c3fa57687d1dcda92a3b5d12 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Fri, 9 Aug 2019 15:58:05 +0100 Subject: [PATCH 05/14] Default to unencrypted connections and skip some related tests for 4.0 As a temporary workaround until security work is completed in js driver --- src/internal/node/node-channel.js | 2 +- test/examples.test.js | 9 +- test/internal/node/tls.test.js | 177 +++++++++++++++++------------- test/rx/navigation.test.js | 71 ++++++------ test/rx/session.test.js | 26 ++++- test/rx/transaction.test.js | 120 +++++++++++++------- test/session.test.js | 6 +- test/summary.test.js | 12 +- test/transaction.test.js | 5 +- 9 files changed, 268 insertions(+), 160 deletions(-) diff --git a/src/internal/node/node-channel.js b/src/internal/node/node-channel.js index cd6308349..62782031e 100644 --- a/src/internal/node/node-channel.js +++ b/src/internal/node/node-channel.js @@ -181,7 +181,7 @@ function isEncrypted (config) { config.encrypted == null || config.encrypted === undefined if (encryptionNotConfigured) { // default to using encryption if trust-all-certificates is available - return true + return false } return config.encrypted === true || config.encrypted === ENCRYPTION_ON } diff --git a/test/examples.test.js b/test/examples.test.js index d7fde7492..607bad217 100644 --- a/test/examples.test.js +++ b/test/examples.test.js @@ -19,6 +19,7 @@ import neo4j from '../src' import sharedNeo4j from './internal/shared-neo4j' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' /** * The tests below are examples that get pulled into the Driver Manual using the tags inside the tests. @@ -32,6 +33,7 @@ describe('#integration examples', () => { const originalConsole = console let driverGlobal + let version let originalTimeout let consoleOverride @@ -57,7 +59,8 @@ describe('#integration examples', () => { const session = driverGlobal.session() try { - await session.run('MATCH (n) DETACH DELETE n') + const result = await session.run('MATCH (n) DETACH DELETE n') + version = ServerVersion.fromString(result.summary.server.version) } finally { await session.close() } @@ -155,6 +158,10 @@ describe('#integration examples', () => { }) it('config trust example', done => { + if (version.compareTo(VERSION_4_0_0) >= 0) { + pending('address within security work') + } + // tag::config-trust[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { encrypted: 'ENCRYPTION_ON', diff --git a/test/internal/node/tls.test.js b/test/internal/node/tls.test.js index c938b235f..ea77c75a0 100644 --- a/test/internal/node/tls.test.js +++ b/test/internal/node/tls.test.js @@ -20,104 +20,127 @@ import neo4j from '../../../src' import path from 'path' import sharedNeo4j from '../shared-neo4j' - -describe(' #integration trust-all-certificates', () => { - let driver - - afterEach(() => { - if (driver) { +import { + ServerVersion, + VERSION_4_0_0 +} from '../../../src/internal/server-version' + +describe('#integration trust', () => { + let serverVersion + + beforeAll(async () => { + const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + try { + serverVersion = await ServerVersion.fromDriver(driver) + } finally { driver.close() } }) - it('should work with default certificate', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: 'ENCRYPTION_ON', - trust: 'TRUST_ALL_CERTIFICATES' + beforeEach(() => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('address within security work') + } + }) + + describe('trust-all-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .then(result => { - expect(result.records[0].get(0).toNumber()).toBe(1) - done() + it('should work with default certificate', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: 'ENCRYPTION_ON', + trust: 'TRUST_ALL_CERTIFICATES' }) - }) -}) - -describe('#integration trust-custom-ca-signed-certificates', () => { - let driver - afterEach(() => { - if (driver) { - driver.close() - } + // When + driver + .session() + .run('RETURN 1') + .then(result => { + expect(result.records[0].get(0).toNumber()).toBe(1) + done() + }) + }) }) - it('should reject unknown certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', - trustedCertificates: ['test/resources/random.certificate'] + describe('trust-custom-ca-signed-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .catch(err => { - expect(err.message).toContain('Server certificate is not trusted') - done() + it('should reject unknown certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', + trustedCertificates: ['test/resources/random.certificate'] }) - }) - it('should accept known certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', - trustedCertificates: [neo4jCertPath()] + // When + driver + .session() + .run('RETURN 1') + .catch(err => { + expect(err.message).toContain('Server certificate is not trusted') + done() + }) }) - // When - driver - .session() - .run('RETURN 1') - .then(done) - }) -}) - -describe('#integration trust-system-ca-signed-certificates', () => { - let driver + it('should accept known certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES', + trustedCertificates: [neo4jCertPath()] + }) - afterEach(() => { - if (driver) { - driver.close() - } + // When + driver + .session() + .run('RETURN 1') + .then(done) + }) }) - it('should reject unknown certificates', done => { - // Given - driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { - encrypted: true, - trust: 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + describe('trust-system-ca-signed-certificates', () => { + let driver + + afterEach(() => { + if (driver) { + driver.close() + } }) - // When - driver - .session() - .run('RETURN 1') - .catch(err => { - expect(err.message).toContain('Server certificate is not trusted') - done() + it('should reject unknown certificates', done => { + // Given + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, { + encrypted: true, + trust: 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' }) + + // When + driver + .session() + .run('RETURN 1') + .catch(err => { + expect(err.message).toContain('Server certificate is not trusted') + done() + }) + }) }) -}) -function neo4jCertPath () { - return sharedNeo4j.neo4jCertPath(path.join('build', 'neo4j')) -} + function neo4jCertPath () { + return sharedNeo4j.neo4jCertPath(path.join('build', 'neo4j')) + } +}) diff --git a/test/rx/navigation.test.js b/test/rx/navigation.test.js index 33f15b8e8..80898df53 100644 --- a/test/rx/navigation.test.js +++ b/test/rx/navigation.test.js @@ -485,10 +485,13 @@ describe('#integration-rx navigation', () => { const result = runnable.run('THIS IS NOT A CYPHER') - await collectAndAssertError(result.keys(), error => { - expect(error.message).toContain('Invalid input') - expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') - }) + await collectAndAssertError( + result.keys(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) } /** @@ -496,19 +499,18 @@ describe('#integration-rx navigation', () => { * @param {RxSession|RxTransaction} runnable */ async function shouldFailOnSubsequentKeysWhenRunFails (version, runnable) { - function expectations (error) { - expect(error.message).toContain('Invalid input') - expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') - } if (version.compareTo(VERSION_4_0_0) < 0) { return } const result = runnable.run('THIS IS NOT A CYPHER') - - await collectAndAssertError(result.keys(), expectations) - await collectAndAssertError(result.keys(), expectations) - await collectAndAssertError(result.keys(), expectations) + const expectedError = jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + await collectAndAssertError(result.keys(), expectedError) + await collectAndAssertError(result.keys(), expectedError) + await collectAndAssertError(result.keys(), expectedError) } /** @@ -522,10 +524,13 @@ describe('#integration-rx navigation', () => { const result = runnable.run('THIS IS NOT A CYPHER') - await collectAndAssertError(result.records(), error => { - expect(error.message).toContain('Invalid input') - expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') - }) + await collectAndAssertError( + result.records(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) } /** @@ -539,10 +544,13 @@ describe('#integration-rx navigation', () => { const result = runnable.run('THIS IS NOT A CYPHER') - await collectAndAssertError(result.summary(), error => { - expect(error.message).toContain('Invalid input') - expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') - }) + await collectAndAssertError( + result.summary(), + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) + ) } /** @@ -550,19 +558,19 @@ describe('#integration-rx navigation', () => { * @param {RxSession|RxTransaction} runnable */ async function shouldFailOnSubsequentSummaryWhenRunFails (version, runnable) { - function expectations (error) { - expect(error.message).toContain('Invalid input') - expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') - } if (version.compareTo(VERSION_4_0_0) < 0) { return } const result = runnable.run('THIS IS NOT A CYPHER') + const expectedError = jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.SyntaxError', + message: jasmine.stringMatching(/Invalid input/) + }) - await collectAndAssertError(result.summary(), expectations) - await collectAndAssertError(result.summary(), expectations) - await collectAndAssertError(result.summary(), expectations) + await collectAndAssertError(result.summary(), expectedError) + await collectAndAssertError(result.summary(), expectedError) + await collectAndAssertError(result.summary(), expectedError) } async function collectAndAssertKeys (result) { @@ -628,15 +636,14 @@ describe('#integration-rx navigation', () => { * @param {Observable} stream * @param {function(err: Error): void} expectationFunc */ - async function collectAndAssertError (stream, expectationFunc) { - const error = await stream + async function collectAndAssertError (stream, expectedError) { + const result = await stream .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error).toBeTruthy() - expectationFunc(error) + expect(result).toEqual([Notification.createError(expectedError)]) } }) diff --git a/test/rx/session.test.js b/test/rx/session.test.js index 0430a5ac5..2b23f1dc5 100644 --- a/test/rx/session.test.js +++ b/test/rx/session.test.js @@ -20,7 +20,7 @@ import { Notification, throwError } from 'rxjs' import { map, materialize, toArray, concat } from 'rxjs/operators' import neo4j from '../../src' -import { ServerVersion } from '../../src/internal/server-version' +import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' import sharedNeo4j from '../internal/shared-neo4j' import { newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../../src/error' @@ -57,6 +57,10 @@ describe('#integration rx-session', () => { }) it('should be able to run a simple statement', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const result = await session .run('UNWIND [1,2,3,4] AS n RETURN n') .records() @@ -77,6 +81,10 @@ describe('#integration rx-session', () => { }) it('should be able to reuse session after failure', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const result1 = await session .run('INVALID STATEMENT') .records() @@ -105,6 +113,10 @@ describe('#integration rx-session', () => { }) it('should run transactions without retries', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const txcWork = new ConfigurableTransactionWork({ statement: 'CREATE (:WithoutRetry) RETURN 5' }) @@ -126,6 +138,10 @@ describe('#integration rx-session', () => { }) it('should run transaction with retries on reactive failures', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const txcWork = new ConfigurableTransactionWork({ statement: 'CREATE (:WithReactiveFailure) RETURN 7', reactiveFailures: [ @@ -152,6 +168,10 @@ describe('#integration rx-session', () => { }) it('should run transaction with retries on synchronous failures', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const txcWork = new ConfigurableTransactionWork({ statement: 'CREATE (:WithSyncFailure) RETURN 9', syncFailures: [ @@ -200,6 +220,10 @@ describe('#integration rx-session', () => { }) it('should fail even after a transient error', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const txcWork = new ConfigurableTransactionWork({ statement: 'CREATE (:Person) RETURN 1', syncFailures: [ diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js index 83d865b18..f8e6bb4ff 100644 --- a/test/rx/transaction.test.js +++ b/test/rx/transaction.test.js @@ -30,7 +30,6 @@ import { import neo4j from '../../src' import { ServerVersion, VERSION_4_0_0 } from '../../src/internal/server-version' import RxSession from '../../src/session-rx' -import RxTransaction from '../../src/transaction-rx' import sharedNeo4j from '../internal/shared-neo4j' import { newError } from '../../src/error' @@ -186,17 +185,22 @@ describe('#integration-rx transaction', () => { await verifyFailsWithWrongStatement(txc) - const error = await txc + const result = await txc .commit() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error).toBeTruthy() - expect(error.error).toContain( - 'Cannot commit statements in this transaction' - ) + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit statements in this transaction/ + ) + }) + ) + ]) }) it('should succeed to rollback after a failed statement', async () => { @@ -229,17 +233,22 @@ describe('#integration-rx transaction', () => { await verifyCanReturnOne(txc) await verifyFailsWithWrongStatement(txc) - const error = await txc + const result = await txc .commit() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error).toBeTruthy() - expect(error.error).toContain( - 'Cannot commit statements in this transaction' - ) + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit statements in this transaction/ + ) + }) + ) + ]) }) it('should succeed to rollback after successful and failed statement', async () => { @@ -272,18 +281,23 @@ describe('#integration-rx transaction', () => { await verifyFailsWithWrongStatement(txc) - const error = await txc + const result = await txc .run('CREATE ()') .records() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error).toBeTruthy() - expect(error.error).toContain( - 'Cannot run statement, because previous statements in the transaction has failed' - ) + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot run statement, because previous statements in the transaction has failed/ + ) + }) + ) + ]) }) it('should allow commit after commit', async () => { @@ -340,16 +354,22 @@ describe('#integration-rx transaction', () => { await verifyCanCreateNode(txc, 6) await verifyCanCommit(txc) - const error = await txc + const result = await txc .rollback() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error.error).toContain( - 'Cannot rollback transaction, because transaction has already been successfully closed' - ) + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot rollback transaction, because transaction has already been successfully closed/ + ) + }) + ) + ]) }) it('should fail to commit after rollback', async () => { @@ -362,16 +382,22 @@ describe('#integration-rx transaction', () => { await verifyCanCreateNode(txc, 6) await verifyCanRollback(txc) - const error = await txc + const result = await txc .commit() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error.error).toContain( - 'Cannot commit this transaction, because it has already been rolled back' - ) + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot commit this transaction, because it has already been rolled back/ + ) + }) + ) + ]) }) it('should fail to run statement after committed transaction', async () => { @@ -490,14 +516,18 @@ describe('#integration-rx transaction', () => { const txc = await session.beginTransaction().toPromise() const result = txc.run('RETURN Wrong') - const error = await result + const messages = await result .records() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error.message).toContain('Variable `Wrong` not defined') + expect(messages).toEqual([ + Notification.createError( + jasmine.stringMatching(/Variable `Wrong` not defined/) + ) + ]) const summary = await result.summary().toPromise() expect(summary).toBeTruthy() @@ -561,15 +591,23 @@ describe('#integration-rx transaction', () => { await verifyCanCreateNode(txc, 15) await verifyCanCommitOrRollback(txc, commit) - const error = await txc + const result = await txc .run('CREATE ()') .records() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - expect(error.error).toContain('Cannot run statement, because transaction') + expect(result).toEqual([ + Notification.createError( + jasmine.objectContaining({ + error: jasmine.stringMatching( + /Cannot run statement, because transaction/ + ) + }) + ) + ]) } async function verifyCanRunMultipleStatements (commit) { @@ -716,17 +754,19 @@ describe('#integration-rx transaction', () => { } async function verifyFailsWithWrongStatement (txc) { - const error = await txc + const result = await txc .run('RETURN') .records() .pipe( materialize(), - map(n => n.error) + toArray() ) .toPromise() - - expect(error).toBeTruthy() - expect(error.code).toContain('SyntaxError') + expect(result).toEqual([ + Notification.createError( + jasmine.stringMatching(/Unexpected end of input/) + ) + ]) } async function verifyCommittedOrRollbacked (commit) { diff --git a/test/session.test.js b/test/session.test.js index c05577004..b205db634 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -25,7 +25,7 @@ import SingleConnectionProvider from '../src/internal/connection-provider-single import FakeConnection from './internal/fake-connection' import sharedNeo4j from './internal/shared-neo4j' import _ from 'lodash' -import { ServerVersion } from '../src/internal/server-version' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' import { isString } from '../src/internal/util' import testUtils from './internal/test-utils' import { newError, PROTOCOL_ERROR, SESSION_EXPIRED } from '../src/error' @@ -307,6 +307,10 @@ describe('#integration session', () => { }) it('should expose cypher notifications ', done => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('seems to be flaky') + } + // Given const statement = 'EXPLAIN MATCH (n), (m) RETURN n, m' // When & Then diff --git a/test/summary.test.js b/test/summary.test.js index 8a0963947..e0427b419 100644 --- a/test/summary.test.js +++ b/test/summary.test.js @@ -19,15 +19,17 @@ import neo4j from '../src' import sharedNeo4j from './internal/shared-neo4j' +import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' describe('#integration result summary', () => { - let driver, session + let driver, session, serverVersion - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() - session.run('MATCH (n) DETACH DELETE n').then(done) + const result = await session.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) }) afterEach(() => { @@ -99,6 +101,10 @@ describe('#integration result summary', () => { }) it('should get notifications from summary', done => { + if (serverVersion.compareTo(VERSION_4_0_0) >= 0) { + pending('seems to be flaky') + } + session.run('EXPLAIN MATCH (n), (m) RETURN n, m').then(result => { let summary = result.summary expect(summary.notifications).toBeDefined() diff --git a/test/transaction.test.js b/test/transaction.test.js index 49d4e4649..7c8ea5042 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -382,10 +382,7 @@ describe('#integration transaction', () => { const tx2 = session2.beginTransaction() tx2.run('CREATE ()').catch(error => { const message = error.message - const expectedPrefix = - message.indexOf('Database not up to the requested version') === - 0 - expect(expectedPrefix).toBeTruthy() + expect(message).toContain('not up to the requested version') done() }) }) From 8ff27cb3722dda4624a7e23996b083b8f2d4baab Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Fri, 9 Aug 2019 16:02:42 +0100 Subject: [PATCH 06/14] Skip test on previous server versions --- test/rx/session.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/rx/session.test.js b/test/rx/session.test.js index 2b23f1dc5..10a2606a3 100644 --- a/test/rx/session.test.js +++ b/test/rx/session.test.js @@ -198,6 +198,10 @@ describe('#integration rx-session', () => { }) it('should fail on transactions that cannot be retried', async () => { + if (serverVersion.compareTo(VERSION_4_0_0) < 0) { + return + } + const txcWork = new ConfigurableTransactionWork({ statement: 'UNWIND [10, 5, 0] AS x CREATE (:Hi) RETURN 10/x' }) From 94fdad0b3473261441bcdc92ad7bfd96637d6da9 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Mon, 12 Aug 2019 13:57:25 +0100 Subject: [PATCH 07/14] Improve documentation --- src/docs.js | 37 +++++++ src/driver.js | 137 +++++++++++++++--------- src/graph-types.js | 88 ++++++++++++++++ src/index.js | 2 +- src/integer.js | 4 +- src/internal/bolt-protocol-v1.js | 20 ++-- src/internal/bookmark.js | 2 +- src/internal/channel-config.js | 2 +- src/internal/connection-channel.js | 6 +- src/internal/connection.js | 2 +- src/internal/logger.js | 6 +- src/internal/node/node-channel.js | 4 +- src/internal/pool.js | 2 +- src/internal/request-message.js | 12 +-- src/internal/routing-table.js | 2 +- src/internal/tx-config.js | 2 +- src/internal/url-util.js | 2 +- src/internal/util.js | 2 +- src/record.js | 10 +- src/result-rx.js | 35 +++++- src/result.js | 35 +++++- src/routing-driver.js | 2 +- src/session-rx.js | 126 +++++++++++++++------- src/session.js | 24 +---- src/spatial-types.js | 23 +++- src/temporal-types.js | 164 +++++++++++++++++++++++++++-- src/transaction-rx.js | 34 +++++- src/transaction.js | 2 +- 28 files changed, 624 insertions(+), 163 deletions(-) create mode 100644 src/docs.js diff --git a/src/docs.js b/src/docs.js new file mode 100644 index 000000000..2c1071bdf --- /dev/null +++ b/src/docs.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2002-2019 "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. + */ + +/** + * Configuration object containing settings for explicit and auto-commit transactions. + *

+ * Configuration is supported for: + *

    + *
  • queries executed in auto-commit transactions using {@link Session#run} and {@link RxSession#run}
  • + *
  • transactions started by transaction functions using {@link Session#readTransaction}, {@link RxSession#readTransaction}, + * {@link Session#writeTransaction} and {@link RxSession#writeTransaction}
  • + *
  • explicit transactions using {@link Session#beginTransaction} and {@link RxSession#beginTransaction}
  • + *
+ * @typedef {Object} TransactionConfig + * @property {number} timeout - the transaction timeout in **milliseconds**. Transactions that execute longer than the configured timeout will + * be terminated by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default timeout + * configured in the database using `dbms.transaction.timeout` setting. Value should not represent a duration of zero or negative duration. + * @property {Object} metadata - the transaction metadata. Specified metadata will be attached to the executing transaction and visible in the output of + * `dbms.listQueries` and `dbms.listTransactions` procedures. It will also get logged to the `query.log`. This functionality makes it easier to tag + * transactions and is equivalent to `dbms.setTXMetaData` procedure. + */ diff --git a/src/driver.js b/src/driver.js index 2f61a544c..044809457 100644 --- a/src/driver.js +++ b/src/driver.js @@ -63,11 +63,11 @@ class Driver { /** * You should not be calling this directly, instead use {@link driver}. * @constructor + * @protected * @param {ServerAddress} address * @param {string} userAgent - * @param {object} authToken - * @param {object} config - * @protected + * @param {Object} authToken + * @param {Object} config */ constructor (address, userAgent, authToken = {}, config = {}) { sanitizeConfig(config) @@ -89,19 +89,13 @@ class Driver { this._afterConstruction() } - /** - * @protected - */ - _afterConstruction () { - this._log.info( - `Direct driver ${this._id} created for server address ${this._address}` - ) - } - /** * Verifies connectivity of this driver by trying to open a connection with the provided driver options. - * @param {string} [database=''] the target database to verify connectivity for. - * @returns {Promise} promise resolved with server info or rejected with error. + * + * @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 = '' } = {}) { const connectionProvider = this._getOrCreateConnectionProvider() @@ -120,11 +114,12 @@ class Driver { * it is closed, the underlying connection will be released to the connection * pool and made available for others to use. * - * @param {Object} args - - * @param {string} args.defaultAccessMode='WRITE' - the access mode of this session, allowed values are {@link READ} and {@link WRITE}. - * @param {string|string[]} args.bookmarks - the initial reference or references to some previous + * @public + * @param {Object} param - The object parameter + * @param {string} param.defaultAccessMode=WRITE - the access mode of this session, allowed values are {@link READ} and {@link WRITE}. + * @param {string|string[]} param.bookmarks - 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. - * @param {string} args.database='' - the database this session will connect to. + * @param {string} param.database - the database this session will operate on. * @return {Session} new session. */ session ({ @@ -140,6 +135,25 @@ class Driver { }) } + /** + * Acquire a reactive 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 {Object} param + * @param {string} param.defaultAccessMode=WRITE - the access mode of this session, allowed values are {@link READ} and {@link WRITE} + * @param {string|string[]} param.bookmarks - the initial reference or references to some previous transactions. Value is optional and + * absence indicates that the bookmarks do not exist or are unknown. + * @param {string} param.database - the database this session will operate on. + * @returns {RxSession} new reactive session. + */ rxSession ({ defaultAccessMode = WRITE, bookmarks, database = '' } = {}) { return new RxSession({ session: this._newSession({ @@ -152,22 +166,44 @@ class Driver { }) } - _newSession ({ defaultAccessMode, bookmarkOrBookmarks, database, reactive }) { - const sessionMode = Driver._validateSessionMode(defaultAccessMode) - const connectionProvider = this._getOrCreateConnectionProvider() - const bookmark = bookmarkOrBookmarks - ? new Bookmark(bookmarkOrBookmarks) - : Bookmark.empty() - return new Session({ - mode: sessionMode, - database, - connectionProvider, - bookmark, + /** + * Close all open sessions and other associated resources. You should + * make sure to use this when you are done with this driver instance. + * @public + */ + close () { + this._log.info(`Driver ${this._id} closing`) + if (this._connectionProvider) { + this._connectionProvider.close() + } + } + + /** + * @protected + */ + _afterConstruction () { + this._log.info( + `Direct driver ${this._id} created for server address ${this._address}` + ) + } + + /** + * @protected + */ + _createConnectionProvider (address, userAgent, authToken) { + return new DirectConnectionProvider({ + id: this._id, config: this._config, - reactive + log: this._log, + address: address, + userAgent: userAgent, + authToken: authToken }) } + /** + * @protected + */ static _validateSessionMode (rawMode) { const mode = rawMode || WRITE if (mode !== ACCESS_MODE_READ && mode !== ACCESS_MODE_WRITE) { @@ -176,18 +212,28 @@ class Driver { return mode } - // Extension point - _createConnectionProvider (address, userAgent, authToken) { - return new DirectConnectionProvider({ - id: this._id, + /** + * @private + */ + _newSession ({ defaultAccessMode, bookmarkOrBookmarks, database, reactive }) { + const sessionMode = Driver._validateSessionMode(defaultAccessMode) + const connectionProvider = this._getOrCreateConnectionProvider() + const bookmark = bookmarkOrBookmarks + ? new Bookmark(bookmarkOrBookmarks) + : Bookmark.empty() + return new Session({ + mode: sessionMode, + database, + connectionProvider, + bookmark, config: this._config, - log: this._log, - address: address, - userAgent: userAgent, - authToken: authToken + reactive }) } + /** + * @private + */ _getOrCreateConnectionProvider () { if (!this._connectionProvider) { this._connectionProvider = this._createConnectionProvider( @@ -199,18 +245,6 @@ class Driver { return this._connectionProvider } - - /** - * Close all open sessions and other associated resources. You should - * make sure to use this when you are done with this driver instance. - * @return undefined - */ - close () { - this._log.info(`Driver ${this._id} closing`) - if (this._connectionProvider) { - this._connectionProvider.close() - } - } } /** @@ -231,6 +265,9 @@ function sanitizeConfig (config) { ) } +/** + * @private + */ function sanitizeIntValue (rawValue, defaultWhenAbsent) { const sanitizedValue = parseInt(rawValue, 10) if (sanitizedValue > 0 || sanitizedValue === 0) { diff --git a/src/graph-types.js b/src/graph-types.js index 0a355c240..a87bec704 100644 --- a/src/graph-types.js +++ b/src/graph-types.js @@ -23,16 +23,32 @@ class Node { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {Array} labels - Array for all labels * @param {Object} properties - Map with node properties */ constructor (identity, labels, properties) { + /** + * Identity of the node. + * @type {Integer} + */ this.identity = identity + /** + * Labels of the node. + * @type {string[]} + */ this.labels = labels + /** + * Properties of the node. + * @type {Object} + */ this.properties = properties } + /** + * @ignore + */ toString () { let s = '(' + this.identity for (let i = 0; i < this.labels.length; i++) { @@ -58,6 +74,7 @@ class Node { class Relationship { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {Integer} start - Identity of start Node * @param {Integer} end - Identity of end Node @@ -65,13 +82,36 @@ class Relationship { * @param {Object} properties - Map with relationship properties */ constructor (identity, start, end, type, properties) { + /** + * Identity of the relationship. + * @type {Integer} + */ this.identity = identity + /** + * Identity of the start node. + * @type {Integer} + */ this.start = start + /** + * Identity of the end node. + * @type {Integer} + */ this.end = end + /** + * Type of the relationship. + * @type {string} + */ this.type = type + /** + * Properties of the relationship. + * @type {Object} + */ this.properties = properties } + /** + * @ignore + */ toString () { let s = '(' + this.start + ')-[:' + this.type let keys = Object.keys(this.properties) @@ -95,18 +135,33 @@ class Relationship { class UnboundRelationship { /** * @constructor + * @protected * @param {Integer} identity - Unique identity * @param {string} type - Relationship type * @param {Object} properties - Map with relationship properties */ constructor (identity, type, properties) { + /** + * Identity of the relationship. + * @type {Integer} + */ this.identity = identity + /** + * Type of the relationship. + * @type {string} + */ this.type = type + /** + * Properties of the relationship. + * @type {Object} + */ this.properties = properties } /** * Bind relationship + * + * @protected * @param {Integer} start - Identity of start node * @param {Integer} end - Identity of end node * @return {Relationship} - Created relationship @@ -121,6 +176,9 @@ class UnboundRelationship { ) } + /** + * @ignore + */ toString () { let s = '-[:' + this.type let keys = Object.keys(this.properties) @@ -143,13 +201,26 @@ class UnboundRelationship { class PathSegment { /** * @constructor + * @protected * @param {Node} start - start node * @param {Relationship} rel - relationship that connects start and end node * @param {Node} end - end node */ constructor (start, rel, end) { + /** + * Start node. + * @type {Node} + */ this.start = start + /** + * Relationship. + * @type {Relationship} + */ this.relationship = rel + /** + * End node. + * @type {Node} + */ this.end = end } } @@ -160,14 +231,31 @@ class PathSegment { class Path { /** * @constructor + * @protected * @param {Node} start - start node * @param {Node} end - end node * @param {Array} segments - Array of Segments */ constructor (start, end, segments) { + /** + * 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 } } diff --git a/src/index.js b/src/index.js index de81a1cb0..f949750ae 100644 --- a/src/index.js +++ b/src/index.js @@ -232,7 +232,7 @@ const logging = { * } * * @param {string} url The URL for the Neo4j database, for instance "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. * @param {Object} config Configuration object. See the configuration section above for details. * @returns {Driver} */ diff --git a/src/integer.js b/src/integer.js index ccbae3223..c4ee7edc0 100644 --- a/src/integer.js +++ b/src/integer.js @@ -854,7 +854,7 @@ Integer.toNumber = function (val) { * @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} + * @returns {string} * @expose */ Integer.toString = function (val, radix) { @@ -1007,7 +1007,7 @@ let toNumber = Integer.toNumber * @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 + * @return {string} - returns a string representation of the integer */ let toString = Integer.toString diff --git a/src/internal/bolt-protocol-v1.js b/src/internal/bolt-protocol-v1.js index 0bff631b1..6b2d3de89 100644 --- a/src/internal/bolt-protocol-v1.js +++ b/src/internal/bolt-protocol-v1.js @@ -66,8 +66,8 @@ export default class BoltProtocol { /** * Transform metadata received in SUCCESS message before it is passed to the handler. - * @param {object} metadata the received metadata. - * @return {object} transformed metadata. + * @param {Object} metadata the received metadata. + * @return {Object} transformed metadata. */ transformMetadata (metadata) { return metadata @@ -75,9 +75,9 @@ export default class BoltProtocol { /** * Perform initialization and authentication of the underlying connection. - * @param {object} param + * @param {Object} param * @param {string} param.userAgent the user agent. - * @param {object} param.authToken the authentication token. + * @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. @@ -107,7 +107,7 @@ export default class BoltProtocol { /** * Begin an explicit transaction. - * @param {object} param + * @param {Object} param * @param {Bookmark} param.bookmark the bookmark. * @param {TxConfig} param.txConfig the configuration. * @param {string} param.database the target database name. @@ -147,7 +147,7 @@ export default class BoltProtocol { /** * Commit the explicit transaction. - * @param {object} param + * @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. @@ -179,7 +179,7 @@ export default class BoltProtocol { /** * Rollback the explicit transaction. - * @param {object} param + * @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. @@ -212,8 +212,8 @@ export default class BoltProtocol { /** * Send a Cypher statement through the underlying connection. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. - * @param {object} param + * @param {Object} parameters the statement parameters. + * @param {Object} param * @param {Bookmark} param.bookmark the bookmark. * @param {TxConfig} param.txConfig the transaction configuration. * @param {string} param.database the target database name. @@ -271,7 +271,7 @@ export default class BoltProtocol { /** * Send a RESET through the underlying connection. - * @param {object} param + * @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. diff --git a/src/internal/bookmark.js b/src/internal/bookmark.js index 94de52fa1..5d6bdc672 100644 --- a/src/internal/bookmark.js +++ b/src/internal/bookmark.js @@ -65,7 +65,7 @@ export default class Bookmark { /** * Get this bookmark as an object for begin transaction call. - * @return {object} the value of this bookmark as object. + * @return {Object} the value of this bookmark as object. */ asBeginTransactionParameters () { if (this.isEmpty()) { diff --git a/src/internal/channel-config.js b/src/internal/channel-config.js index 8436da7c5..598e8707b 100644 --- a/src/internal/channel-config.js +++ b/src/internal/channel-config.js @@ -43,7 +43,7 @@ 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 {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) { diff --git a/src/internal/connection-channel.js b/src/internal/connection-channel.js index 829b9a5d8..64d6c8da6 100644 --- a/src/internal/connection-channel.js +++ b/src/internal/connection-channel.js @@ -96,7 +96,7 @@ export default class ChannelConnection extends Connection { /** * 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 {Object} config - the driver configuration. * @param {ConnectionErrorHandler} errorHandler - the error handler for connection errors. * @param {Logger} log - configured logger. * @return {Connection} - new connection. @@ -131,7 +131,7 @@ export default class ChannelConnection extends Connection { /** * 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. + * @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) { @@ -199,7 +199,7 @@ export default class ChannelConnection extends Connection { /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. - * @param {object} authToken the object containing auth information. + * @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) { diff --git a/src/internal/connection.js b/src/internal/connection.js index f6baa4470..64e510c5f 100644 --- a/src/internal/connection.js +++ b/src/internal/connection.js @@ -79,7 +79,7 @@ export default class Connection { /** * 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. + * @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) { diff --git a/src/internal/logger.js b/src/internal/logger.js index be6b47d49..1bd56ee6c 100644 --- a/src/internal/logger.js +++ b/src/internal/logger.js @@ -48,7 +48,7 @@ class Logger { /** * Create a new logger based on the given driver configuration. - * @param {object} driverConfig the driver configuration as supplied by the user. + * @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) { @@ -186,7 +186,7 @@ function isLevelEnabled (configuredLevel, targetLevel) { /** * Extract the configured logging level from the driver's logging configuration. - * @param {object} loggingConfig the logging configuration. + * @param {Object} loggingConfig the logging configuration. * @return {string} the configured log level or default when none configured. */ function extractConfiguredLevel (loggingConfig) { @@ -207,7 +207,7 @@ function extractConfiguredLevel (loggingConfig) { /** * Extract the configured logger function from the driver's logging configuration. - * @param {object} loggingConfig the logging configuration. + * @param {Object} loggingConfig the logging configuration. * @return {function(level: string, message: string)} the configured logging function. */ function extractConfiguredLogger (loggingConfig) { diff --git a/src/internal/node/node-channel.js b/src/internal/node/node-channel.js index 62782031e..1511b7261 100644 --- a/src/internal/node/node-channel.js +++ b/src/internal/node/node-channel.js @@ -197,7 +197,7 @@ function trustStrategyName (config) { * 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. + * @return {Object} a new options object. */ function newTlsOptions (hostname, ca = undefined) { return { @@ -290,7 +290,7 @@ export default class NodeChannel { /** * Setup connection timeout on the socket, if configured. * @param {ChannelConfig} config - configuration of this channel. - * @param {object} socket - `net.Socket` or `tls.TLSSocket` object. + * @param {Object} socket - `net.Socket` or `tls.TLSSocket` object. * @private */ _setupConnectionTimeout (config, socket) { diff --git a/src/internal/pool.js b/src/internal/pool.js index 4a862f0ec..73effd895 100644 --- a/src/internal/pool.js +++ b/src/internal/pool.js @@ -62,7 +62,7 @@ class Pool { /** * Acquire and idle resource fom the pool or create a new one. * @param {ServerAddress} address the address for which we're acquiring. - * @return {object} resource that is ready to use. + * @return {Object} resource that is ready to use. */ acquire (address) { return this._acquire(address).then(resource => { diff --git a/src/internal/request-message.js b/src/internal/request-message.js index 5e687b687..e310e156c 100644 --- a/src/internal/request-message.js +++ b/src/internal/request-message.js @@ -55,7 +55,7 @@ export default class RequestMessage { /** * Create a new INIT message. * @param {string} clientName the client name. - * @param {object} authToken the authentication token. + * @param {Object} authToken the authentication token. * @return {RequestMessage} new INIT message. */ static init (clientName, authToken) { @@ -69,7 +69,7 @@ export default class RequestMessage { /** * Create a new RUN message. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. + * @param {Object} parameters the statement parameters. * @return {RequestMessage} new RUN message. */ static run (statement, parameters) { @@ -99,7 +99,7 @@ export default class RequestMessage { /** * Create a new HELLO message. * @param {string} userAgent the user agent. - * @param {object} authToken the authentication token. + * @param {Object} authToken the authentication token. * @return {RequestMessage} new HELLO message. */ static hello (userAgent, authToken) { @@ -147,7 +147,7 @@ export default class RequestMessage { /** * Create a new RUN message with additional metadata. * @param {string} statement the cypher statement. - * @param {object} parameters the statement parameters. + * @param {Object} parameters the statement parameters. * @param {Bookmark} bookmark the bookmark. * @param {TxConfig} txConfig the configuration. * @param {string} database the database name. @@ -215,7 +215,7 @@ export default class RequestMessage { * @param {TxConfig} txConfig the configuration. * @param {string} database the database name. * @param {string} mode the access mode. - * @return {object} a metadata object. + * @return {Object} a metadata object. */ function buildTxMetadata (bookmark, txConfig, database, mode) { const metadata = {} @@ -241,7 +241,7 @@ function buildTxMetadata (bookmark, txConfig, database, mode) { * Create an object that represents streaming metadata. * @param {Integer|number} stmtId The statement id to stream its results. * @param {Integer|number} n The number of records to stream. - * @returns {object} a metadata object. + * @returns {Object} a metadata object. */ function buildStreamMetadata (stmtId, n) { const metadata = { n: int(n) } diff --git a/src/internal/routing-table.js b/src/internal/routing-table.js index 9516b8703..ce12ec6f5 100644 --- a/src/internal/routing-table.js +++ b/src/internal/routing-table.js @@ -81,7 +81,7 @@ export default class RoutingTable { /** * Remove all occurrences of the element in the array. * @param {Array} array the array to filter. - * @param {object} element the element to remove. + * @param {Object} element the element to remove. * @return {Array} new filtered array. */ function removeFromArray (array, element) { diff --git a/src/internal/tx-config.js b/src/internal/tx-config.js index c505e7b30..0f5b5ea90 100644 --- a/src/internal/tx-config.js +++ b/src/internal/tx-config.js @@ -30,7 +30,7 @@ import { newError } from '../error' export default class TxConfig { /** * @constructor - * @param {object} config the raw configuration object. + * @param {Object} config the raw configuration object. */ constructor (config) { assertValidConfig(config) diff --git a/src/internal/url-util.js b/src/internal/url-util.js index f2d89d933..a9bbff8a3 100644 --- a/src/internal/url-util.js +++ b/src/internal/url-util.js @@ -58,7 +58,7 @@ class Url { /** * Nonnull object representing parsed query string key-value pairs. Duplicated keys not supported. * Example: '{}', '{'key1': 'value1', 'key2': 'value2'}', etc. - * @type {object} + * @type {Object} */ this.query = query } diff --git a/src/internal/util.js b/src/internal/util.js index f08ce7a97..a55cb7d2a 100644 --- a/src/internal/util.js +++ b/src/internal/util.js @@ -47,7 +47,7 @@ function isObject (obj) { /** * Check and normalize given statement and parameters. * @param {string|{text: string, parameters: object}} statement the statement to check. - * @param {object} parameters + * @param {Object} parameters * @return {{query: string, params: object}} the normalized query with parameters. * @throws TypeError when either given query or parameters are invalid. */ diff --git a/src/record.js b/src/record.js index 29bd7f9de..0fc9fffba 100644 --- a/src/record.js +++ b/src/record.js @@ -50,7 +50,7 @@ class Record { /** * Create a new record object. * @constructor - * @access private + * @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 @@ -58,7 +58,15 @@ class Record { * generated. */ constructor (keys, fields, fieldLookup = null) { + /** + * 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) diff --git a/src/result-rx.js b/src/result-rx.js index 03e0d2970..bb5f3b87c 100644 --- a/src/result-rx.js +++ b/src/result-rx.js @@ -20,6 +20,7 @@ import { newError } from './error' import ResultSummary from './result-summary' import { Observable, Subject, ReplaySubject, from } from 'rxjs' import { flatMap, publishReplay, refCount, shareReplay } from 'rxjs/operators' +import Record from './record' const States = { READY: 0, @@ -27,10 +28,14 @@ const States = { COMPLETED: 2 } +/** + * The reactive result interface. + */ export default class RxResult { /** - * - * @param {Observable} result + * @constructor + * @protected + * @param {Observable} result - An observable of single Result instance to relay requests. */ constructor (result) { const replayedResult = result.pipe( @@ -49,10 +54,28 @@ export default class RxResult { this._state = States.READY } + /** + * Returns an observable that exposes a single item containing field names + * returned by the executing statement. + * + * Errors raised by actual statement execution can surface on the returned + * observable stream. + * + * @public + * @returns {Observable} - An observable stream (with exactly one element) of field names. + */ keys () { return this._keys } + /** + * Returns an observable that exposes each record returned by the executing statement. + * + * Errors raised during the streaming phase can surface on the returned observable stream. + * + * @public + * @returns {Observable} - An observable stream of records. + */ records () { return this._result.pipe( flatMap( @@ -65,7 +88,13 @@ export default class RxResult { } /** - * return {Observable} + * Returns an observable that exposes a single item of {@link ResultSummary} that is generated by + * the server after the streaming of the executing statement is completed. + * + * *Subscribing to this stream before subscribing to records() stream causes the results to be discarded on the server.* + * + * @public + * @returns {Observable} - An observable stream (with exactly one element) of result summary. */ summary () { return this._result.pipe( diff --git a/src/result.js b/src/result.js index 0f3a55abc..47881796f 100644 --- a/src/result.js +++ b/src/result.js @@ -53,6 +53,15 @@ class Result { this._connectionHolder = connectionHolder || EMPTY_CONNECTION_HOLDER } + /** + * 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 () { return new Promise((resolve, reject) => { this._streamObserverPromise.then(observer => @@ -64,6 +73,15 @@ class Result { }) } + /** + * Returns a promise for the result summary. + * + * *Should not be combined with {@link Result#subscribe} function.* + * + * @public + * @returns {Promise} - Result summary. + * + */ summary () { return new Promise((resolve, reject) => { this._streamObserverPromise.then(o => @@ -77,8 +95,9 @@ class Result { /** * Create and return new Promise + * + * @private * @return {Promise} new Promise. - * @access private */ _getOrCreatePromise () { if (!this._p) { @@ -104,7 +123,8 @@ class Result { /** * Waits for all results and calls the passed in function with the results. - * Cannot be combined with the {@link Result#subscribe} function. + * + * *Should not be combined with {@link Result#subscribe} function.* * * @param {function(result: {records:Array, summary: ResultSummary})} onFulfilled - function to be called * when finished. @@ -117,7 +137,9 @@ class Result { /** * Catch errors when using promises. - * Cannot be used with the subscribe function. + * + * *Should not be combined with {@link Result#subscribe} function.* + * * @param {function(error: Neo4jError)} onRejected - Function to be called upon errors. * @return {Promise} promise. */ @@ -130,6 +152,7 @@ class Result { * 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. @@ -163,6 +186,12 @@ class Result { this._streamObserverPromise.then(o => o.subscribe(observer)) } + /** + * Signals the stream observer that the future records should be discarded on the server. + * + * @protected + * @since 4.0.0 + */ _discard () { this._streamObserverPromise.then(o => o.discard()) } diff --git a/src/routing-driver.js b/src/routing-driver.js index a822637e8..d5f68f4d7 100644 --- a/src/routing-driver.js +++ b/src/routing-driver.js @@ -65,7 +65,7 @@ function createHostNameResolver (config) { /** * @private - * @returns {object} the given config. + * @returns {Object} the given config. */ function validateConfig (config) { const resolver = config.resolver diff --git a/src/session-rx.js b/src/session-rx.js index 0597a83f0..b1279ad19 100644 --- a/src/session-rx.js +++ b/src/session-rx.js @@ -17,8 +17,7 @@ * limitations under the License. */ import { defer, Observable, throwError } from 'rxjs' -import { map, flatMap, catchError, concat } from 'rxjs/operators' -import { newError } from './error' +import { flatMap, catchError, concat } from 'rxjs/operators' import RxResult from './result-rx' import Session from './session' import RxTransaction from './transaction-rx' @@ -26,15 +25,32 @@ import { ACCESS_MODE_READ, ACCESS_MODE_WRITE } from './internal/constants' import TxConfig from './internal/tx-config' import RxRetryLogic from './internal/retry-logic-rx' +/** + * A Reactive session, which provides the same functionality as {@link Session} but through a Reactive API. + */ export default class RxSession { /** - * @param {Session} session + * Constructs a reactive session with given default session instance and provided driver configuration. + * + * @protected + * @param {Object} param - Object parameter + * @param {Session} param.session - The underlying session instance to relay requests */ constructor ({ session, config } = {}) { this._session = session this._retryLogic = _createRetryLogic(config) } + /** + * Creates a reactive result that will execute the statement with the provided parameters and the provided + * transaction configuration that applies to the underlying auto-commit transaction. + * + * @public + * @param {string} statement - Statement to be executed. + * @param {Object} parameters - Parameter values to use in statement execution. + * @param {TransactionConfig} transactionConfig - Configuration for the new auto-commit transaction. + * @returns {RxResult} - A reactive result + */ run (statement, parameters, transactionConfig) { return new RxResult( new Observable(observer => { @@ -52,10 +68,76 @@ export default class RxSession { ) } + /** + * Starts a new explicit transaction with the provided transaction configuration. + * + * @public + * @param {TransactionConfig} transactionConfig - Configuration for the new transaction. + * @returns {Observable} - A reactive stream that will generate at most **one** RxTransaction instance. + */ beginTransaction (transactionConfig) { return this._beginTransaction(this._session._mode, transactionConfig) } + /** + * Executes the provided unit of work in a {@link READ} reactive transaction which is created with the provided + * transaction configuration. + * @public + * @param {function(txc: RxTransaction): Observable} work - A unit of work to be executed. + * @param {TransactionConfig} transactionConfig - Configuration for the enclosing transaction created by the driver. + * @returns {Observable} - A reactive stream returned by the unit of work. + */ + readTransaction (work, transactionConfig) { + return this._runTransaction(ACCESS_MODE_READ, work, transactionConfig) + } + + /** + * Executes the provided unit of work in a {@link WRITE} reactive transaction which is created with the provided + * transaction configuration. + * @public + * @param {function(txc: RxTransaction): Observable} work - A unit of work to be executed. + * @param {TransactionConfig} transactionConfig - Configuration for the enclosing transaction created by the driver. + * @returns {Observable} - A reactive stream returned by the unit of work. + */ + writeTransaction (work, transactionConfig) { + return this._runTransaction(ACCESS_MODE_WRITE, work, transactionConfig) + } + + /** + * Closes this reactive session. + * + * @public + * @returns {Observable} - An empty reactive stream + */ + close () { + return new Observable(observer => { + this._session + .close() + .then(() => { + observer.complete() + }) + .catch(err => observer.error(err)) + }) + } + + /** + * Returns the bookmark received following the last successfully completed statement, which is executed + * either in an {@link RxTransaction} obtained from this session instance or directly through one of + * the {@link RxSession#run} method of this session instance. + * + * If no bookmark was received or if this transaction was rolled back, the bookmark value will not be + * changed. + * + * @public + * @returns {string} + */ + lastBookmark () { + return this._session.lastBookmark() + } + + /** + * @private + */ _beginTransaction (accessMode, transactionConfig) { let txConfig = TxConfig.empty() if (transactionConfig) { @@ -78,23 +160,10 @@ export default class RxSession { }) } - readTransaction (transactionWork, transactionConfig) { - return this._runTransaction( - ACCESS_MODE_READ, - transactionWork, - transactionConfig - ) - } - - writeTransaction (transactionWork, transactionConfig) { - return this._runTransaction( - ACCESS_MODE_WRITE, - transactionWork, - transactionConfig - ) - } - - _runTransaction (accessMode, transactionWork, transactionConfig) { + /** + * @private + */ + _runTransaction (accessMode, work, transactionConfig) { let txConfig = TxConfig.empty() if (transactionConfig) { txConfig = new TxConfig(transactionConfig) @@ -105,7 +174,7 @@ export default class RxSession { flatMap(txc => defer(() => { try { - return transactionWork(txc) + return work(txc) } catch (err) { return throwError(err) } @@ -117,21 +186,6 @@ export default class RxSession { ) ) } - - close () { - return new Observable(observer => { - this._session - .close() - .then(() => { - observer.complete() - }) - .catch(err => observer.error(err)) - }) - } - - lastBookmark () { - return this._session.lastBookmark() - } } function _createRetryLogic (config) { diff --git a/src/session.js b/src/session.js index a5b4b0ecf..32bd94e52 100644 --- a/src/session.js +++ b/src/session.js @@ -31,25 +31,6 @@ import TransactionExecutor from './internal/transaction-executor' import Bookmark from './internal/bookmark' import TxConfig from './internal/tx-config' -// Typedef for JSDoc. Declares TransactionConfig type and makes it possible to use in in method-level docs. -/** - * Configuration object containing settings for explicit and auto-commit transactions. - *

- * Configuration is supported for: - *

    - *
  • queries executed in auto-commit transactions using {@link Session#run}
  • - *
  • transactions started by transaction functions using {@link Session#readTransaction} and {@link Session#writeTransaction}
  • - *
  • explicit transactions using {@link Session#beginTransaction}
  • - *
- * @typedef {object} TransactionConfig - * @property {number} timeout - the transaction timeout in **milliseconds**. Transactions that execute longer than the configured timeout will - * be terminated by the database. This functionality allows to limit query/transaction execution time. Specified timeout overrides the default timeout - * configured in the database using `dbms.transaction.timeout` setting. Value should not represent a duration of zero or negative duration. - * @property {object} metadata - the transaction metadata. Specified metadata will be attached to the executing transaction and visible in the output of - * `dbms.listQueries` and `dbms.listTransactions` procedures. It will also get logged to the `query.log`. This functionality makes it easier to tag - * transactions and is equivalent to `dbms.setTXMetaData` procedure. - */ - /** * A Session instance is used for handling the connection and * sending statements through the connection. @@ -60,6 +41,7 @@ import TxConfig from './internal/tx-config' class Session { /** * @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. @@ -100,6 +82,8 @@ class Session { * Run Cypher statement * Could be called with a statement object i.e.: `{text: "MATCH ...", prameters: {param: 1}}` * or with the statement and parameters as separate arguments. + * + * @public * @param {mixed} statement - Cypher statement to execute * @param {Object} parameters - Map with parameters to use in statement * @param {TransactionConfig} [transactionConfig] - configuration for the new auto-commit transaction. @@ -266,7 +250,7 @@ class Session { /** * Close this session. - * @return ${Promise} + * @return {Promise} */ async close () { if (this._open) { diff --git a/src/spatial-types.js b/src/spatial-types.js index af0654fc6..60ce04fd4 100644 --- a/src/spatial-types.js +++ b/src/spatial-types.js @@ -30,16 +30,35 @@ export class Point { * @param {Integer|number} 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 `y` coordinate of the point or `undefined` if point has 2 dimensions. + * @param {number} [z=undefined] the `z` coordinate of the point or `undefined` if point has 2 dimensions. */ constructor (srid, x, y, z) { + /** + * The coordinate reference system identifier. + * @type {Integer|Number} + */ this.srid = assertNumberOrInteger(srid, 'SRID') + /** + * 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 () { return this.z || this.z === 0 ? `Point{srid=${formatAsFloat(this.srid)}, x=${formatAsFloat( @@ -63,7 +82,7 @@ Object.defineProperty(Point.prototype, POINT_IDENTIFIER_PROPERTY, { /** * Test if given object is an instance of {@link Point} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Point}, `false` otherwise. */ export function isPoint (obj) { diff --git a/src/temporal-types.js b/src/temporal-types.js index 467dbd9c9..f9d68a3a4 100644 --- a/src/temporal-types.js +++ b/src/temporal-types.js @@ -51,15 +51,34 @@ export class Duration { * @param {Integer|number} nanoseconds the number of nanoseconds for the new duration. */ constructor (months, days, seconds, nanoseconds) { + /** + * The number of months. + * @type {Integer|number} + */ this.months = assertNumberOrInteger(months, 'Months') + /** + * The number of days. + * @type {Integer|number} + */ this.days = assertNumberOrInteger(days, 'Days') assertNumberOrInteger(seconds, 'Seconds') assertNumberOrInteger(nanoseconds, 'Nanoseconds') + /** + * The number of seconds. + * @type {Integer|number} + */ this.seconds = util.normalizeSecondsForDuration(seconds, nanoseconds) + /** + * The number of nanoseconds. + * @type {Integer|number} + */ this.nanoseconds = util.normalizeNanosecondsForDuration(nanoseconds) Object.freeze(this) } + /** + * @ignore + */ toString () { return util.durationToIsoString( this.months, @@ -78,7 +97,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Duration} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Duration}, `false` otherwise. */ export function isDuration (obj) { @@ -98,9 +117,25 @@ export class LocalTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor (hour, minute, second, nanosecond) { + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) Object.freeze(this) } @@ -123,6 +158,9 @@ export class LocalTime { ) } + /** + * @ignore + */ toString () { return util.timeToIsoString( this.hour, @@ -141,7 +179,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link LocalTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link LocalTime}, `false` otherwise. */ export function isLocalTime (obj) { @@ -163,10 +201,30 @@ export class Time { * This is different from standard JavaScript `Date.getTimezoneOffset()` which is the difference, in minutes, from local time to UTC. */ constructor (hour, minute, second, nanosecond, timeZoneOffsetSeconds) { + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) + /** + * The time zone offset in seconds. + * @type {Integer|number} + */ this.timeZoneOffsetSeconds = assertNumberOrInteger( timeZoneOffsetSeconds, 'Time zone offset in seconds' @@ -193,6 +251,9 @@ export class Time { ) } + /** + * @ignore + */ toString () { return ( util.timeToIsoString( @@ -213,7 +274,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Time} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Time}, `false` otherwise. */ export function isTime (obj) { @@ -232,8 +293,20 @@ export class Date { * @param {Integer|number} day the day for the new local date. */ constructor (year, month, day) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) Object.freeze(this) } @@ -254,6 +327,9 @@ export class Date { ) } + /** + * @ignore + */ toString () { return util.dateToIsoString(this.year, this.month, this.day) } @@ -267,7 +343,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link Date} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link Date}, `false` otherwise. */ export function isDate (obj) { @@ -290,12 +366,40 @@ export class LocalDateTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor (year, month, day, hour, minute, second, nanosecond) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) Object.freeze(this) } @@ -321,6 +425,9 @@ export class LocalDateTime { ) } + /** + * @ignore + */ toString () { return localDateTimeToString( this.year, @@ -342,7 +449,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link LocalDateTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link LocalDateTime}, `false` otherwise. */ export function isLocalDateTime (obj) { @@ -379,19 +486,61 @@ export class DateTime { timeZoneOffsetSeconds, timeZoneId ) { + /** + * The year. + * @type {Integer|number} + */ this.year = util.assertValidYear(year) + /** + * The month. + * @type {Integer|number} + */ this.month = util.assertValidMonth(month) + /** + * The day. + * @type {Integer|number} + */ this.day = util.assertValidDay(day) + /** + * The hour. + * @type {Integer|number} + */ this.hour = util.assertValidHour(hour) + /** + * The minute. + * @type {Integer|number} + */ this.minute = util.assertValidMinute(minute) + /** + * The second. + * @type {Integer|number} + */ this.second = util.assertValidSecond(second) + /** + * The nanosecond. + * @type {Integer|number} + */ this.nanosecond = util.assertValidNanosecond(nanosecond) const [offset, id] = verifyTimeZoneArguments( timeZoneOffsetSeconds, timeZoneId ) + /** + * The time zone offset in seconds. + * + * *Either this or {@link timeZoneId} is defined.* + * + * @type {Integer|number} + */ this.timeZoneOffsetSeconds = offset + /** + * The time zone id. + * + * *Either this or {@link timeZoneOffsetSeconds} is defined.* + * + * @type {string} + */ this.timeZoneId = id Object.freeze(this) @@ -419,6 +568,9 @@ export class DateTime { ) } + /** + * @ignore + */ toString () { const localDateTimeStr = localDateTimeToString( this.year, @@ -444,7 +596,7 @@ Object.defineProperty( /** * Test if given object is an instance of {@link DateTime} class. - * @param {object} obj the object to test. + * @param {Object} obj the object to test. * @return {boolean} `true` if given object is a {@link DateTime}, `false` otherwise. */ export function isDateTime (obj) { diff --git a/src/transaction-rx.js b/src/transaction-rx.js index 66f315743..4d636de3a 100644 --- a/src/transaction-rx.js +++ b/src/transaction-rx.js @@ -16,20 +16,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { newError } from './error' -import { Observable, from } from 'rxjs' -import Transaction from './transaction' +import { Observable } from 'rxjs' import RxResult from './result-rx' +import Transaction from './transaction' +/** + * A reactive transaction, which provides the same functionality as {@link Transaction} but through a Reactive API. + */ export default class RxTransaction { /** - * - * @param {Transaction} txc + * @constructor + * @protected + * @param {Transaction} txc - The underlying transaction instance to relay requests */ constructor (txc) { this._txc = txc } + /** + * Creates a reactive result that will execute the statement in this transaction, with the provided parameters. + * + * @public + * @param {string} statement - Statement to be executed. + * @param {Object} parameters - Parameter values to use in statement execution. + * @returns {RxResult} - A reactive result + */ + run (statement, parameters) { return new RxResult( new Observable(observer => { @@ -45,6 +57,12 @@ export default class RxTransaction { ) } + /** + * Commits the transaction. + * + * @public + * @returns {Observable} - An empty observable + */ commit () { return new Observable(observer => { this._txc @@ -56,6 +74,12 @@ export default class RxTransaction { }) } + /** + * Rollbacks the transaction. + * + * @public + * @returns {Observable} - An empty observable + */ rollback () { return new Observable(observer => { this._txc diff --git a/src/transaction.js b/src/transaction.js index d459fb055..32ba928d1 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -373,7 +373,7 @@ function finishTransaction (commit, connectionHolder, onError, onComplete) { * need to influence real connection holder to release connections. * @param {ResultStreamObserver} observer - an observer for the created result. * @param {string} statement - the cypher statement that produced the result. - * @param {object} parameters - the parameters for cypher statement that produced the result. + * @param {Object} parameters - the parameters for cypher statement that produced the result. * @return {Result} new result. * @private */ From 5dbb41d489935fe77129368d738063915f1d85e1 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Mon, 12 Aug 2019 17:49:48 +0100 Subject: [PATCH 08/14] Update README --- README.md | 417 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 288 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 1b12ae7fc..303c99605 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,28 @@ A database driver for Neo4j 3.0.0+. Resources to get you started: -- [Detailed docs](http://neo4j.com/docs/api/javascript-driver/current/). +- Detailed docs _Not available yet_ - [Sample small project using the driver](https://github.com/neo4j-examples/movies-javascript-bolt) - [Sample application using the driver](https://github.com/neo4j-examples/neo4j-movies-template) - [Neo4j Manual](https://neo4j.com/docs/) - [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) -## Include module in Node.js application +## What's New + +- Introduces a brand new reactive API (built on top of RxJS) available with 4.0 version server, which includes reactive protocol improvements. +- Session instances can now be acquired against a specific database against a multi-database server, which is available with 4.0 version server. + +## Breaking Changes + +- Driver API is moved from `neo4j.v1` to `neo4j` namespace. +- `driver#session()` method now makes use of object destructuring rather than positional arguments. +- `session#close()` now returns a `Promise` and no more accepts a callback function argument. +- `driver.onError` callback is removed and errors should be monitored on related code paths (i.e. through `Promise#catch`, etc.). +- `bolt+routing` scheme is now renamed to `neo4j`. `neo4j` scheme is designed to work work with all possible 4.0 server deployments, but `bolt` scheme is still available for explicit single instance connections. + +## Including the Driver + +### In Node.js application Stable channel: @@ -39,7 +54,7 @@ driver.close() otherwise application shutdown might hang or it might exit with a non-zero exit code. -## Include in web browser +### In web browser We build a special browser version of the driver, which supports connecting to Neo4j over WebSockets. It can be included in an HTML page using one of the following tags: @@ -63,13 +78,11 @@ This will make a global `neo4j` object available, where you can access the drive ```javascript var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j') ) ``` -\* Since 2.0, driver API is moved from `neo4j.v1` to `neo4j`. - It is not required to explicitly close the driver on a web page. Web browser should gracefully close all open WebSockets when the page is unloaded. However, driver instance should be explicitly closed when it's lifetime is not the same as the lifetime of the web page: @@ -80,7 +93,7 @@ driver.close() ## Usage examples -Driver lifecycle: +### Constructing a Driver ```javascript // Create a driver instance, for the user neo4j with password neo4j. @@ -95,66 +108,162 @@ var driver = neo4j.driver( driver.close() ``` -Session API: +### Acquiring a Session + +#### Regular Session ```javascript // Create a session to run Cypher statements in. // Note: Always make sure to close sessions when you are done using them! var session = driver.session() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var session = driver.session({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var session = driver.session({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var session = driver.session({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` + +#### Reactive Session + +```javascript +// Create a reactive session to run Cypher statements in. +// Note: Always make sure to close sessions when you are done using them! +var rxSession = driver.rxSession() +``` + +##### with a Default Access Mode of `READ` + +```javascript +var rxSession = driver.rxSession({ defaultAccessMode: neo4j.session.READ }) +``` + +##### with Bookmarks + +```javascript +var rxSession = driver.rxSession({ + bookmarks: [bookmark1FromPreviousSession, bookmark2FromPreviousSession] +}) +``` + +##### against a Database + +```javascript +var rxSession = driver.rxSession({ + database: 'foo', + defaultAccessMode: neo4j.session.WRITE +}) +``` +### Executing Statements + +#### Consuming Records with Streaming API + +```javascript // Run a Cypher statement, reading the result in a streaming manner as records arrive: session - .run('MERGE (alice:Person {name : {nameParam} }) RETURN alice.name AS name', { + .run('MERGE (alice:Person {name : $nameParam}) RETURN alice.name AS name', { nameParam: 'Alice' }) .subscribe({ - onNext: function(record) { + onKeys: keys => { + console.log(keys) + }, + onNext: record => { console.log(record.get('name')) }, - onCompleted: function() { + onCompleted: () => { session.close() }, - onError: function(error) { + onError: error => { console.log(error) } }) +``` + +Subscriber API allows following combinations of `onKeys`, `onNext`, `onCompleted` and `onError` callback invocations: -// or +- zero or one `onKeys`, +- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked in this case +- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked in this case. + +#### Consuming Records with Promise API + +```javascript // the Promise way, where the complete result is collected before we act on it: session - .run('MERGE (james:Person {name : {nameParam} }) RETURN james.name AS name', { + .run('MERGE (james:Person {name : $nameParam}) RETURN james.name AS name', { nameParam: 'James' }) - .then(function(result) { - result.records.forEach(function(record) { + .then(result => { + result.records.forEach(record => { console.log(record.get('name')) }) - session.close() }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) ``` -Transaction functions API: +#### Consuming Records with Reactive API + +```javascript +rxSession + .run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', { + nameParam: 'Bob' + }) + .records() + .pipe( + map(record => record.get('name')), + concat(rxSession.close()) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(err) + }) +``` + +### Transaction functions ```javascript // Transaction functions provide a convenient API with minimal boilerplate and // retries on network fluctuations and transient errors. Maximum retry time is // configured on the driver level and is 30 seconds by default: +// Applies both to standard and reactive sessions. neo4j.driver('bolt://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { maxTransactionRetryTime: 30000 }) +``` + +#### Reading with Async Session +```javascript // It is possible to execute read transactions that will benefit from automatic // retries on both single instance ('bolt' URI scheme) and Causal Cluster // ('neo4j' URI scheme) and will get automatic load balancing in cluster deployments -var readTxResultPromise = session.readTransaction(function(transaction) { +var readTxResultPromise = session.readTransaction(txc => { // used transaction will be committed automatically, no need for explicit commit/rollback - var result = transaction.run( - 'MATCH (person:Person) RETURN person.name AS name' - ) + var result = txc.run('MATCH (person:Person) RETURN person.name AS name') // at this point it is possible to either return the result or process it and return the // result of processing it is also possible to run more statements in the same transaction return result @@ -162,152 +271,165 @@ var readTxResultPromise = session.readTransaction(function(transaction) { // returned Promise can be later consumed like this: readTxResultPromise - .then(function(result) { - session.close() + .then(result => { console.log(result.records) }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) +``` + +#### Reading with Reactive Session +```javascript +rxSession + .readTransaction(txc => + txc + .run('MATCH (person:Person) RETURN person.name AS name') + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: err => console.log(error) + }) +``` + +#### Writing with Async Session + +```javascript // It is possible to execute write transactions that will benefit from automatic retries // on both single instance ('bolt' URI scheme) and Causal Cluster ('neo4j' URI scheme) -var writeTxResultPromise = session.writeTransaction(function(transaction) { +var writeTxResultPromise = session.writeTransaction(async txc => { // used transaction will be committed automatically, no need for explicit commit/rollback - var result = transaction.run( + var result = await txc.run( "MERGE (alice:Person {name : 'Alice' }) RETURN alice.name AS name" ) // at this point it is possible to either return the result or process it and return the // result of processing it is also possible to run more statements in the same transaction - return result.records.map(function(record) { - return record.get('name') - }) + return result.records.map(record => record.get('name')) }) // returned Promise can be later consumed like this: writeTxResultPromise - .then(function(namesArray) { - session.close() + .then(namesArray => { console.log(namesArray) }) - .catch(function(error) { + .catch(error => { console.log(error) }) + .then(() => session.close()) ``` -Explicit transactions API: +#### Writing with Reactive Session ```javascript -// run statement in a transaction -var tx = session.beginTransaction() - -tx.run('MERGE (bob:Person {name : {nameParam} }) RETURN bob.name AS name', { - nameParam: 'Bob' -}).subscribe({ - onNext: function(record) { - console.log(record.get('name')) - }, - onCompleted: function() { - console.log('First query completed') - }, - onError: function(error) { - console.log(error) - } -}) +rxSession + .writeTransaction(txc => + txc + .run("MERGE (alice:Person {name: 'James'}) RETURN alice.name AS name") + .records() + .pipe(map(record => record.get('name'))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` -tx.run('MERGE (adam:Person {name : {nameParam} }) RETURN adam.name AS name', { - nameParam: 'Adam' -}).subscribe({ - onNext: function(record) { - console.log(record.get('name')) - }, - onCompleted: function() { - console.log('Second query completed') - }, - onError: function(error) { - console.log(error) - } -}) +### Explicit Transactions -//decide if the transaction should be committed or rolled back -var success = false +#### With Async Session -if (success) { - tx.commit().subscribe({ - onCompleted: function() { - // this transaction is now committed and session can be closed - session.close() - }, - onError: function(error) { - console.log(error) +```javascript +// run statement in a transaction +const txc = session.beginTransaction() +try { + const result1 = await txc.run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' } - }) -} else { - //transaction is rolled black and nothing is created in the database + ) + result1.records.forEach(r => console.log(r.get('name'))) + console.log('First query completed') + + const result2 = await txc.run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + result2.records.forEach(r => console.log(r.get('name'))) + console.log('Second query completed') + + await txc.commit() + console.log('committed') +} catch (error) { + console.log(error) + await txc.rollback() console.log('rolled back') - tx.rollback() +} finally { + await session.close() } ``` -Subscriber API allows following combinations of `onNext`, `onCompleted` and `onError` callback invocations: - -- zero or more `onNext` followed by `onCompleted` when operation was successful. `onError` will not be invoked - in this case -- zero or more `onNext` followed by `onError` when operation failed. Callback `onError` might be invoked after - couple `onNext` invocations because records are streamed lazily by the database. `onCompleted` will not be invoked - in this case - -## Parallelization - -In a single session, multiple queries will be executed serially. In order to parallelize queries, multiple sessions are required. - -## Building - - npm install - npm run build - -This produces browser-compatible standalone files under `lib/browser` and a Node.js module version under `lib/`. -See files under `examples/` on how to use. - -## Testing - -Tests **require** latest [Boltkit](https://github.com/neo4j-contrib/boltkit) to be installed in the system. It is needed to start, stop and configure local test database. Boltkit can be installed with the following command: - - pip install --upgrade boltkit - -To run tests against "default" Neo4j version: - - ./runTests.sh - -To run tests against specified Neo4j version: - -./runTests.sh '-e 3.1.3' +#### With Reactive Session -Simple `npm test` can also be used if you already have a running version of a compatible Neo4j server. - -For development, you can have the build tool rerun the tests each time you change -the source code: - - gulp watch-n-test - -### Testing on windows - -Running tests on windows requires PhantomJS installed and its bin folder added in windows system variable `Path`. -To run the same test suite, run `.\runTest.ps1` instead in powershell with admin right. -The admin right is required to start/stop Neo4j properly as a system service. -While there is no need to grab admin right if you are running tests against an existing Neo4j server using `npm test`. +```javascript +rxSession + .beginTransaction() + .pipe( + flatMap(txc => + concat( + txc + .run( + 'MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', + { + nameParam: 'Bob' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('First query completed'), + txc + .run( + 'MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', + { + nameParam: 'Adam' + } + ) + .records() + .pipe(map(r => r.get('name'))), + of('Second query completed'), + txc.commit(), + of('committed') + ).pipe(catchError(err => txc.rollback().pipe(throwError(err)))) + ) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) +``` -## A note on numbers and the Integer type +### Numbers and the Integer type The Neo4j type system includes 64-bit integer values. However, JavaScript can only safely represent integers between `-(2``53``- 1)` and `(2``53``- 1)`. In order to support the full Neo4j type system, the driver will not automatically convert to javascript integers. Any time the driver receives an integer value from Neo4j, it will be represented with an internal integer type by the driver. -### Write integers +_**Any javascript number value passed as a parameter will be recognized as `Float` type.**_ + +#### Writing integers -Number written directly e.g. `session.run("CREATE (n:Node {age: {age}})", {age: 22})` will be of type `Float` in Neo4j. +Numbers written directly e.g. `session.run("CREATE (n:Node {age: {age}})", {age: 22})` will be of type `Float` in Neo4j. To write the `age` as an integer the `neo4j.int` method should be used: ```javascript @@ -324,7 +446,7 @@ session.run('CREATE (n {age: {myIntParam}})', { }) ``` -### Read integers +#### Reading integers Since Integers can be larger than can be represented as JavaScript numbers, it is only safe to convert to JavaScript numbers if you know that they will not exceed `(2``53``- 1)` in size. In order to facilitate working with integers the driver include `neo4j.isInt`, `neo4j.integer.inSafeRange`, `neo4j.integer.toNumber`, and `neo4j.integer.toString`. @@ -345,7 +467,7 @@ if (!neo4j.integer.inSafeRange(aLargerInteger)) { } ``` -### Enable native numbers +#### Enabling native numbers Starting from 1.6 version of the driver it is possible to configure it to only return native numbers instead of custom `Integer` objects. The configuration option affects all integers returned by the driver. **Enabling this option can result in a loss of precision and incorrect numeric @@ -359,3 +481,40 @@ var driver = neo4j.driver( { disableLosslessIntegers: true } ) ``` + +## Building + + npm install + npm run build + +This produces browser-compatible standalone files under `lib/browser` and a Node.js module version under `lib/`. +See files under `examples/` on how to use. + +## Testing + +Tests **require** latest [Boltkit](https://github.com/neo4j-contrib/boltkit) and [Firefox](https://www.mozilla.org/firefox/) to be installed in the system. + +Boltkit is needed to start, stop and configure local test database. Boltkit can be installed with the following command: + + pip3 install --upgrade boltkit + +To run tests against "default" Neo4j version: + + ./runTests.sh + +To run tests against specified Neo4j version: + +./runTests.sh '-e 3.1.3' + +Simple `npm test` can also be used if you already have a running version of a compatible Neo4j server. + +For development, you can have the build tool rerun the tests each time you change +the source code: + + gulp watch-n-test + +### Testing on windows + +To run the same test suite, run `.\runTest.ps1` instead in powershell with admin right. +The admin right is required to start/stop Neo4j properly as a system service. +While there is no need to grab admin right if you are running tests against an existing Neo4j server using `npm test`. From 627e9d11ae92379217188b803a71fa8d51a34dd4 Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Tue, 13 Aug 2019 13:22:16 +0100 Subject: [PATCH 09/14] Add type definitions for reactive API --- gulpfile.babel.js | 4 +- test/types/driver.test.ts | 51 +++++++++- test/types/result-rx.test.ts | 45 +++++++++ test/types/result.test.ts | 7 ++ test/types/session-rx.test.ts | 151 ++++++++++++++++++++++++++++++ test/types/transaction-rx.test.ts | 71 ++++++++++++++ tsconfig.json | 4 +- types/driver.d.ts | 11 +++ types/index.d.ts | 6 +- types/result-rx.d.ts | 31 ++++++ types/result.d.ts | 7 +- types/session-rx.d.ts | 51 ++++++++++ types/transaction-rx.d.ts | 31 ++++++ 13 files changed, 457 insertions(+), 13 deletions(-) create mode 100644 test/types/result-rx.test.ts create mode 100644 test/types/session-rx.test.ts create mode 100644 test/types/transaction-rx.test.ts create mode 100644 types/result-rx.d.ts create mode 100644 types/session-rx.d.ts create mode 100644 types/transaction-rx.d.ts diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 81a7492f5..7b5814577 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -201,7 +201,9 @@ gulp.task('run-ts-declaration-tests', function (done) { target: 'es6', noImplicitAny: true, noImplicitReturns: true, - strictNullChecks: true + strictNullChecks: true, + moduleResolution: 'node', + types: [] }) ) .pipe(gulp.dest('build/test/types')) diff --git a/test/types/driver.test.ts b/test/types/driver.test.ts index cf1e98d55..aa577282d 100644 --- a/test/types/driver.test.ts +++ b/test/types/driver.test.ts @@ -30,6 +30,9 @@ import { Parameters } from '../../types/statement-runner' import Session from '../../types/session' import { Neo4jError } from '../../types/error' import { ServerInfo } from '../../types/result-summary' +import RxSession from '../../types/session-rx' +import { concat, map, catchError } from 'rxjs/operators' +import { throwError } from 'rxjs' const dummy: any = null @@ -88,12 +91,14 @@ const session7: Session = driver.session({ bookmarks: 'bookmark2' }) -session1.run('RETURN 1').then(result => { - session1.close() - result.records.forEach(record => { - console.log(record) +session1 + .run('RETURN 1') + .then(result => { + result.records.forEach(record => { + console.log(record) + }) }) -}) + .then(() => session1.close()) const close: void = driver.close() @@ -101,3 +106,39 @@ driver.verifyConnectivity().then((serverInfo: ServerInfo) => { console.log(serverInfo.version) console.log(serverInfo.address) }) + +const rxSession1: RxSession = driver.rxSession() +const rxSession2: RxSession = driver.rxSession({ defaultAccessMode: READ }) +const rxSession3: RxSession = driver.rxSession({ defaultAccessMode: 'READ' }) +const rxSession4: RxSession = driver.rxSession({ defaultAccessMode: WRITE }) +const rxSession5: RxSession = driver.rxSession({ defaultAccessMode: 'WRITE' }) +const rxSession6: RxSession = driver.rxSession({ + defaultAccessMode: READ, + bookmarks: 'bookmark1' +}) +const rxSession7: RxSession = driver.rxSession({ + defaultAccessMode: READ, + bookmarks: ['bookmark1', 'bookmark2'] +}) +const rxSession8: RxSession = driver.rxSession({ + defaultAccessMode: WRITE, + bookmarks: 'bookmark1' +}) +const rxSession9: RxSession = driver.rxSession({ + defaultAccessMode: WRITE, + bookmarks: ['bookmark1', 'bookmark2'] +}) + +rxSession1 + .run('RETURN 1') + .records() + .pipe( + map(r => r.get(0)), + concat(rxSession1.close()), + catchError(err => rxSession1.close().pipe(concat(throwError(err)))) + ) + .subscribe({ + next: data => console.log(data), + complete: () => console.log('completed'), + error: error => console.log(error) + }) diff --git a/test/types/result-rx.test.ts b/test/types/result-rx.test.ts new file mode 100644 index 000000000..8dc7f53f7 --- /dev/null +++ b/test/types/result-rx.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2002-2019 "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 RxResult from '../../types/result-rx' +import Record from '../../types/record' +import ResultSummary from '../../types/result-summary' +import { Observer } from 'rxjs' + +const dummy: any = null + +const res: RxResult = dummy + +res.keys().subscribe({ + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +}) + +res.records().subscribe({ + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +}) + +res.summary().subscribe({ + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +}) diff --git a/test/types/result.test.ts b/test/types/result.test.ts index 0f4a46005..4ba47f443 100644 --- a/test/types/result.test.ts +++ b/test/types/result.test.ts @@ -50,3 +50,10 @@ res.subscribe({ onError: (error: Error) => console.log(error), onCompleted: (summary: ResultSummary) => console.log(summary) }) + +res.subscribe({ + onKeys: (keys: string[]) => console.log(keys), + onNext: (record: Record) => console.log(record), + onError: (error: Error) => console.log(error), + onCompleted: (summary: ResultSummary) => console.log(summary) +}) diff --git a/test/types/session-rx.test.ts b/test/types/session-rx.test.ts new file mode 100644 index 000000000..be77b64a1 --- /dev/null +++ b/test/types/session-rx.test.ts @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2002-2019 "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 RxSession from '../../types/session-rx' +import { TransactionConfig } from '../../types/session' +import RxTransaction from '../../types/transaction-rx' +import Record from '../../types/record' +import RxResult from '../../types/result-rx' +import ResultSummary from '../../types/result-summary' +import Integer from '../../types/integer' +import { Observable, of, Observer, throwError } from 'rxjs' +import { concat, finalize, catchError } from 'rxjs/operators' + +const dummy: any = null +const intValue: Integer = Integer.fromInt(42) + +const keysObserver: Observer = { + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +} + +const recordsObserver: Observer = { + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +} + +const summaryObserver: Observer = { + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +} + +const rxSession: RxSession = dummy + +const txConfig1: TransactionConfig = {} +const txConfig2: TransactionConfig = { timeout: 5000 } +const txConfig3: TransactionConfig = { timeout: intValue } +const txConfig4: TransactionConfig = { metadata: {} } +const txConfig5: TransactionConfig = { + metadata: { + key1: 'value1', + key2: 5, + key3: { a: 'a', b: 'b' }, + key4: [1, 2, 3] + } +} +const txConfig6: TransactionConfig = { + timeout: 2000, + metadata: { key1: 'value1', key2: 2 } +} +const txConfig7: TransactionConfig = { + timeout: intValue, + metadata: { key1: 'value1', key2: 2 } +} + +const tx1: Observable = rxSession.beginTransaction() +const bookmark: null | string = rxSession.lastBookmark() + +const observable1: Observable = rxSession.readTransaction( + (tx: RxTransaction) => { + return of(10) + } +) + +const observable2: Observable = rxSession.readTransaction( + (tx: RxTransaction) => { + return of('42') + } +) + +const observable3: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => { + return of(10) + } +) + +const observable4: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => { + return of('42') + } +) + +const close1: Observable = rxSession.close() +const close2: Observable = rxSession + .close() + .pipe(finalize(() => 'session closed')) + +const result1: RxResult = rxSession.run('RETURN 1') +result1.keys().subscribe(keysObserver) +result1.records().subscribe(recordsObserver) +result1 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const result2: RxResult = rxSession.run('RETURN $value', { value: '42' }) +result2.keys().subscribe(keysObserver) +result2.records().subscribe(recordsObserver) +result2 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const result3: RxResult = rxSession.run( + 'RETURN $value', + { value: '42' }, + txConfig1 +) +result3.keys().subscribe(keysObserver) +result3.records().subscribe(recordsObserver) +result3 + .summary() + .pipe( + concat(close1), + catchError(err => close1.pipe(concat(throwError(err)))) + ) + .subscribe(summaryObserver) + +const tx2: Observable = rxSession.beginTransaction(txConfig2) +const observable5: Observable = rxSession.readTransaction( + (tx: RxTransaction) => of(''), + txConfig3 +) +const observable6: Observable = rxSession.writeTransaction( + (tx: RxTransaction) => of(42), + txConfig4 +) diff --git a/test/types/transaction-rx.test.ts b/test/types/transaction-rx.test.ts new file mode 100644 index 000000000..bacf7a830 --- /dev/null +++ b/test/types/transaction-rx.test.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2002-2019 "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 RxTransaction from '../../types/transaction-rx' +import Record from '../../types/record' +import RxResult from '../../types/result-rx' +import ResultSummary from '../../types/result-summary' +import { Observable, of, Observer, throwError } from 'rxjs' +import { concat, finalize, catchError } from 'rxjs/operators' + +const dummy: any = null + +const stringObserver: Observer = { + next: value => console.log(value), + complete: () => console.log('complete'), + error: error => console.log(`error: ${error}`) +} + +const keysObserver: Observer = { + next: value => console.log(`keys: ${value}`), + complete: () => console.log('keys complete'), + error: error => console.log(`keys error: ${error}`) +} + +const recordsObserver: Observer = { + next: value => console.log(`record: ${value}`), + complete: () => console.log('records complete'), + error: error => console.log(`records error: ${error}`) +} + +const summaryObserver: Observer = { + next: value => console.log(`summary: ${value}`), + complete: () => console.log('summary complete'), + error: error => console.log(`summary error: ${error}`) +} + +const tx: RxTransaction = dummy + +const result1: RxResult = tx.run('RETURN 1') +result1.keys().subscribe(keysObserver) +result1.records().subscribe(recordsObserver) +result1.summary().subscribe(summaryObserver) + +const result2: RxResult = tx.run('RETURN $value', { value: '42' }) +result2.keys().subscribe(keysObserver) +result2.records().subscribe(recordsObserver) +result2.summary().subscribe(summaryObserver) + +tx.commit() + .pipe(concat(of('committed'))) + .subscribe(stringObserver) + +tx.rollback() + .pipe(concat(of('rolled back'))) + .subscribe(stringObserver) diff --git a/tsconfig.json b/tsconfig.json index 4881be175..b1ed827a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "module": "es6", "target": "es6", "noImplicitAny": true, - "noImplicitReturns": true + "noImplicitReturns": true, + "moduleResolution": "node", + "types": [] } } diff --git a/types/driver.d.ts b/types/driver.d.ts index d087753be..2701fb16d 100644 --- a/types/driver.d.ts +++ b/types/driver.d.ts @@ -18,6 +18,7 @@ */ import Session from './session' +import RxSession from './session-rx' import { Parameters } from './statement-runner' import { Neo4jError } from './error' import { ServerInfo } from './result-summary' @@ -72,6 +73,16 @@ declare interface Driver { database?: string }): Session + rxSession({ + defaultAccessMode, + bookmarks, + database + }?: { + defaultAccessMode?: SessionMode + bookmarks?: string | string[] + database?: string + }): RxSession + close(): void verifyConnectivity(): Promise diff --git a/types/index.d.ts b/types/index.d.ts index 5062de5ab..568e149bc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -46,7 +46,7 @@ import { SERVICE_UNAVAILABLE, SESSION_EXPIRED } from './error' -import Result, { Observer, StatementResult } from './result' +import Result, { ResultObserver, StatementResult } from './result' import ResultSummary, { Notification, NotificationPosition, @@ -174,7 +174,7 @@ declare const forExport: { Record: Record Result: Result StatementResult: StatementResult - Observer: Observer + ResultObserver: ResultObserver ResultSummary: ResultSummary Plan: Plan ProfiledPlan: ProfiledPlan @@ -227,7 +227,7 @@ export { Record, Result, StatementResult, - Observer, + ResultObserver, ResultSummary, Plan, ProfiledPlan, diff --git a/types/result-rx.d.ts b/types/result-rx.d.ts new file mode 100644 index 000000000..7ca0baa18 --- /dev/null +++ b/types/result-rx.d.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2002-2019 "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 { Observable } from 'rxjs' +import ResultSummary from './result-summary' +import Record from './record' + +declare interface RxResult { + keys(): Observable + + records(): Observable + + summary(): Observable +} + +export default RxResult diff --git a/types/result.d.ts b/types/result.d.ts index 8a2d2bc9a..022f38a18 100644 --- a/types/result.d.ts +++ b/types/result.d.ts @@ -25,15 +25,16 @@ declare type StatementResult = { summary: ResultSummary } -declare type Observer = { +declare type ResultObserver = { + onKeys?(keys: string[]): void onNext?(record: Record): void onCompleted?(summary: ResultSummary): void onError?(error: Error): void } declare interface Result extends Promise { - subscribe(observer: Observer): void + subscribe(observer: ResultObserver): void } -export { StatementResult, Observer } +export { StatementResult, ResultObserver } export default Result diff --git a/types/session-rx.d.ts b/types/session-rx.d.ts new file mode 100644 index 000000000..ec6d4ee6a --- /dev/null +++ b/types/session-rx.d.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2002-2019 "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 RxResult from './result-rx' +import RxTransaction from './transaction-rx' +import { TransactionConfig } from './session' +import { Parameters } from './statement-runner' +import { Observable } from 'rxjs' + +declare type RxTransactionWork = (tx: RxTransaction) => Observable + +declare interface RxSession { + run( + statement: string, + parameters?: Parameters, + config?: TransactionConfig + ): RxResult + + beginTransaction(config?: TransactionConfig): Observable + + lastBookmark(): string | null + + readTransaction( + work: RxTransactionWork, + config?: TransactionConfig + ): Observable + + writeTransaction( + work: RxTransactionWork, + config?: TransactionConfig + ): Observable + + close(): Observable +} + +export default RxSession diff --git a/types/transaction-rx.d.ts b/types/transaction-rx.d.ts new file mode 100644 index 000000000..a7719224a --- /dev/null +++ b/types/transaction-rx.d.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2002-2019 "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 { Observable } from 'rxjs' +import { Parameters } from './statement-runner' +import RxResult from './result-rx' + +declare interface RxTransaction { + run(statement: string, parameters?: Parameters): RxResult + + commit(): Observable + + rollback(): Observable +} + +export default RxTransaction From 8d88bbc5985a8ce8cb7863aec6fbaa4b61e227fb Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Tue, 13 Aug 2019 13:32:51 +0100 Subject: [PATCH 10/14] Update README --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 303c99605..861d3e18a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ A database driver for Neo4j 3.0.0+. Resources to get you started: - Detailed docs _Not available yet_ -- [Sample small project using the driver](https://github.com/neo4j-examples/movies-javascript-bolt) -- [Sample application using the driver](https://github.com/neo4j-examples/neo4j-movies-template) - [Neo4j Manual](https://neo4j.com/docs/) - [Neo4j Refcard](https://neo4j.com/docs/cypher-refcard/current/) @@ -14,13 +12,14 @@ Resources to get you started: - Introduces a brand new reactive API (built on top of RxJS) available with 4.0 version server, which includes reactive protocol improvements. - Session instances can now be acquired against a specific database against a multi-database server, which is available with 4.0 version server. +- A new `driver.verifyConnectivity()` method is introduced for connectivity verification purposes. ## Breaking Changes - Driver API is moved from `neo4j.v1` to `neo4j` namespace. -- `driver#session()` method now makes use of object destructuring rather than positional arguments. +- `driver#session()` method now makes use of object destructuring rather than positional arguments (see [Acquiring a Session](#acquiring-a-session) for examples). - `session#close()` now returns a `Promise` and no more accepts a callback function argument. -- `driver.onError` callback is removed and errors should be monitored on related code paths (i.e. through `Promise#catch`, etc.). +- `driver.onError` and `driver.onCompleted` callbacks are completely removed. Errors should be monitored on related code paths (i.e. through `Promise#catch`, etc.). - `bolt+routing` scheme is now renamed to `neo4j`. `neo4j` scheme is designed to work work with all possible 4.0 server deployments, but `bolt` scheme is still available for explicit single instance connections. ## Including the Driver @@ -99,7 +98,7 @@ driver.close() // Create a driver instance, for the user neo4j with password neo4j. // It should be enough to have a single driver per database per application. var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j') ) @@ -249,7 +248,7 @@ rxSession // retries on network fluctuations and transient errors. Maximum retry time is // configured on the driver level and is 30 seconds by default: // Applies both to standard and reactive sessions. -neo4j.driver('bolt://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { +neo4j.driver('neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { maxTransactionRetryTime: 30000 }) ``` @@ -476,7 +475,7 @@ To enable potentially lossy integer values use the driver's configuration object ```javascript var driver = neo4j.driver( - 'bolt://localhost', + 'neo4j://localhost', neo4j.auth.basic('neo4j', 'neo4j'), { disableLosslessIntegers: true } ) From e753bdd47c35fe665b3bdc97d1e0fa76b9164eaa Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Thu, 15 Aug 2019 20:24:26 +0100 Subject: [PATCH 11/14] Make transaction commit and rollback return standard promises --- src/transaction.js | 18 ++++++++++++++---- test/transaction.test.js | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 32ba928d1..49c9f2105 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -107,7 +107,12 @@ class Transaction { this._state = committed.state // clean up this._onClose() - return committed.result + return new Promise((resolve, reject) => { + committed.result.subscribe({ + onCompleted: () => resolve(), + onError: error => reject(error) + }) + }) } /** @@ -118,15 +123,20 @@ class Transaction { * @returns {Result} New Result */ rollback () { - let committed = this._state.rollback({ + let rolledback = this._state.rollback({ connectionHolder: this._connectionHolder, onError: this._onError, onComplete: this._onComplete }) - this._state = committed.state + this._state = rolledback.state // clean up this._onClose() - return committed.result + return new Promise((resolve, reject) => { + rolledback.result.subscribe({ + onCompleted: () => resolve(), + onError: error => reject(error) + }) + }) } /** diff --git a/test/transaction.test.js b/test/transaction.test.js index 7c8ea5042..4505ae793 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -19,6 +19,7 @@ import neo4j from '../src' import sharedNeo4j from './internal/shared-neo4j' import { ServerVersion } from '../src/internal/server-version' +import TxConfig from '../src/internal/tx-config' describe('#integration transaction', () => { let driver @@ -571,6 +572,20 @@ describe('#integration transaction', () => { }) }) + it('should return empty promise on commit', async () => { + const tx = session.beginTransaction() + const result = await tx.commit() + + expect(result).toBeUndefined() + }) + + it('should return empty promise on rollback', async () => { + const tx = session.beginTransaction() + const result = await tx.rollback() + + expect(result).toBeUndefined() + }) + function expectSyntaxError (error) { expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError') } From 00632b4be685690a718cfe1b42ba3369bfc7235e Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Fri, 16 Aug 2019 13:38:21 +0100 Subject: [PATCH 12/14] Make channels and connections Promise aware for close operations --- src/internal/browser/browser-channel.js | 27 ++++++-- src/internal/connection-channel.js | 7 +- src/internal/connection-delegate.js | 4 +- src/internal/connection.js | 5 +- src/internal/node/node-channel.js | 22 ++++--- test/browser/karma-firefox.conf.js | 2 +- test/internal/browser/browser-channel.test.js | 43 +++++++++++++ test/internal/node/node-channel.test.js | 64 +++++++++++++++++++ 8 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 test/internal/node/node-channel.test.js diff --git a/src/internal/browser/browser-channel.js b/src/internal/browser/browser-channel.js index b81038c48..3c0b4ff41 100644 --- a/src/internal/browser/browser-channel.js +++ b/src/internal/browser/browser-channel.js @@ -21,6 +21,13 @@ import HeapBuffer from './browser-buf' import { newError } from '../../error' import { ENCRYPTION_OFF, ENCRYPTION_ON } from '../util' +// Just to be sure that these values are with us even after WebSocket is injected +// for tests. +const WS_CONNECTING = 0 +const WS_OPEN = 1 +const WS_CLOSING = 2 +const WS_CLOSED = 3 + /** * Create a new WebSocketChannel to be used in web browsers. * @access private @@ -131,13 +138,19 @@ export default class WebSocketChannel { /** * Close the connection - * @param {function} cb - Function to call on close. + * @returns {Promise} A promise that will be resolved after channel is closed */ - close (cb = () => null) { - this._open = false - this._clearConnectionTimeout() - this._ws.close() - this._ws.onclose = cb + 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() + } + }) } /** @@ -151,7 +164,7 @@ export default class WebSocketChannel { const webSocket = this._ws return setTimeout(() => { - if (webSocket.readyState !== WebSocket.OPEN) { + if (webSocket.readyState !== WS_OPEN) { this._connectionTimeoutFired = true webSocket.close() } diff --git a/src/internal/connection-channel.js b/src/internal/connection-channel.js index 64d6c8da6..372342931 100644 --- a/src/internal/connection-channel.js +++ b/src/internal/connection-channel.js @@ -438,9 +438,9 @@ export default class ChannelConnection extends Connection { /** * Call close on the channel. - * @param {function} cb - Function to call on close. + * @returns {Promise} - A promise that will be resolved when the underlying channel is closed. */ - close (cb = () => null) { + close () { if (this._log.isDebugEnabled()) { this._log.debug(`${this} closing`) } @@ -451,11 +451,10 @@ export default class ChannelConnection extends Connection { this._protocol.prepareToClose() } - this._ch.close(() => { + return this._ch.close().then(() => { if (this._log.isDebugEnabled()) { this._log.debug(`${this} closed`) } - cb() }) } diff --git a/src/internal/connection-delegate.js b/src/internal/connection-delegate.js index 97a9e2665..60beb3f58 100644 --- a/src/internal/connection-delegate.js +++ b/src/internal/connection-delegate.js @@ -83,8 +83,8 @@ export default class DelegateConnection extends Connection { return this._delegate.resetAndFlush() } - close (cb = () => null) { - return this._delegate.close(cb) + close () { + return this._delegate.close() } _release () { diff --git a/src/internal/connection.js b/src/internal/connection.js index 64e510c5f..f51c5cbb7 100644 --- a/src/internal/connection.js +++ b/src/internal/connection.js @@ -106,9 +106,10 @@ export default class Connection { /** * Call close on the channel. - * @param {function} cb - Function to call on close. + * @returns {Promise} - A promise that will be resolved when the connection is closed. + * */ - close (cb = () => null) { + close () { throw new Error('not implemented') } diff --git a/src/internal/node/node-channel.js b/src/internal/node/node-channel.js index 1511b7261..75a1a3c13 100644 --- a/src/internal/node/node-channel.js +++ b/src/internal/node/node-channel.js @@ -334,16 +334,18 @@ export default class NodeChannel { /** * Close the connection - * @param {function} cb - Function to call on close. + * @returns {Promise} A promise that will be resolved after channel is closed */ - close (cb = () => null) { - this._open = false - if (this._conn) { - this._conn.end() - this._conn.removeListener('end', this._handleConnectionTerminated) - this._conn.on('end', cb) - } else { - cb() - } + close () { + return new Promise((resolve, reject) => { + if (this._open) { + this._open = false + this._conn.removeListener('end', this._handleConnectionTerminated) + this._conn.on('end', () => resolve()) + this._conn.end() + } else { + resolve() + } + }) } } diff --git a/test/browser/karma-firefox.conf.js b/test/browser/karma-firefox.conf.js index c3153e5db..f971bbb48 100644 --- a/test/browser/karma-firefox.conf.js +++ b/test/browser/karma-firefox.conf.js @@ -36,7 +36,7 @@ module.exports = function (config) { reporters: ['spec'], port: 9876, // karma web server port colors: true, - logLevel: config.LOG_DEBUG, + logLevel: config.LOG_ERROR, browsers: ['FirefoxHeadless'], autoWatch: false, singleRun: true, diff --git a/test/internal/browser/browser-channel.test.js b/test/internal/browser/browser-channel.test.js index 0ad05d3bb..23101591e 100644 --- a/test/internal/browser/browser-channel.test.js +++ b/test/internal/browser/browser-channel.test.js @@ -24,6 +24,11 @@ import { setTimeoutMock } from '../timers-util' import { ENCRYPTION_OFF, ENCRYPTION_ON } from '../../../src/internal/util' import ServerAddress from '../../../src/internal/server-address' +const WS_CONNECTING = 0 +const WS_OPEN = 1 +const WS_CLOSING = 2 +const WS_CLOSED = 3 + /* eslint-disable no-global-assign */ describe('#unit WebSocketChannel', () => { let OriginalWebSocket @@ -158,6 +163,44 @@ describe('#unit WebSocketChannel', () => { testWarningInMixedEnvironment(ENCRYPTION_OFF, 'https') }) + it('should resolve close if websocket is already closed', () => { + WebSocket = () => { + return { + readyState: WS_CLOSED + } + } + + const address = ServerAddress.fromUrl('bolt://localhost:8989') + const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + + const channel = new WebSocketChannel(channelConfig) + + return expectAsync(channel.close()).toBeResolved() + }) + + it('should resolve close when websocket is closed', () => { + WebSocket = () => { + const ws = { + readyState: WS_OPEN, + onclose: () => {} + } + + ws.close = () => { + ws.readyState = WS_CLOSED + ws.onclose() + } + + return ws + } + + const address = ServerAddress.fromUrl('bolt://localhost:8989') + const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + + const channel = new WebSocketChannel(channelConfig) + + return expectAsync(channel.close()).toBeResolved() + }) + function testFallbackToLiteralIPv6 (boltAddress, expectedWsAddress) { // replace real WebSocket with a function that throws when IPv6 address is used WebSocket = url => { diff --git a/test/internal/node/node-channel.test.js b/test/internal/node/node-channel.test.js new file mode 100644 index 000000000..46867eb47 --- /dev/null +++ b/test/internal/node/node-channel.test.js @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2002-2019 "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 '../../../src/internal/node/node-channel' +import ChannelConfig from '../../../src/internal/channel-config' +import { SERVICE_UNAVAILABLE } from '../../../src/error' +import ServerAddress from '../../../src/internal/server-address' +import { connect } from 'net' + +describe('#unit NodeChannel', () => { + it('should resolve close if websocket is already closed', () => { + const address = ServerAddress.fromUrl('bolt://localhost:9999') + const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + const channel = new NodeChannel(channelConfig) + + // Modify the connection to be closed + channel._open = false + + return expectAsync(channel.close()).toBeResolved() + }) + + it('should resolve close when websocket is connected', () => { + const channel = createMockedChannel(true) + + return expectAsync(channel.close()).toBeResolved() + }) +}) + +function createMockedChannel (connected) { + let endCallback = null + const address = ServerAddress.fromUrl('bolt://localhost:9999') + const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + const channel = new NodeChannel(channelConfig) + const socket = { + end: () => { + channel._open = false + endCallback() + }, + removeListener: () => {}, + on: (key, cb) => { + if (key === 'end') { + endCallback = cb + } + } + } + channel._conn = socket + channel._open = connected + return channel +} From 07ec4cbcfcbc6e9461c5ae8c37dca2784109f29c Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Wed, 21 Aug 2019 16:32:36 +0100 Subject: [PATCH 13/14] Modify driver.close() and related internals to return Promise --- README.md | 8 +- src/driver.js | 3 +- src/internal/browser/browser-channel.js | 18 +- src/internal/connection-channel.js | 12 +- src/internal/connection-delegate.js | 2 +- src/internal/connection-provider-pooled.js | 22 +- src/internal/connection-provider-routing.js | 24 +- src/internal/node/node-channel.js | 15 +- src/internal/pool.js | 73 +- src/internal/routing-util.js | 5 +- src/internal/server-version.js | 11 +- test/bolt-v3.test.js | 516 +-- test/bolt-v4.test.js | 38 +- test/driver.test.js | 163 +- test/examples.test.js | 113 +- test/internal/bolt-stub.js | 78 +- test/internal/browser/browser-channel.test.js | 183 +- test/internal/connection-channel.test.js | 61 +- test/internal/connection-delegate.test.js | 6 +- test/internal/dummy-channel.js | 6 +- test/internal/fake-connection.js | 1 + test/internal/logger.test.js | 50 +- .../node/direct.driver.boltkit.test.js | 593 ++- test/internal/node/node-channel.test.js | 2 + test/internal/node/package.test.js | 8 +- .../node/routing.driver.boltkit.test.js | 3514 +++++++---------- test/internal/node/tls.test.js | 16 +- test/internal/pool.test.js | 794 ++-- test/internal/routing-util.test.js | 1 + test/internal/server-version.test.js | 39 +- test/internal/shared-neo4j.js | 20 +- test/result.test.js | 8 +- test/rx/navigation.test.js | 12 +- test/rx/session.test.js | 17 +- test/rx/summary.test.js | 13 +- test/rx/transaction.test.js | 10 +- test/session.test.js | 89 +- test/spatial-types.test.js | 51 +- test/stress.test.js | 30 +- test/summary.test.js | 7 +- test/temporal-types.test.js | 20 +- test/transaction.test.js | 102 +- test/types.test.js | 82 +- test/types/driver.test.ts | 2 +- types/driver.d.ts | 2 +- 45 files changed, 2852 insertions(+), 3988 deletions(-) diff --git a/README.md b/README.md index 861d3e18a..4773ab684 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ var neo4j = require('neo4j-driver') Driver instance should be closed when Node.js application exits: ```javascript -driver.close() +driver.close() // returns a Promise ``` otherwise application shutdown might hang or it might exit with a non-zero exit code. @@ -87,7 +87,7 @@ WebSockets when the page is unloaded. However, driver instance should be explici is not the same as the lifetime of the web page: ```javascript -driver.close() +driver.close() // returns a Promise ``` ## Usage examples @@ -104,7 +104,7 @@ var driver = neo4j.driver( // Close the driver when application exits. // This closes all used network connections. -driver.close() +await driver.close() ``` ### Acquiring a Session @@ -189,7 +189,7 @@ session console.log(record.get('name')) }, onCompleted: () => { - session.close() + session.close() // returns a Promise }, onError: error => { console.log(error) diff --git a/src/driver.js b/src/driver.js index 044809457..8c193589a 100644 --- a/src/driver.js +++ b/src/driver.js @@ -174,8 +174,9 @@ class Driver { close () { this._log.info(`Driver ${this._id} closing`) if (this._connectionProvider) { - this._connectionProvider.close() + return this._connectionProvider.close() } + return Promise.resolve() } /** diff --git a/src/internal/browser/browser-channel.js b/src/internal/browser/browser-channel.js index 3c0b4ff41..dfa8d1e15 100644 --- a/src/internal/browser/browser-channel.js +++ b/src/internal/browser/browser-channel.js @@ -38,7 +38,11 @@ export default class WebSocketChannel { * @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) { + constructor ( + config, + protocolSupplier = detectWebPageProtocol, + socketFactory = url => new WebSocket(url) + ) { this._open = true this._pending = [] this._error = null @@ -51,14 +55,14 @@ export default class WebSocketChannel { return } - this._ws = createWebSocket(scheme, config.address) + this._ws = createWebSocket(scheme, config.address, socketFactory) this._ws.binaryType = 'arraybuffer' let 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.wasClean) { + if (e && !e.wasClean) { self._handleConnectionError() } } @@ -142,7 +146,7 @@ export default class WebSocketChannel { */ close () { return new Promise((resolve, reject) => { - if (this._ws && this._ws.readyState != WS_CLOSED) { + if (this._ws && this._ws.readyState !== WS_CLOSED) { this._open = false this._clearConnectionTimeout() this._ws.onclose = () => resolve() @@ -187,11 +191,11 @@ export default class WebSocketChannel { } } -function createWebSocket (scheme, address) { +function createWebSocket (scheme, address, socketFactory) { const url = scheme + '://' + address.asHostPort() try { - return new WebSocket(url) + 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 ':'. @@ -208,7 +212,7 @@ function createWebSocket (scheme, address) { // That is why here we "catch" SyntaxError and rewrite IPv6 address if needed. const windowsFriendlyUrl = asWindowsFriendlyIPv6Address(scheme, address) - return new WebSocket(windowsFriendlyUrl) + return socketFactory(windowsFriendlyUrl) } else { throw error } diff --git a/src/internal/connection-channel.js b/src/internal/connection-channel.js index 372342931..a4fd927b4 100644 --- a/src/internal/connection-channel.js +++ b/src/internal/connection-channel.js @@ -440,7 +440,7 @@ export default class ChannelConnection extends Connection { * Call close on the channel. * @returns {Promise} - A promise that will be resolved when the underlying channel is closed. */ - close () { + async close () { if (this._log.isDebugEnabled()) { this._log.debug(`${this} closing`) } @@ -451,11 +451,11 @@ export default class ChannelConnection extends Connection { this._protocol.prepareToClose() } - return this._ch.close().then(() => { - if (this._log.isDebugEnabled()) { - this._log.debug(`${this} closed`) - } - }) + await this._ch.close() + + if (this._log.isDebugEnabled()) { + this._log.debug(`${this} closed`) + } } toString () { diff --git a/src/internal/connection-delegate.js b/src/internal/connection-delegate.js index 60beb3f58..ad6a61192 100644 --- a/src/internal/connection-delegate.js +++ b/src/internal/connection-delegate.js @@ -92,6 +92,6 @@ export default class DelegateConnection extends Connection { this._delegate._errorHandler = this._originalErrorHandler } - this._delegate._release() + return this._delegate._release() } } diff --git a/src/internal/connection-provider-pooled.js b/src/internal/connection-provider-pooled.js index 9a0987070..082029723 100644 --- a/src/internal/connection-provider-pooled.js +++ b/src/internal/connection-provider-pooled.js @@ -98,22 +98,16 @@ export default class PooledConnectionProvider extends ConnectionProvider { */ _destroyConnection (conn) { delete this._openConnections[conn.id] - conn.close() + return conn.close() } - close () { - try { - // purge all idle connections in the connection pool - this._connectionPool.purgeAll() - } finally { - // 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 - for (let connectionId in this._openConnections) { - if (this._openConnections.hasOwnProperty(connectionId)) { - this._openConnections[connectionId].close() - } - } - } + 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) { diff --git a/src/internal/connection-provider-routing.js b/src/internal/connection-provider-routing.js index dec06b51f..84255231c 100644 --- a/src/internal/connection-provider-routing.js +++ b/src/internal/connection-provider-routing.js @@ -155,7 +155,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - this._connectionPool.purge(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) { @@ -223,10 +225,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable ) }) - .then(newRoutingTable => { + .then(newRoutingTable => this._applyRoutingTableIfPossible(currentRoutingTable, newRoutingTable) - return newRoutingTable - }) + ) } _fetchRoutingTableFromKnownRoutersFallbackToSeedRouter ( @@ -249,10 +250,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable ) }) - .then(newRoutingTable => { + .then(newRoutingTable => this._applyRoutingTableIfPossible(currentRoutingTable, newRoutingTable) - return newRoutingTable - }) + ) } _fetchRoutingTableUsingKnownRouters (knownRouters, currentRoutingTable) { @@ -381,7 +381,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable) { + async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable) { if (!newRoutingTable) { // none of routing servers returned valid routing table, throw exception throw newError( @@ -396,12 +396,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._useSeedRouter = true } - this._updateRoutingTable(newRoutingTable) + await this._updateRoutingTable(newRoutingTable) + + return newRoutingTable } - _updateRoutingTable (newRoutingTable) { + async _updateRoutingTable (newRoutingTable) { // close old connections to servers not present in the new routing table - this._connectionPool.keepAll(newRoutingTable.allServers()) + await this._connectionPool.keepAll(newRoutingTable.allServers()) // make this driver instance aware of the new table this._routingTables[newRoutingTable.database] = newRoutingTable diff --git a/src/internal/node/node-channel.js b/src/internal/node/node-channel.js index 75a1a3c13..73122f59d 100644 --- a/src/internal/node/node-channel.js +++ b/src/internal/node/node-channel.js @@ -278,6 +278,7 @@ export default class NodeChannel { } _handleConnectionTerminated () { + this._open = false this._error = newError( 'Connection was closed by server', this._connectionErrorCode @@ -338,13 +339,23 @@ export default class NodeChannel { */ 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', () => resolve()) + this._conn.on('end', () => cleanup()) + this._conn.on('close', () => cleanup()) this._conn.end() + this._conn.destroy() } else { - resolve() + cleanup() } }) } diff --git a/src/internal/pool.js b/src/internal/pool.js index 73effd895..7f6d632a3 100644 --- a/src/internal/pool.js +++ b/src/internal/pool.js @@ -23,22 +23,25 @@ import Logger from './logger' class Pool { /** - * @param {function(function): Promise} create an allocation function that creates a promise with a new resource. It's given - * a single argument, 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} destroy called with the resource when it is evicted from this pool - * @param {function} 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} installIdleObserver called when the resource is released back to pool - * @param {function} removeIdleObserver called when the resource is acquired from the 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) => {}, - destroy = conn => true, + create = (address, release) => Promise.resolve(), + destroy = conn => Promise.resolve(), validate = conn => true, installIdleObserver = (conn, observer) => {}, removeIdleObserver = conn => {}, @@ -57,6 +60,7 @@ class Pool { this._activeResourceCounts = {} this._release = this._release.bind(this) this._log = log + this._closed = false } /** @@ -119,27 +123,31 @@ class Pool { /** * 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) { - this._purgeKey(address.asKey()) + return this._purgeKey(address.asKey()) } /** * Destroy all idle resources in this pool. + * @returns {Promise} A promise that is resolved when the resources are purged */ - purgeAll () { - Object.keys(this._pools).forEach(key => this._purgeKey(key)) + close () { + this._closed = true + return 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) - keysToPurge.forEach(key => this._purgeKey(key)) + return Promise.all(keysToPurge.map(key => this._purgeKey(key))) } /** @@ -160,7 +168,11 @@ class Pool { return this._activeResourceCounts[address.asKey()] || 0 } - _acquire (address) { + async _acquire (address) { + if (this._closed) { + throw newError('Pool is closed, it is no more able to serve requests.') + } + const key = address.asKey() let pool = this._pools[key] if (!pool) { @@ -178,19 +190,19 @@ class Pool { // idle resource is valid and can be acquired return Promise.resolve(resource) } else { - this._destroy(resource) + await this._destroy(resource) } } if (this._maxSize && this.activeResourceCount(address) >= this._maxSize) { - return Promise.resolve(null) + return null } // there exist no idle valid resources, create a new one for acquisition - return this._create(address, this._release) + return await this._create(address, this._release) } - _release (address, resource) { + async _release (address, resource) { const key = address.asKey() const pool = this._pools[key] @@ -202,11 +214,8 @@ class Pool { `${resource} destroyed and can't be released to the pool ${key} because it is not functional` ) } - this._destroy(resource) + await this._destroy(resource) } else { - if (this._log.isDebugEnabled()) { - this._log.debug(`${resource} released to the pool ${key}`) - } if (this._installIdleObserver) { this._installIdleObserver(resource, { onError: () => { @@ -214,11 +223,17 @@ class Pool { if (pool) { this._pools[key] = pool.filter(r => r !== resource) } - this._destroy(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 @@ -227,21 +242,21 @@ class Pool { `${resource} destroyed and can't be released to the pool ${key} because pool has been purged` ) } - this._destroy(resource) + await this._destroy(resource) } resourceReleased(key, this._activeResourceCounts) this._processPendingAcquireRequests(address) } - _purgeKey (key) { + async _purgeKey (key) { const pool = this._pools[key] || [] while (pool.length) { const resource = pool.pop() if (this._removeIdleObserver) { this._removeIdleObserver(resource) } - this._destroy(resource) + await this._destroy(resource) } delete this._pools[key] } diff --git a/src/internal/routing-util.js b/src/internal/routing-util.js index de09f8826..5e1e0cdad 100644 --- a/src/internal/routing-util.js +++ b/src/internal/routing-util.js @@ -45,10 +45,7 @@ export default class RoutingUtil { */ callRoutingProcedure (session, database, routerAddress) { return this._callAvailableRoutingProcedure(session, database) - .then(result => { - session.close() - return result.records - }) + .then(result => session.close().then(() => result.records)) .catch(error => { if (error.code === DATABASE_NOT_FOUND_CODE) { throw error diff --git a/src/internal/server-version.js b/src/internal/server-version.js index 1179c31ed..d1ae7d635 100644 --- a/src/internal/server-version.js +++ b/src/internal/server-version.js @@ -47,10 +47,13 @@ class ServerVersion { */ static fromDriver (driver) { const session = driver.session() - return session.run('RETURN 1').then(result => { - session.close() - return ServerVersion.fromString(result.summary.server.version) - }) + return session + .run('RETURN 1') + .then(result => + session + .close() + .then(() => ServerVersion.fromString(result.summary.server.version)) + ) } /** diff --git a/test/bolt-v3.test.js b/test/bolt-v3.test.js index 6ff546eec..a3803f678 100644 --- a/test/bolt-v3.test.js +++ b/test/bolt-v3.test.js @@ -37,27 +37,23 @@ describe('#integration Bolt V3 API', () => { let serverVersion let originalTimeout - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000 - session.run('MATCH (n) DETACH DELETE n').then(result => { - serverVersion = ServerVersion.fromString(result.summary.server.version) - done() - }) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { + afterEach(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - session.close() - driver.close() + await session.close() + await driver.close() }) - it('should set transaction metadata for auto-commit transaction', done => { + it('should set transaction metadata for auto-commit transaction', async () => { if (!databaseSupportsBoltV3()) { - done() return } @@ -68,108 +64,85 @@ describe('#integration Bolt V3 API', () => { } // call listTransactions procedure that should list itself with the specified metadata - session - .run('CALL dbms.listTransactions()', {}, { metadata: metadata }) - .then(result => { - const receivedMetadatas = result.records.map(r => r.get('metaData')) - expect(receivedMetadatas).toContain(metadata) - done() - }) - .catch(error => { - done.fail(error) - }) + const result = await session.run( + 'CALL dbms.listTransactions()', + {}, + { metadata: metadata } + ) + const receivedMetadatas = result.records.map(r => r.get('metaData')) + expect(receivedMetadatas).toContain(metadata) }) - it('should set transaction timeout for auto-commit transaction', done => { + it('should set transaction timeout for auto-commit transaction', async () => { if (!databaseSupportsBoltV3()) { - done() return } - session - .run('CREATE (:Node)') // create a dummy node - .then(() => { - const otherSession = driver.session() - const tx = otherSession.beginTransaction() - tx.run('MATCH (n:Node) SET n.prop = 1') // lock dummy node but keep the transaction open - .then(() => { - // run a query in an auto-commit transaction with timeout and try to update the locked dummy node - session - .run( - 'MATCH (n:Node) SET n.prop = $newValue', - { newValue: 2 }, - { timeout: 1 } - ) - .then(() => done.fail('Failure expected')) - .catch(error => { - expectTransactionTerminatedError(error) - - tx.rollback() - .then(() => otherSession.close()) - .then(() => done()) - .catch(error => done.fail(error)) - }) - }) + await session.run('CREATE (:Node)') // create a dummy node + + const otherSession = driver.session() + const tx = otherSession.beginTransaction() + await tx.run('MATCH (n:Node) SET n.prop = 1') // lock dummy node but keep the transaction open + + // run a query in an auto-commit transaction with timeout and try to update the locked dummy node + await expectAsync( + session.run( + 'MATCH (n:Node) SET n.prop = $newValue', + { newValue: 2 }, + { timeout: 1 } + ) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/transaction has been terminated/) }) - }) + ) - it('should set transaction metadata with read transaction function', done => { - testTransactionMetadataWithTransactionFunctions(true, done) + await tx.rollback() + await otherSession.close() }) - it('should set transaction metadata with write transaction function', done => { - testTransactionMetadataWithTransactionFunctions(false, done) - }) + it('should set transaction metadata with read transaction function', () => + testTransactionMetadataWithTransactionFunctions(true)) + + it('should set transaction metadata with write transaction function', () => + testTransactionMetadataWithTransactionFunctions(false)) - it('should fail auto-commit transaction with metadata when database does not support Bolt V3', done => { + it('should fail auto-commit transaction with metadata when database does not support Bolt V3', () => testAutoCommitTransactionConfigWhenBoltV3NotSupported( - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail auto-commit transaction with timeout when database does not support Bolt V3', done => { + it('should fail auto-commit transaction with timeout when database does not support Bolt V3', () => testAutoCommitTransactionConfigWhenBoltV3NotSupported( - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) - it('should fail read transaction function with metadata when database does not support Bolt V3', done => { + it('should fail read transaction function with metadata when database does not support Bolt V3', () => testTransactionFunctionConfigWhenBoltV3NotSupported( true, - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail read transaction function with timeout when database does not support Bolt V3', done => { + it('should fail read transaction function with timeout when database does not support Bolt V3', () => testTransactionFunctionConfigWhenBoltV3NotSupported( true, - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) - it('should fail write transaction function with metadata when database does not support Bolt V3', done => { + it('should fail write transaction function with metadata when database does not support Bolt V3', () => testTransactionFunctionConfigWhenBoltV3NotSupported( false, - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail write transaction function with timeout when database does not support Bolt V3', done => { + it('should fail write transaction function with timeout when database does not support Bolt V3', () => testTransactionFunctionConfigWhenBoltV3NotSupported( false, - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) - it('should set transaction metadata for explicit transactions', done => { + it('should set transaction metadata for explicit transactions', async () => { if (!databaseSupportsBoltV3()) { - done() return } @@ -180,96 +153,73 @@ describe('#integration Bolt V3 API', () => { } const tx = session.beginTransaction({ metadata: metadata }) + // call listTransactions procedure that should list itself with the specified metadata - tx.run('CALL dbms.listTransactions()') - .then(result => { - const receivedMetadatas = result.records.map(r => r.get('metaData')) - expect(receivedMetadatas).toContain(metadata) - tx.commit() - .then(() => done()) - .catch(error => done.fail(error)) - }) - .catch(error => { - done.fail(error) - }) + const result = await tx.run('CALL dbms.listTransactions()') + const receivedMetadatas = result.records.map(r => r.get('metaData')) + expect(receivedMetadatas).toContain(metadata) + + await tx.commit() }) - it('should set transaction timeout for explicit transactions', done => { + it('should set transaction timeout for explicit transactions', async () => { if (!databaseSupportsBoltV3()) { - done() return } - session - .run('CREATE (:Node)') // create a dummy node - .then(() => { - const otherSession = driver.session() - const otherTx = otherSession.beginTransaction() - otherTx - .run('MATCH (n:Node) SET n.prop = 1') // lock dummy node but keep the transaction open - .then(() => { - // run a query in an explicit transaction with timeout and try to update the locked dummy node - const tx = session.beginTransaction({ timeout: 1 }) - tx.run('MATCH (n:Node) SET n.prop = $newValue', { newValue: 2 }) - .then(() => done.fail('Failure expected')) - .catch(error => { - expectTransactionTerminatedError(error) - - otherTx - .rollback() - .then(() => otherSession.close()) - .then(() => done()) - .catch(error => done.fail(error)) - }) - }) + await session.run('CREATE (:Node)') // create a dummy node + + const otherSession = driver.session() + const otherTx = otherSession.beginTransaction() + await otherTx.run('MATCH (n:Node) SET n.prop = 1') // lock dummy node but keep the transaction open + + // run a query in an explicit transaction with timeout and try to update the locked dummy node + const tx = session.beginTransaction({ timeout: 1 }) + await expectAsync( + tx.run('MATCH (n:Node) SET n.prop = $newValue', { newValue: 2 }) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/transaction has been terminated/) }) + ) + + await otherTx.rollback() + await otherSession.close() }) - it('should fail to run in explicit transaction with metadata when database does not support Bolt V3', done => { + it('should fail to run in explicit transaction with metadata when database does not support Bolt V3', () => testRunInExplicitTransactionWithConfigWhenBoltV3NotSupported( - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail to run in explicit transaction with timeout when database does not support Bolt V3', done => { + it('should fail to run in explicit transaction with timeout when database does not support Bolt V3', () => testRunInExplicitTransactionWithConfigWhenBoltV3NotSupported( - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) - it('should fail to commit explicit transaction with metadata when database does not support Bolt V3', done => { + it('should fail to commit explicit transaction with metadata when database does not support Bolt V3', () => testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported( true, - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail to commit explicit transaction with timeout when database does not support Bolt V3', done => { + it('should fail to commit explicit transaction with timeout when database does not support Bolt V3', () => testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported( true, - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) - it('should fail to rollback explicit transaction with metadata when database does not support Bolt V3', done => { + it('should fail to rollback explicit transaction with metadata when database does not support Bolt V3', () => testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported( false, - TX_CONFIG_WITH_METADATA, - done - ) - }) + TX_CONFIG_WITH_METADATA + )) - it('should fail to rollback explicit transaction with timeout when database does not support Bolt V3', done => { + it('should fail to rollback explicit transaction with timeout when database does not support Bolt V3', () => testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported( false, - TX_CONFIG_WITH_TIMEOUT, - done - ) - }) + TX_CONFIG_WITH_TIMEOUT + )) it('should fail to run auto-commit transaction with invalid timeout', () => { INVALID_TIMEOUT_VALUES.forEach(invalidValue => @@ -343,7 +293,7 @@ describe('#integration Bolt V3 API', () => { ) }) - it('should use bookmarks for auto commit transactions', done => { + it('should use bookmarks for auto commit transactions', async () => { if (!databaseSupportsBoltV3()) { done() return @@ -351,34 +301,29 @@ describe('#integration Bolt V3 API', () => { const initialBookmark = session.lastBookmark() - session.run('CREATE ()').then(() => { - const bookmark1 = session.lastBookmark() - expect(bookmark1).not.toBeNull() - expect(bookmark1).toBeDefined() - expect(bookmark1).not.toEqual(initialBookmark) - - session.run('CREATE ()').then(() => { - const bookmark2 = session.lastBookmark() - expect(bookmark2).not.toBeNull() - expect(bookmark2).toBeDefined() - expect(bookmark2).not.toEqual(initialBookmark) - expect(bookmark2).not.toEqual(bookmark1) - - session.run('CREATE ()').then(() => { - const bookmark3 = session.lastBookmark() - expect(bookmark3).not.toBeNull() - expect(bookmark3).toBeDefined() - expect(bookmark3).not.toEqual(initialBookmark) - expect(bookmark3).not.toEqual(bookmark1) - expect(bookmark3).not.toEqual(bookmark2) - - done() - }) - }) - }) + await session.run('CREATE ()') + const bookmark1 = session.lastBookmark() + expect(bookmark1).not.toBeNull() + expect(bookmark1).toBeDefined() + expect(bookmark1).not.toEqual(initialBookmark) + + await session.run('CREATE ()') + const bookmark2 = session.lastBookmark() + expect(bookmark2).not.toBeNull() + expect(bookmark2).toBeDefined() + expect(bookmark2).not.toEqual(initialBookmark) + expect(bookmark2).not.toEqual(bookmark1) + + await session.run('CREATE ()') + const bookmark3 = session.lastBookmark() + expect(bookmark3).not.toBeNull() + expect(bookmark3).toBeDefined() + expect(bookmark3).not.toEqual(initialBookmark) + expect(bookmark3).not.toEqual(bookmark1) + expect(bookmark3).not.toEqual(bookmark2) }) - it('should use bookmarks for auto commit and explicit transactions', done => { + it('should use bookmarks for auto commit and explicit transactions', async () => { if (!databaseSupportsBoltV3()) { done() return @@ -387,80 +332,62 @@ describe('#integration Bolt V3 API', () => { const initialBookmark = session.lastBookmark() const tx1 = session.beginTransaction() - tx1.run('CREATE ()').then(() => { - tx1.commit().then(() => { - const bookmark1 = session.lastBookmark() - expect(bookmark1).not.toBeNull() - expect(bookmark1).toBeDefined() - expect(bookmark1).not.toEqual(initialBookmark) - - session.run('CREATE ()').then(() => { - const bookmark2 = session.lastBookmark() - expect(bookmark2).not.toBeNull() - expect(bookmark2).toBeDefined() - expect(bookmark2).not.toEqual(initialBookmark) - expect(bookmark2).not.toEqual(bookmark1) - - const tx2 = session.beginTransaction() - tx2.run('CREATE ()').then(() => { - tx2.commit().then(() => { - const bookmark3 = session.lastBookmark() - expect(bookmark3).not.toBeNull() - expect(bookmark3).toBeDefined() - expect(bookmark3).not.toEqual(initialBookmark) - expect(bookmark3).not.toEqual(bookmark1) - expect(bookmark3).not.toEqual(bookmark2) - - done() - }) - }) - }) - }) - }) + await tx1.run('CREATE ()') + await tx1.commit() + const bookmark1 = session.lastBookmark() + expect(bookmark1).not.toBeNull() + expect(bookmark1).toBeDefined() + expect(bookmark1).not.toEqual(initialBookmark) + + await session.run('CREATE ()') + const bookmark2 = session.lastBookmark() + expect(bookmark2).not.toBeNull() + expect(bookmark2).toBeDefined() + expect(bookmark2).not.toEqual(initialBookmark) + expect(bookmark2).not.toEqual(bookmark1) + + const tx2 = session.beginTransaction() + await tx2.run('CREATE ()') + await tx2.commit() + const bookmark3 = session.lastBookmark() + expect(bookmark3).not.toBeNull() + expect(bookmark3).toBeDefined() + expect(bookmark3).not.toEqual(initialBookmark) + expect(bookmark3).not.toEqual(bookmark1) + expect(bookmark3).not.toEqual(bookmark2) }) - it('should use bookmarks for auto commit transactions and transaction functions', done => { + it('should use bookmarks for auto commit transactions and transaction functions', async () => { if (!databaseSupportsBoltV3()) { - done() return } const initialBookmark = session.lastBookmark() - session - .writeTransaction(tx => tx.run('CREATE ()')) - .then(() => { - const bookmark1 = session.lastBookmark() - expect(bookmark1).not.toBeNull() - expect(bookmark1).toBeDefined() - expect(bookmark1).not.toEqual(initialBookmark) - - session.run('CREATE ()').then(() => { - const bookmark2 = session.lastBookmark() - expect(bookmark2).not.toBeNull() - expect(bookmark2).toBeDefined() - expect(bookmark2).not.toEqual(initialBookmark) - expect(bookmark2).not.toEqual(bookmark1) - - session - .writeTransaction(tx => tx.run('CREATE ()')) - .then(() => { - const bookmark3 = session.lastBookmark() - expect(bookmark3).not.toBeNull() - expect(bookmark3).toBeDefined() - expect(bookmark3).not.toEqual(initialBookmark) - expect(bookmark3).not.toEqual(bookmark1) - expect(bookmark3).not.toEqual(bookmark2) - - done() - }) - }) - }) + await session.writeTransaction(tx => tx.run('CREATE ()')) + const bookmark1 = session.lastBookmark() + expect(bookmark1).not.toBeNull() + expect(bookmark1).toBeDefined() + expect(bookmark1).not.toEqual(initialBookmark) + + await session.run('CREATE ()') + const bookmark2 = session.lastBookmark() + expect(bookmark2).not.toBeNull() + expect(bookmark2).toBeDefined() + expect(bookmark2).not.toEqual(initialBookmark) + expect(bookmark2).not.toEqual(bookmark1) + + await session.writeTransaction(tx => tx.run('CREATE ()')) + const bookmark3 = session.lastBookmark() + expect(bookmark3).not.toBeNull() + expect(bookmark3).toBeDefined() + expect(bookmark3).not.toEqual(initialBookmark) + expect(bookmark3).not.toEqual(bookmark1) + expect(bookmark3).not.toEqual(bookmark2) }) - function testTransactionMetadataWithTransactionFunctions (read, done) { + async function testTransactionMetadataWithTransactionFunctions (read) { if (!databaseSupportsBoltV3()) { - done() return } @@ -474,42 +401,36 @@ describe('#integration Bolt V3 API', () => { ? session.readTransaction(work, { metadata: metadata }) : session.writeTransaction(work, { metadata: metadata }) - txFunctionWithMetadata(tx => tx.run('CALL dbms.listTransactions()')) - .then(result => { - const receivedMetadatas = result.records.map(r => r.get('metaData')) - expect(receivedMetadatas).toContain(metadata) - done() - }) - .catch(error => { - done.fail(error) - }) + const result = await txFunctionWithMetadata(tx => + tx.run('CALL dbms.listTransactions()') + ) + const receivedMetadatas = result.records.map(r => r.get('metaData')) + expect(receivedMetadatas).toContain(metadata) } - function testAutoCommitTransactionConfigWhenBoltV3NotSupported ( - txConfig, - done + async function testAutoCommitTransactionConfigWhenBoltV3NotSupported ( + txConfig ) { if (databaseSupportsBoltV3()) { - done() return } - session - .run('RETURN $x', { x: 42 }, txConfig) - .then(() => done.fail('Failure expected')) - .catch(error => { - expectBoltV3NotSupportedError(error) - done() + await expectAsync( + session.run('RETURN $x', { x: 42 }, txConfig) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /Driver is connected to the database that does not support transaction configuration/ + ) }) + ) } - function testTransactionFunctionConfigWhenBoltV3NotSupported ( + async function testTransactionFunctionConfigWhenBoltV3NotSupported ( read, - txConfig, - done + txConfig ) { if (databaseSupportsBoltV3()) { - done() return } @@ -518,71 +439,52 @@ describe('#integration Bolt V3 API', () => { ? session.readTransaction(work, txConfig) : session.writeTransaction(work, txConfig) - txFunctionWithMetadata(tx => tx.run('RETURN 42')) - .then(() => done.fail('Failure expected')) - .catch(error => { - expectBoltV3NotSupportedError(error) - done() + await expectAsync( + txFunctionWithMetadata(tx => tx.run('RETURN 42')) + ).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /Driver is connected to the database that does not support transaction configuration/ + ) }) + ) } - function testRunInExplicitTransactionWithConfigWhenBoltV3NotSupported ( - txConfig, - done + async function testRunInExplicitTransactionWithConfigWhenBoltV3NotSupported ( + txConfig ) { if (databaseSupportsBoltV3()) { - done() return } const tx = session.beginTransaction(txConfig) - tx.run('RETURN 42') - .then(() => done.fail('Failure expected')) - .catch(error => { - expectBoltV3NotSupportedError(error) - session.close() - done() + + await expectAsync(tx.run('RETURN 42')).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /Driver is connected to the database that does not support transaction configuration/ + ) }) + ) } - function testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported ( + async function testCloseExplicitTransactionWithConfigWhenBoltV3NotSupported ( commit, - txConfig, - done + txConfig ) { if (databaseSupportsBoltV3()) { - done() return } const tx = session.beginTransaction(txConfig) - const promise = commit ? tx.commit() : tx.rollback() - - promise - .then(() => done.fail('Failure expected')) - .catch(error => { - expectBoltV3NotSupportedError(error) - session.close() - done() - }) - } - function expectBoltV3NotSupportedError (error) { - expect( - error.message.indexOf( - 'Driver is connected to the database that does not support transaction configuration' - ) - ).toBeGreaterThan(-1) - } - - function expectTransactionTerminatedError (error) { - const hasExpectedMessage = - error.message.toLowerCase().indexOf('transaction has been terminated') > - -1 - if (!hasExpectedMessage) { - console.log(`Unexpected error with code: ${error.code}`, error) - } - expect(hasExpectedMessage).toBeTruthy() + await expectAsync(commit ? tx.commit() : tx.rollback()).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching( + /Driver is connected to the database that does not support transaction configuration/ + ) + }) + ) } function databaseSupportsBoltV3 () { diff --git a/test/bolt-v4.test.js b/test/bolt-v4.test.js index b76e196b7..d9958e344 100644 --- a/test/bolt-v4.test.js +++ b/test/bolt-v4.test.js @@ -27,22 +27,19 @@ describe('#integration Bolt V4 API', () => { let serverVersion let originalTimeout - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000 - session.run('MATCH (n) DETACH DELETE n').then(result => { - serverVersion = ServerVersion.fromString(result.summary.server.version) - done() - }) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { + afterEach(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - session.close() - driver.close() + await session.close() + await driver.close() }) describe('multi-database', () => { @@ -60,7 +57,6 @@ describe('#integration Bolt V4 API', () => { .then(() => done.fail('Failure expected')) .catch(error => { expectBoltV4NotSupportedError(error) - session.close() done() }) }) @@ -78,7 +74,6 @@ describe('#integration Bolt V4 API', () => { .then(() => done.fail('Failure expected')) .catch(error => { expectBoltV4NotSupportedError(error) - session.close() done() }) }) @@ -96,7 +91,6 @@ describe('#integration Bolt V4 API', () => { .then(() => done.fail('Failure expected')) .catch(error => { expectBoltV4NotSupportedError(error) - session.close() done() }) }) @@ -114,7 +108,6 @@ describe('#integration Bolt V4 API', () => { .then(() => done.fail('Failure expected')) .catch(error => { expectBoltV4NotSupportedError(error) - session.close() done() }) }) @@ -125,15 +118,10 @@ describe('#integration Bolt V4 API', () => { } const session = driver.session() + const result = await session.readTransaction(tx => tx.run('RETURN 1')) - try { - const result = await session.readTransaction(tx => tx.run('RETURN 1')) - - expect(result.summary.database).toBeTruthy() - expect(result.summary.database.name).toBeNull() - } finally { - session.close() - } + expect(result.summary.database).toBeTruthy() + expect(result.summary.database.name).toBeNull() }) }) @@ -151,7 +139,7 @@ describe('#integration Bolt V4 API', () => { } catch (error) { expect(error.code).toContain('DatabaseNotFound') } finally { - neoSession.close() + await neoSession.close() } }) @@ -182,7 +170,7 @@ describe('#integration Bolt V4 API', () => { expect(result.records.length).toBe(1) expect(result.records[0].get('n.name')).toBe('neo4j') } finally { - neoSession.close() + await neoSession.close() } }) @@ -206,8 +194,8 @@ describe('#integration Bolt V4 API', () => { expect(result.records.length).toBe(1) expect(result.records[0].get('n.name')).toBe('neo4j') } finally { - neoSession.close() - neoDriver.close() + await neoSession.close() + await neoDriver.close() } }) }) @@ -230,7 +218,7 @@ describe('#integration Bolt V4 API', () => { expect(result.summary.database).toBeTruthy() expect(result.summary.database.name).toBe(database || 'neo4j') } finally { - neoSession.close() + await neoSession.close() } } diff --git a/test/driver.test.js b/test/driver.test.js index 35595d0ad..478de8d59 100644 --- a/test/driver.test.js +++ b/test/driver.test.js @@ -31,18 +31,15 @@ describe('#integration driver', () => { let driver let serverVersion - beforeAll(done => { + beforeAll(async () => { const tmpDriver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) - ServerVersion.fromDriver(tmpDriver).then(version => { - tmpDriver.close() - serverVersion = version - done() - }) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(tmpDriver) + await tmpDriver.close() }) - afterEach(() => { + afterEach(async () => { if (driver) { - driver.close() + await driver.close() driver = null } }) @@ -56,7 +53,6 @@ describe('#integration driver', () => { // Then expect(session).not.toBeNull() - driver.close() }) it('should handle connection errors', async () => { @@ -64,15 +60,14 @@ describe('#integration driver', () => { driver = neo4j.driver('bolt://local-host', sharedNeo4j.authToken) const session = driver.session() const txc = session.beginTransaction() - try { - await txc.run('RETURN 1') - expect(true).toBeFalsy('exception expected') - } catch (error) { - expect(error.message).not.toBeNull() - expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - } finally { - session.close() - } + + await expectAsync(txc.run('RETURN 1')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE + }) + ) + + await session.close() }, 10000) it('should fail with correct error message when connecting to port 80', done => { @@ -130,15 +125,14 @@ describe('#integration driver', () => { driver = neo4j.driver('bolt://localhost', wrongCredentials()) const session = driver.session() const txc = session.beginTransaction() - try { - await txc.run('RETURN 1') - expect(true).toBeFalsy('exception expected') - } catch (error) { - expect(error.message).not.toBeNull() - expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized') - } finally { - session.close() - } + + await expectAsync(txc.run('RETURN 1')).toBeRejectedWith( + jasmine.objectContaining({ + code: 'Neo.ClientError.Security.Unauthorized' + }) + ) + + await session.close() }) it('should fail queries on wrong credentials', done => { @@ -223,27 +217,25 @@ describe('#integration driver', () => { // Given driver = neo4j.driver('neo4j://localhost', sharedNeo4j.authToken) const session = driver.session() - try { - await session.run('RETURN 1') - expect(true).toBeFalsy('exception expected') - } catch (error) { - expect(error.message).toContain( - 'Could not perform discovery. No routing servers available.' - ) - expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - } finally { - session.close() - } + + await expectAsync(session.run('RETURN 1')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE, + message: jasmine.stringMatching(/No routing servers available/) + }) + ) + + await session.close() }) - it('should have correct user agent', () => { + it('should have correct user agent', async () => { const directDriver = neo4j.driver('bolt://localhost') expect(directDriver._userAgent).toBe('neo4j-javascript/0.0.0-dev') - directDriver.close() + await directDriver.close() const routingDriver = neo4j.driver('neo4j://localhost') expect(routingDriver._userAgent).toBe('neo4j-javascript/0.0.0-dev') - routingDriver.close() + await routingDriver.close() }) it('should fail when bolt:// scheme used with routing params', () => { @@ -252,42 +244,38 @@ describe('#integration driver', () => { ).toThrow() }) - it('should sanitize pool setting values in the config', () => { - testConfigSanitizing('maxConnectionLifetime', 60 * 60 * 1000) - testConfigSanitizing('maxConnectionPoolSize', DEFAULT_MAX_SIZE) - testConfigSanitizing( + it('should sanitize pool setting values in the config', async () => { + await testConfigSanitizing('maxConnectionLifetime', 60 * 60 * 1000) + await testConfigSanitizing('maxConnectionPoolSize', DEFAULT_MAX_SIZE) + await testConfigSanitizing( 'connectionAcquisitionTimeout', DEFAULT_ACQUISITION_TIMEOUT ) }) - it('should discard closed connections', done => { + it('should discard closed connections', async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) const session1 = driver.session() - session1.run('CREATE () RETURN 42').then(() => { - session1.close() - - // one connection should be established - const connections1 = openConnectionFrom(driver) - expect(connections1.length).toEqual(1) + await session1.run('CREATE () RETURN 42') + await session1.close() - // close/break existing pooled connection - connections1.forEach(connection => connection.close()) + // one connection should be established + const connections1 = openConnectionFrom(driver) + expect(connections1.length).toEqual(1) - const session2 = driver.session() - session2.run('RETURN 1').then(() => { - session2.close() + // close/break existing pooled connection + await Promise.all(connections1.map(connection => connection.close())) - // existing connection should be disposed and new one should be created - const connections2 = openConnectionFrom(driver) - expect(connections2.length).toEqual(1) + const session2 = driver.session() + await session2.run('RETURN 1') + await session2.close() - expect(connections1[0]).not.toEqual(connections2[0]) + // existing connection should be disposed and new one should be created + const connections2 = openConnectionFrom(driver) + expect(connections2.length).toEqual(1) - done() - }) - }) + expect(connections1[0]).not.toEqual(connections2[0]) }) it('should discard old connections', async () => { @@ -298,7 +286,7 @@ describe('#integration driver', () => { const session1 = driver.session() await session1.run('CREATE () RETURN 42') - session1.close() + await session1.close() // one connection should be established const connections1 = openConnectionFrom(driver) @@ -312,7 +300,7 @@ describe('#integration driver', () => { const session2 = driver.session() await session2.run('RETURN 1') - session2.close() + await session2.close() // old connection should be disposed and new one should be created const connections2 = openConnectionFrom(driver) @@ -399,11 +387,13 @@ describe('#integration driver', () => { driver = neo4j.driver(url, sharedNeo4j.authToken) const session = driver.session() - session.run('RETURN 42').then(result => { - expect(result.records[0].get(0).toNumber()).toEqual(42) - session.close() - done() - }) + session + .run('RETURN 42') + .then(result => { + expect(result.records[0].get(0).toNumber()).toEqual(42) + }) + .then(() => session.close()) + .then(() => done()) } function testNumberInReturnedRecord (inputNumber, expectedNumber, done) { @@ -415,8 +405,6 @@ describe('#integration driver', () => { session .run('RETURN $number AS n0, $number AS n1', { number: inputNumber }) .then(result => { - session.close() - const records = result.records expect(records.length).toEqual(1) const record = records[0] @@ -431,9 +419,9 @@ describe('#integration driver', () => { n0: expectedNumber, n1: expectedNumber }) - - done() }) + .then(() => session.close()) + .then(() => done()) } /** @@ -449,16 +437,23 @@ describe('#integration driver', () => { return neo4j.auth.basic('neo4j', 'who would use such a password') } - function testConfigSanitizing (configProperty, defaultValue) { - validateConfigSanitizing({}, defaultValue) - validateConfigSanitizing({ [configProperty]: 42 }, 42) - validateConfigSanitizing({ [configProperty]: 0 }, 0) - validateConfigSanitizing({ [configProperty]: '42' }, 42) - validateConfigSanitizing({ [configProperty]: '042' }, 42) - validateConfigSanitizing({ [configProperty]: -42 }, Number.MAX_SAFE_INTEGER) + async function testConfigSanitizing (configProperty, defaultValue) { + await validateConfigSanitizing({}, defaultValue) + await validateConfigSanitizing({ [configProperty]: 42 }, 42) + await validateConfigSanitizing({ [configProperty]: 0 }, 0) + await validateConfigSanitizing({ [configProperty]: '42' }, 42) + await validateConfigSanitizing({ [configProperty]: '042' }, 42) + await validateConfigSanitizing( + { [configProperty]: -42 }, + Number.MAX_SAFE_INTEGER + ) } - function validateConfigSanitizing (config, configProperty, expectedValue) { + async function validateConfigSanitizing ( + config, + configProperty, + expectedValue + ) { const driver = neo4j.driver( 'bolt://localhost', sharedNeo4j.authToken, @@ -467,7 +462,7 @@ describe('#integration driver', () => { try { expect(driver._config[configProperty]).toEqual(expectedValue) } finally { - driver.close() + await driver.close() } } diff --git a/test/examples.test.js b/test/examples.test.js index 607bad217..4ffcc19e4 100644 --- a/test/examples.test.js +++ b/test/examples.test.js @@ -57,18 +57,12 @@ describe('#integration examples', () => { }) consoleOverride = { log: msg => consoleOverridePromiseResolve(msg) } - const session = driverGlobal.session() - try { - const result = await session.run('MATCH (n) DETACH DELETE n') - version = ServerVersion.fromString(result.summary.server.version) - } finally { - await session.close() - } + version = await sharedNeo4j.cleanupAndGetVersion(driverGlobal) }) - afterAll(() => { + afterAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - driverGlobal.close() + await driverGlobal.close() }) it('autocommit transaction example', async () => { @@ -104,18 +98,16 @@ describe('#integration examples', () => { } }) - it('basic auth example', done => { + it('basic auth example', async () => { // tag::basic-auth[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)) // end::basic-auth[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('config connection pool example', done => { + it('config connection pool example', async () => { // tag::config-connection-pool[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { maxConnectionLifetime: 3 * 60 * 60 * 1000, // 3 hours @@ -124,26 +116,22 @@ describe('#integration examples', () => { }) // end::config-connection-pool[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('config connection timeout example', done => { + it('config connection timeout example', async () => { // tag::config-connection-timeout[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { connectionTimeout: 20 * 1000 // 20 seconds }) // end::config-connection-timeout[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('config max retry time example', done => { + it('config max retry time example', async () => { // tag::config-max-retry-time[] const maxRetryTimeMs = 15 * 1000 // 15 seconds const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { @@ -151,13 +139,11 @@ describe('#integration examples', () => { }) // end::config-max-retry-time[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('config trust example', done => { + it('config trust example', async () => { if (version.compareTo(VERSION_4_0_0) >= 0) { pending('address within security work') } @@ -169,23 +155,19 @@ describe('#integration examples', () => { }) // end::config-trust[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('config unencrypted example', done => { + it('config unencrypted example', async () => { // tag::config-unencrypted[] const driver = neo4j.driver(uri, neo4j.auth.basic(user, password), { encrypted: 'ENCRYPTION_OFF' }) // end::config-unencrypted[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) /* eslint-disable no-unused-vars */ @@ -207,10 +189,8 @@ describe('#integration examples', () => { session .run('CREATE (n:Person { name: $name })', { name: name }) - .then(() => { - session.close() - driver.close() - }) + .then(() => session.close()) + .then(() => driver.close()) } // end::config-custom-resolver[] @@ -218,7 +198,7 @@ describe('#integration examples', () => { }) /* eslint-enable no-unused-vars */ - it('custom auth example', done => { + it('custom auth example', async () => { const principal = user const credentials = password const realm = undefined @@ -232,20 +212,18 @@ describe('#integration examples', () => { ) // end::custom-auth[] - driver.verifyConnectivity().then(() => { - driver.close() - done() - }) + await driver.verifyConnectivity() + await driver.close() }) - it('kerberos auth example', () => { + it('kerberos auth example', async () => { const ticket = 'a base64 encoded ticket' // tag::kerberos-auth[] const driver = neo4j.driver(uri, neo4j.auth.kerberos(ticket)) // end::kerberos-auth[] - driver.close() + await driver.close() }) it('cypher error example', async () => { @@ -302,7 +280,7 @@ describe('#integration examples', () => { } // ... on application exit: - driver.close() + await driver.close() // end::driver-lifecycle[] expect(await consoleLoggedMsg).toEqual('Driver created') @@ -332,7 +310,7 @@ describe('#integration examples', () => { } // on application exit: - driver.close() + await driver.close() // end::hello-world[] expect(await consoleLoggedMsg).toContain('hello, world, from node') @@ -367,7 +345,7 @@ describe('#integration examples', () => { } // on application exit: - driver.close() + await driver.close() // end::language-guide-page[] expect(await consoleLoggedMsg).toEqual(personName) @@ -429,9 +407,9 @@ describe('#integration examples', () => { collectedNames.push(name) }, onCompleted: () => { - session.close() - - console.log('Names: ' + collectedNames.join(', ')) + session.close().then(() => { + console.log('Names: ' + collectedNames.join(', ')) + }) }, onError: error => { console.log(error) @@ -515,13 +493,14 @@ describe('#integration examples', () => { }) // end::service-unavailable[] - consoleLoggedMsg.then(loggedMsg => { - driver.close() - expect(loggedMsg).toBe( - 'Unable to create node: ' + neo4j.error.SERVICE_UNAVAILABLE - ) - done() - }) + consoleLoggedMsg + .then(loggedMsg => { + expect(loggedMsg).toBe( + 'Unable to create node: ' + neo4j.error.SERVICE_UNAVAILABLE + ) + }) + .then(() => driver.close()) + .then(() => done()) }) it('session example', async () => { @@ -635,9 +614,8 @@ describe('#integration examples', () => { ) .then(() => { savedBookmarks.push(session1.lastBookmark()) - - return session1.close() }) + .then(() => session1.close()) // Create the second person and employment relationship. const session2 = driver.session({ defaultAccessMode: neo4j.WRITE }) @@ -649,12 +627,11 @@ describe('#integration examples', () => { ) .then(() => { savedBookmarks.push(session2.lastBookmark()) - - return session2.close() }) + .then(() => session2.close()) // Create a friendship between the two people created above. - const last = Promise.all([first, second]).then(ignore => { + const last = Promise.all([first, second]).then(() => { const session3 = driver.session({ defaultAccessMode: neo4j.WRITE, bookmarks: savedBookmarks diff --git a/test/internal/bolt-stub.js b/test/internal/bolt-stub.js index be20ac4ee..82117e461 100644 --- a/test/internal/bolt-stub.js +++ b/test/internal/bolt-stub.js @@ -30,10 +30,6 @@ class UnsupportedBoltStub { 'BoltStub: unable to start with template, unavailable on this platform' ) } - - run (callback) { - throw new Error('BoltStub: unable to run, unavailable on this platform') - } } const verbose = @@ -46,6 +42,7 @@ class SupportedBoltStub extends UnsupportedBoltStub { this._mustache = require('mustache') this._fs = require('fs') this._tmp = require('tmp') + this._net = require('net') } static create () { @@ -71,8 +68,10 @@ class SupportedBoltStub extends UnsupportedBoltStub { }) } + let exited = false let exitCode = -1 boltStub.on('close', code => { + exited = true exitCode = code }) @@ -80,7 +79,34 @@ class SupportedBoltStub extends UnsupportedBoltStub { console.warn('Failed to start child process:' + error) }) - return new StubServer(() => exitCode) + return new Promise((resolve, reject) => { + let timedOut = false + const timeoutId = setTimeout(() => { + timedOut = true + reject(`unable to connect to localhost:${port}`) + }, 15000) + + const tryConnect = () => { + const client = this._net.createConnection({ port }, () => { + clearTimeout(timeoutId) + resolve( + new StubServer(() => { + return { + exited: exited, + code: exitCode + } + }) + ) + }) + client.on('error', () => { + if (!timedOut) { + setTimeout(tryConnect, 200) + } + }) + } + + tryConnect() + }) } startWithTemplate (scriptTemplate, parameters, port) { @@ -90,24 +116,41 @@ class SupportedBoltStub extends UnsupportedBoltStub { this._fs.writeFileSync(script, scriptContents, 'utf-8') return this.start(script, port) } - - run (callback) { - // wait to make sure boltstub is started before running user code - setTimeout(callback, 1000) - } } class StubServer { - constructor (exitCodeSupplier) { - this._exitCodeSupplier = exitCodeSupplier + constructor (exitStatusSupplier) { + this._exitStatusSupplier = exitStatusSupplier this.exit.bind(this) } - exit (callback) { - // give process some time to exit - setTimeout(() => { - callback(this._exitCodeSupplier()) - }, 1000) + exit () { + return new Promise((resolve, reject) => { + let timedOut = false + const timeoutId = setTimeout(() => { + timedOut = true + reject('timed out waiting for the stub server to exit') + }, 5000) + + const checkStatus = () => { + const exitStatus = this._exitStatusSupplier() + if (exitStatus.exited) { + clearTimeout(timeoutId) + + if (exitStatus.code === 0) { + resolve() + } else { + reject(`stub server exited with code: ${exitCode}`) + } + } else { + if (!timedOut) { + setTimeout(() => checkStatus(), 500) + } + } + } + + checkStatus() + }) } } @@ -133,6 +176,5 @@ export default { supported: supported, start: stub.start.bind(stub), startWithTemplate: stub.startWithTemplate.bind(stub), - run: stub.run.bind(stub), newDriver: newDriver } diff --git a/test/internal/browser/browser-channel.test.js b/test/internal/browser/browser-channel.test.js index 23101591e..18e56a0ce 100644 --- a/test/internal/browser/browser-channel.test.js +++ b/test/internal/browser/browser-channel.test.js @@ -23,6 +23,7 @@ import { SERVICE_UNAVAILABLE } from '../../../src/error' import { setTimeoutMock } from '../timers-util' import { ENCRYPTION_OFF, ENCRYPTION_ON } from '../../../src/internal/util' import ServerAddress from '../../../src/internal/server-address' +import { read } from 'fs' const WS_CONNECTING = 0 const WS_OPEN = 1 @@ -31,24 +32,12 @@ const WS_CLOSED = 3 /* eslint-disable no-global-assign */ describe('#unit WebSocketChannel', () => { - let OriginalWebSocket let webSocketChannel - let originalConsoleWarn - beforeEach(() => { - OriginalWebSocket = WebSocket - originalConsoleWarn = console.warn - console.warn = () => { - // mute by default - } - }) - - afterEach(() => { - WebSocket = OriginalWebSocket + afterEach(async () => { if (webSocketChannel) { - webSocketChannel.close() + await webSocketChannel.close() } - console.warn = originalConsoleWarn }) it('should fallback to literal IPv6 when SyntaxError is thrown', () => { @@ -65,23 +54,12 @@ describe('#unit WebSocketChannel', () => { ) }) - it('should clear connection timeout when closed', () => { + it('should clear connection timeout when closed', async () => { const fakeSetTimeout = setTimeoutMock.install() try { // do not execute setTimeout callbacks fakeSetTimeout.pause() - let fakeWebSocketClosed = false - - // replace real WebSocket with a function that does nothing - WebSocket = () => { - return { - close: () => { - fakeWebSocketClosed = true - } - } - } - const address = ServerAddress.fromUrl('bolt://localhost:8989') const driverConfig = { connectionTimeout: 4242 } const channelConfig = new ChannelConfig( @@ -90,15 +68,19 @@ describe('#unit WebSocketChannel', () => { SERVICE_UNAVAILABLE ) - webSocketChannel = new WebSocketChannel(channelConfig) + webSocketChannel = new WebSocketChannel( + channelConfig, + undefined, + createWebSocketFactory(WS_OPEN) + ) - expect(fakeWebSocketClosed).toBeFalsy() + expect(webSocketChannel._ws.readyState).toBe(WS_OPEN) expect(fakeSetTimeout.invocationDelays).toEqual([]) expect(fakeSetTimeout.clearedTimeouts).toEqual([]) - webSocketChannel.close() + await webSocketChannel.close() - expect(fakeWebSocketClosed).toBeTruthy() + expect(webSocketChannel._ws.readyState).toBe(WS_CLOSED) expect(fakeSetTimeout.invocationDelays).toEqual([]) expect(fakeSetTimeout.clearedTimeouts).toEqual([0]) // cleared one timeout with id 0 } finally { @@ -132,13 +114,6 @@ describe('#unit WebSocketChannel', () => { it('should fail when encryption configured with unsupported trust strategy', () => { const protocolSupplier = () => 'http:' - - WebSocket = () => { - return { - close: () => {} - } - } - const address = ServerAddress.fromUrl('bolt://localhost:8989') const driverConfig = { encrypted: true, trust: 'TRUST_ALL_CERTIFICATES' } const channelConfig = new ChannelConfig( @@ -147,7 +122,11 @@ describe('#unit WebSocketChannel', () => { SERVICE_UNAVAILABLE ) - const channel = new WebSocketChannel(channelConfig, protocolSupplier) + const channel = new WebSocketChannel( + channelConfig, + protocolSupplier, + createWebSocketFactory(WS_CONNECTING) + ) expect(channel._error).toBeDefined() expect(channel._error.name).toEqual('Neo4jError') @@ -163,54 +142,39 @@ describe('#unit WebSocketChannel', () => { testWarningInMixedEnvironment(ENCRYPTION_OFF, 'https') }) - it('should resolve close if websocket is already closed', () => { - WebSocket = () => { - return { - readyState: WS_CLOSED - } - } - + it('should resolve close if websocket is already closed', async () => { const address = ServerAddress.fromUrl('bolt://localhost:8989') const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) + const channel = new WebSocketChannel( + channelConfig, + undefined, + createWebSocketFactory(WS_CLOSED) + ) - const channel = new WebSocketChannel(channelConfig) - - return expectAsync(channel.close()).toBeResolved() + await expectAsync(channel.close()).toBeResolved() }) - it('should resolve close when websocket is closed', () => { - WebSocket = () => { - const ws = { - readyState: WS_OPEN, - onclose: () => {} - } - - ws.close = () => { - ws.readyState = WS_CLOSED - ws.onclose() - } - - return ws - } - + it('should resolve close when websocket is closed', async () => { const address = ServerAddress.fromUrl('bolt://localhost:8989') const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) - const channel = new WebSocketChannel(channelConfig) + const channel = new WebSocketChannel( + channelConfig, + undefined, + createWebSocketFactory(WS_OPEN) + ) - return expectAsync(channel.close()).toBeResolved() + await expectAsync(channel.close()).toBeResolved() }) function testFallbackToLiteralIPv6 (boltAddress, expectedWsAddress) { // replace real WebSocket with a function that throws when IPv6 address is used - WebSocket = url => { + const socketFactory = url => { if (url.indexOf('[') !== -1) { throw new SyntaxError() } - return { - url: url, - close: () => {} - } + const fakeFactory = createWebSocketFactory(WS_OPEN) + return fakeFactory(url) } const address = ServerAddress.fromUrl(boltAddress) @@ -222,7 +186,11 @@ describe('#unit WebSocketChannel', () => { SERVICE_UNAVAILABLE ) - webSocketChannel = new WebSocketChannel(channelConfig) + webSocketChannel = new WebSocketChannel( + channelConfig, + undefined, + socketFactory + ) expect(webSocketChannel._ws.url).toEqual(expectedWsAddress) } @@ -233,50 +201,63 @@ describe('#unit WebSocketChannel', () => { expectedScheme ) { const protocolSupplier = () => windowLocationProtocol - - // replace real WebSocket with a function that memorizes the url - WebSocket = url => { - return { - url: url, - close: () => {} - } - } - const address = ServerAddress.fromUrl('bolt://localhost:8989') const channelConfig = new ChannelConfig( address, driverConfig, SERVICE_UNAVAILABLE ) - const channel = new WebSocketChannel(channelConfig, protocolSupplier) + const channel = new WebSocketChannel( + channelConfig, + protocolSupplier, + createWebSocketFactory(WS_OPEN) + ) expect(channel._ws.url).toEqual(expectedScheme + '://localhost:8989') } function testWarningInMixedEnvironment (encrypted, scheme) { - // replace real WebSocket with a function that memorizes the url - WebSocket = url => { - return { - url: url, - close: () => {} - } - } + const originalConsoleWarn = console.warn + try { + // replace console.warn with a function that memorizes the message + const warnMessages = [] + console.warn = message => warnMessages.push(message) - // replace console.warn with a function that memorizes the message - const warnMessages = [] - console.warn = message => warnMessages.push(message) + const address = ServerAddress.fromUrl('bolt://localhost:8989') + const config = new ChannelConfig( + address, + { encrypted: encrypted }, + SERVICE_UNAVAILABLE + ) + const protocolSupplier = () => scheme + ':' - const address = ServerAddress.fromUrl('bolt://localhost:8989') - const config = new ChannelConfig( - address, - { encrypted: encrypted }, - SERVICE_UNAVAILABLE - ) - const protocolSupplier = () => scheme + ':' + const channel = new WebSocketChannel( + config, + protocolSupplier, + createWebSocketFactory(WS_OPEN) + ) - const channel = new WebSocketChannel(config, protocolSupplier) + expect(channel).toBeDefined() + expect(warnMessages.length).toEqual(1) + } finally { + console.warn = originalConsoleWarn + } + } - expect(channel).toBeDefined() - expect(warnMessages.length).toEqual(1) + function createWebSocketFactory (readyState) { + const ws = {} + + ws.readyState = readyState + ws.close = () => { + ws.readyState = WS_CLOSED + if (ws.onclose && typeof ws.onclose === 'function') { + ws.onclose({ wasClean: true }) + } + } + + return url => { + ws.url = url + return ws + } } }) diff --git a/test/internal/connection-channel.test.js b/test/internal/connection-channel.test.js index 26cee3c52..f2877d229 100644 --- a/test/internal/connection-channel.test.js +++ b/test/internal/connection-channel.test.js @@ -45,13 +45,12 @@ describe('#integration ChannelConnection', () => { /** @type {Connection} */ let connection - afterEach(done => { + afterEach(async () => { const usedConnection = connection connection = null if (usedConnection) { - usedConnection.close() + await usedConnection.close() } - done() }) it('should have correct creation timestamp', () => { @@ -398,49 +397,41 @@ describe('#integration ChannelConnection', () => { connection._handleFatalError(newError('Hello', SERVICE_UNAVAILABLE)) }) - it('should send INIT/HELLO and GOODBYE messages', done => { + it('should send INIT/HELLO and GOODBYE messages', async () => { const messages = [] connection = createConnection('bolt://localhost') recordWrittenMessages(connection, messages) - connection - .connect('mydriver/0.0.0', basicAuthToken()) - .then(() => { - expect(connection.isOpen()).toBeTruthy() - connection.close(() => { - expect(messages.length).toBeGreaterThan(0) - expect(messages[0].signature).toEqual(0x01) // first message is either INIT or HELLO - - const serverVersion = ServerVersion.fromString(connection.version) - if (serverVersion.compareTo(VERSION_3_5_0) >= 0) { - expect(messages[messages.length - 1].signature).toEqual(0x02) // last message is GOODBYE in V3 - } - done() - }) - }) - .catch(done.fail) + await connection.connect('mydriver/0.0.0', basicAuthToken()) + + expect(connection.isOpen()).toBeTruthy() + await connection.close() + + expect(messages.length).toBeGreaterThan(0) + expect(messages[0].signature).toEqual(0x01) // first message is either INIT or HELLO + + const serverVersion = ServerVersion.fromString(connection.version) + if (serverVersion.compareTo(VERSION_3_5_0) >= 0) { + expect(messages[messages.length - 1].signature).toEqual(0x02) // last message is GOODBYE in V3 + } }) - it('should not prepare broken connection to close', done => { + it('should not prepare broken connection to close', async () => { connection = createConnection('bolt://localhost') - connection - .connect('my-connection/9.9.9', basicAuthToken()) - .then(() => { - expect(connection._protocol).toBeDefined() - expect(connection._protocol).not.toBeNull() + await connection.connect('my-connection/9.9.9', basicAuthToken()) + expect(connection._protocol).toBeDefined() + expect(connection._protocol).not.toBeNull() - // make connection seem broken - connection._isBroken = true - expect(connection.isOpen()).toBeFalsy() + // make connection seem broken + connection._isBroken = true + expect(connection.isOpen()).toBeFalsy() - connection._protocol.prepareToClose = () => { - throw new Error('Not supposed to be called') - } + connection._protocol.prepareToClose = () => { + throw new Error('Not supposed to be called') + } - connection.close(() => done()) - }) - .catch(error => done.fail(error)) + await connection.close() }) function packedHandshakeMessage () { diff --git a/test/internal/connection-delegate.test.js b/test/internal/connection-delegate.test.js index 3c0fdea4a..3af1d0779 100644 --- a/test/internal/connection-delegate.test.js +++ b/test/internal/connection-delegate.test.js @@ -149,12 +149,12 @@ describe('#unit DelegateConnection', () => { expect(spy).toHaveBeenCalledTimes(1) }) - it('should delegate close', () => { + it('should delegate close', async () => { const delegate = new Connection(null) - const spy = spyOn(delegate, 'close') + const spy = spyOn(delegate, 'close').and.returnValue(Promise.resolve()) const connection = new DelegateConnection(delegate, null) - connection.close() + await connection.close() expect(spy).toHaveBeenCalledTimes(1) }) diff --git a/test/internal/dummy-channel.js b/test/internal/dummy-channel.js index bc29b40f7..d00aca888 100644 --- a/test/internal/dummy-channel.js +++ b/test/internal/dummy-channel.js @@ -51,11 +51,9 @@ export default class DummyChannel { return new CombinedBuffer(this.written) } - close (cb) { + close () { this.clear() - if (cb) { - return cb() - } + return Promise.resolve() } clear () { diff --git a/test/internal/fake-connection.js b/test/internal/fake-connection.js index e0ab6ade6..40d6abcc1 100644 --- a/test/internal/fake-connection.js +++ b/test/internal/fake-connection.js @@ -83,6 +83,7 @@ export default class FakeConnection extends Connection { _release () { this.releaseInvoked++ + return Promise.resolve() } isOpen () { diff --git a/test/internal/logger.test.js b/test/internal/logger.test.js index 4a99997ff..474b43c98 100644 --- a/test/internal/logger.test.js +++ b/test/internal/logger.test.js @@ -75,7 +75,7 @@ describe('#unit Logger', () => { }) describe('#integration Logger', () => { - it('should log when logger configured in the driver', done => { + it('should log when logger configured in the driver', async () => { const logged = [] const config = memorizingLoggerConfig(logged) const driver = neo4j.driver( @@ -85,31 +85,25 @@ describe('#integration Logger', () => { ) const session = driver.session() - session - .run('RETURN 42') - .then(() => { - expect(logged.length).toBeGreaterThan(0) - - const seenLevels = logged.map(log => log.level) - const seenMessages = logged.map(log => log.message) - - // at least info and debug should've been used - expect(seenLevels).toContain('info') - expect(seenLevels).toContain('debug') - - // the executed statement should've been logged - const statementLogged = seenMessages.find( - message => message.indexOf('RETURN 42') !== -1 - ) - expect(statementLogged).toBeTruthy() - }) - .catch(error => { - done.fail(error) - }) - .then(() => { - driver.close() - done() - }) + await session.run('RETURN 42') + + expect(logged.length).toBeGreaterThan(0) + + const seenLevels = logged.map(log => log.level) + const seenMessages = logged.map(log => log.message) + + // at least info and debug should've been used + expect(seenLevels).toContain('info') + expect(seenLevels).toContain('debug') + + // the executed statement should've been logged + const statementLogged = seenMessages.find( + message => message.indexOf('RETURN 42') !== -1 + ) + expect(statementLogged).toBeTruthy() + + await session.close() + await driver.close() }) it('should log debug to console when configured in the driver', async () => { @@ -141,7 +135,7 @@ describe('#integration Logger', () => { expect(driverCreationLogged).toBeTruthy() } finally { console.log = originalConsoleLog - driver.close() + await driver.close() } }) @@ -172,7 +166,7 @@ describe('#integration Logger', () => { expect(driverCreationLogged).toBeTruthy() } finally { console.log = originalConsoleLog - driver.close() + await driver.close() } }) }) diff --git a/test/internal/node/direct.driver.boltkit.test.js b/test/internal/node/direct.driver.boltkit.test.js index a1b4a730a..dc3411141 100644 --- a/test/internal/node/direct.driver.boltkit.test.js +++ b/test/internal/node/direct.driver.boltkit.test.js @@ -35,530 +35,381 @@ describe('#stub-direct direct driver with stub server', () => { }) describe('should run query', () => { - function verifyShouldRunQuery (version, done) { + async function verifyShouldRunQuery (version) { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/return_x.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - // When - const session = driver.session() - // Then - session.run('RETURN $x', { x: 1 }).then(res => { - expect(res.records[0].get('x').toInt()).toEqual(1) - session.close() - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + // When + const session = driver.session() + // Then + const res = await session.run('RETURN $x', { x: 1 }) + expect(res.records[0].get('x').toInt()).toEqual(1) + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyShouldRunQuery('v2', done) - }) + it('v2', () => verifyShouldRunQuery('v2')) - it('v3', done => { - verifyShouldRunQuery('v3', done) - }) + it('v3', () => verifyShouldRunQuery('v3')) - it('v4', done => { - verifyShouldRunQuery('v4', done) - }) + it('v4', () => verifyShouldRunQuery('v4')) }) describe('should send and receive bookmark for read transaction', () => { - function verifyBookmarkForReadTxc (version, done) { + async function verifyBookmarkForReadTxc (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/read_tx_with_bookmarks.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session({ - defaultAccessMode: READ, - bookmarks: ['neo4j:bookmark:v1:tx42'] - }) - const tx = session.beginTransaction() - tx.run('MATCH (n) RETURN n.name AS name').then(result => { - const records = result.records - expect(records.length).toEqual(2) - expect(records[0].get('name')).toEqual('Bob') - expect(records[1].get('name')).toEqual('Alice') - - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session({ + defaultAccessMode: READ, + bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const tx = session.beginTransaction() + const result = await tx.run('MATCH (n) RETURN n.name AS name') + const records = result.records + expect(records.length).toEqual(2) + expect(records[0].get('name')).toEqual('Bob') + expect(records[1].get('name')).toEqual('Alice') + + await tx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyBookmarkForReadTxc('v2', done) - }) + it('v2', () => verifyBookmarkForReadTxc('v2')) - it('v3', done => { - verifyBookmarkForReadTxc('v3', done) - }) + it('v3', () => verifyBookmarkForReadTxc('v3')) - it('v4', done => { - verifyBookmarkForReadTxc('v4', done) - }) + it('v4', () => verifyBookmarkForReadTxc('v4')) }) describe('should send and receive bookmark for write transaction', () => { - function verifyBookmarkForWriteTxc (version, done) { + async function verifyBookmarkForWriteTxc (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/write_tx_with_bookmarks.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session({ - defaultAccessMode: WRITE, - bookmarks: ['neo4j:bookmark:v1:tx42'] - }) - const tx = session.beginTransaction() - tx.run("CREATE (n {name:'Bob'})").then(result => { - const records = result.records - expect(records.length).toEqual(0) - - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session({ + defaultAccessMode: WRITE, + bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const tx = session.beginTransaction() + const result = await tx.run("CREATE (n {name:'Bob'})") + const records = result.records + expect(records.length).toEqual(0) + + await tx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyBookmarkForWriteTxc('v2', done) - }) + it('v2', () => verifyBookmarkForWriteTxc('v2')) - it('v3', done => { - verifyBookmarkForWriteTxc('v3', done) - }) + it('v3', () => verifyBookmarkForWriteTxc('v3')) - it('v4', done => { - verifyBookmarkForWriteTxc('v4', done) - }) + it('v4', () => verifyBookmarkForWriteTxc('v4')) }) describe('should send and receive bookmark between write and read transactions', () => { - function verifyBookmark (version, done) { + async function verifyBookmark (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/write_read_tx_with_bookmarks.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session({ - defaultAccessMode: WRITE, - bookmarks: ['neo4j:bookmark:v1:tx42'] - }) - const writeTx = session.beginTransaction() - writeTx.run("CREATE (n {name:'Bob'})").then(result => { - const records = result.records - expect(records.length).toEqual(0) - - writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - const readTx = session.beginTransaction() - readTx.run('MATCH (n) RETURN n.name AS name').then(result => { - const records = result.records - expect(records.length).toEqual(1) - expect(records[0].get('name')).toEqual('Bob') - - readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual( - 'neo4j:bookmark:v1:tx424242' - ) - - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session({ + defaultAccessMode: WRITE, + bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const writeTx = session.beginTransaction() + const result1 = await writeTx.run("CREATE (n {name:'Bob'})") + const records1 = result1.records + expect(records1.length).toEqual(0) + + await writeTx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + + const readTx = session.beginTransaction() + const result2 = await readTx.run('MATCH (n) RETURN n.name AS name') + const records2 = result2.records + expect(records2.length).toEqual(1) + expect(records2[0].get('name')).toEqual('Bob') + + await readTx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242') + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyBookmark('v2', done) - }) + it('v2', () => verifyBookmark('v2')) - it('v3', done => { - verifyBookmark('v3', done) - }) + it('v3', () => verifyBookmark('v3')) - it('v4', done => { - verifyBookmark('v4', done) - }) + it('v4', () => verifyBookmark('v4')) }) describe('should throw service unavailable when server dies', () => { - function verifyServiceUnavailable (version, done) { + async function verifyServiceUnavailable (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/read_dead.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').catch(error => { - expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session({ defaultAccessMode: READ }) + await expectAsync( + session.run('MATCH (n) RETURN n.name') + ).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE }) - }) + ) + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyServiceUnavailable('v2', done) - }) + it('v2', () => verifyServiceUnavailable('v2')) - it('v3', done => { - verifyServiceUnavailable('v3', done) - }) + it('v3', () => verifyServiceUnavailable('v3')) - it('v4', done => { - verifyServiceUnavailable('v4', done) - }) + it('v4', () => verifyServiceUnavailable('v4')) }) describe('should close connection when RESET fails', () => { - function verifyCloseConnection (version, done) { + async function verifyCloseConnection (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/reset_error.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session() - - session - .run('RETURN 42 AS answer') - .then(result => { - const records = result.records - expect(records.length).toEqual(1) - expect(records[0].get(0).toNumber()).toEqual(42) - session.close().then(() => { - expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - .catch(error => done.fail(error)) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session() + + const result = await session.run('RETURN 42 AS answer') + const records = result.records + expect(records.length).toEqual(1) + expect(records[0].get(0).toNumber()).toEqual(42) + + await session.close() + expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) + + await driver.close() + await server.exit() } - it('v2', done => { - verifyCloseConnection('v2', done) - }) + it('v2', () => verifyCloseConnection('v2')) - it('v3', done => { - verifyCloseConnection('v3', done) - }) + it('v3', () => verifyCloseConnection('v3')) - it('v4', done => { - verifyCloseConnection('v4', done) - }) + it('v4', () => verifyCloseConnection('v4')) }) describe('should send RESET on error', () => { - function verifyReset (version, done) { + async function verifyReset (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/query_with_error.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session() - - session - .run('RETURN 10 / 0') - .then(result => { - done.fail( - 'Should fail but received a result: ' + JSON.stringify(result) - ) - }) - .catch(error => { - expect(error.code).toEqual( - 'Neo.ClientError.Statement.ArithmeticError' - ) - expect(error.message).toEqual('/ by zero') - - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session() + + await expectAsync(session.run('RETURN 10 / 0')).toBeRejectedWith( + jasmine.objectContaining({ + code: 'Neo.ClientError.Statement.ArithmeticError', + message: '/ by zero' + }) + ) + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyReset('v2', done) - }) + it('v2', () => verifyReset('v2')) - it('v3', done => { - verifyReset('v3', done) - }) + it('v3', () => verifyReset('v3')) - it('v4', done => { - verifyReset('v4', done) - }) + it('v4', () => verifyReset('v4')) }) describe('should include database connection id in logs', () => { - function verifyConnectionId (version, done) { + async function verifyConnectionId (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/hello_run_exit.script`, 9001 ) - boltStub.run(() => { - const messages = [] - const logging = { - level: 'debug', - logger: (level, message) => messages.push(message) - } + const messages = [] + const logging = { + level: 'debug', + logger: (level, message) => messages.push(message) + } - const driver = boltStub.newDriver('bolt://127.0.0.1:9001', { - logging: logging - }) - const session = driver.session() - - session - .run('MATCH (n) RETURN n.name') - .then(result => { - const names = result.records.map(record => record.get(0)) - expect(names).toEqual(['Foo', 'Bar']) - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - - // logged messages should contain connection_id supplied by the database - const containsDbConnectionIdMessage = messages.find(message => - message.match(/Connection \[[0-9]+]\[bolt-123456789]/) - ) - if (!containsDbConnectionIdMessage) { - console.log(messages) - } - expect(containsDbConnectionIdMessage).toBeTruthy() - - done() - }) - }) - }) - .catch(error => done.fail(error)) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001', { + logging: logging }) + const session = driver.session() + + const result = await session.run('MATCH (n) RETURN n.name') + + const names = result.records.map(record => record.get(0)) + expect(names).toEqual(['Foo', 'Bar']) + + await session.close() + await driver.close() + await server.exit() + + // logged messages should contain connection_id supplied by the database + const containsDbConnectionIdMessage = messages.find(message => + message.match(/Connection \[[0-9]+]\[bolt-123456789]/) + ) + if (!containsDbConnectionIdMessage) { + console.log(messages) + } + expect(containsDbConnectionIdMessage).toBeTruthy() } - it('v3', done => { - verifyConnectionId('v3', done) - }) + it('v3', () => verifyConnectionId('v3')) - it('v4', done => { - verifyConnectionId('v4', done) - }) + it('v4', () => verifyConnectionId('v4')) }) describe('should close connection if it dies sitting idle in connection pool', () => { - function verifyConnectionCleanup (version, done) { + async function verifyConnectionCleanup (version) { if (!boltStub.supported) { done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/read.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session({ defaultAccessMode: READ }) - - session - .run('MATCH (n) RETURN n.name') - .then(result => { - const records = result.records - expect(records.length).toEqual(3) - expect(records[0].get(0)).toBe('Bob') - expect(records[1].get(0)).toBe('Alice') - expect(records[2].get(0)).toBe('Tina') - - const connectionKey = Object.keys(openConnections(driver))[0] - expect(connectionKey).toBeTruthy() - - const connection = openConnections(driver, connectionKey) - session.close().then(() => { - // generate a fake fatal error - connection._handleFatalError( - newError('connection reset', SERVICE_UNAVAILABLE) - ) - - // expect that the connection to be removed from the pool - expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) - expect(activeResources(driver, '127.0.0.1:9001')).toBeFalsy() - // expect that the connection to be unregistered from the open connections registry - expect(openConnections(driver, connectionKey)).toBeFalsy() - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - .catch(error => done.fail(error)) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session({ defaultAccessMode: READ }) + + const result = await session.run('MATCH (n) RETURN n.name') + const records = result.records + expect(records.length).toEqual(3) + expect(records[0].get(0)).toBe('Bob') + expect(records[1].get(0)).toBe('Alice') + expect(records[2].get(0)).toBe('Tina') + + const connectionKey = Object.keys(openConnections(driver))[0] + expect(connectionKey).toBeTruthy() + + const connection = openConnections(driver, connectionKey) + await session.close() + + // generate a fake fatal error + connection._handleFatalError( + newError('connection reset', SERVICE_UNAVAILABLE) + ) + + // expect that the connection to be removed from the pool + expect(connectionPool(driver, '127.0.0.1:9001').length).toEqual(0) + expect(activeResources(driver, '127.0.0.1:9001')).toBeFalsy() + // expect that the connection to be unregistered from the open connections registry + expect(openConnections(driver, connectionKey)).toBeFalsy() + + await driver.close() + await server.exit() } - it('v2', done => { - verifyConnectionCleanup('v2', done) - }) + it('v2', () => verifyConnectionCleanup('v2')) - it('v3', done => { - verifyConnectionCleanup('v3', done) - }) + it('v3', () => verifyConnectionCleanup('v3')) - it('v4', done => { - verifyConnectionCleanup('v4', done) - }) + it('v4', () => verifyConnectionCleanup('v4')) }) describe('should fail if commit fails due to broken connection', () => { - function verifyFailureOnCommit (version, done) { + async function verifyFailureOnCommit (version) { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/${version}/connection_error_on_commit.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('bolt://127.0.0.1:9001') - const session = driver.session() - - const writeTx = session.beginTransaction() - - writeTx - .run("CREATE (n {name: 'Bob'})") - .then(() => - writeTx.commit().then( - result => fail('expected an error'), - error => { - expect(error.code).toBe(SERVICE_UNAVAILABLE) - } - ) - ) - .then(() => - session.close().then(() => { - driver.close() - - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - ) - .catch(error => done.fail(error)) - }) + const driver = boltStub.newDriver('bolt://127.0.0.1:9001') + const session = driver.session() + + const writeTx = session.beginTransaction() + await writeTx.run("CREATE (n {name: 'Bob'})") + + await expectAsync(writeTx.commit()).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE + }) + ) + + await session.close() + await driver.close() + await server.exit() } - it('v2', done => { - verifyFailureOnCommit('v2', done) - }) + it('v2', () => verifyFailureOnCommit('v2')) - it('v3', done => { - verifyFailureOnCommit('v3', done) - }) + it('v3', () => verifyFailureOnCommit('v3')) }) function connectionPool (driver, key) { diff --git a/test/internal/node/node-channel.test.js b/test/internal/node/node-channel.test.js index 46867eb47..1c47080a5 100644 --- a/test/internal/node/node-channel.test.js +++ b/test/internal/node/node-channel.test.js @@ -47,6 +47,8 @@ function createMockedChannel (connected) { const channelConfig = new ChannelConfig(address, {}, SERVICE_UNAVAILABLE) const channel = new NodeChannel(channelConfig) const socket = { + destroyed: false, + destroy: () => {}, end: () => { channel._open = false endCallback() diff --git a/test/internal/node/package.test.js b/test/internal/node/package.test.js index 258b10082..fda05a678 100644 --- a/test/internal/node/package.test.js +++ b/test/internal/node/package.test.js @@ -25,9 +25,9 @@ var sharedNeo4j = require('../shared-neo4j').default describe('Package', function () { var driver - afterAll(function () { + afterAll(async () => { if (driver) { - driver.close() + await driver.close() } }) @@ -50,9 +50,9 @@ describe('Package', function () { .then(function (result) { expect(result.records.length).toBe(1) expect(result.records[0].get('answer').toNumber()).toBe(1) - session.close() - done() }) + .then(() => session.close()) + .then(() => done()) .catch(function (e) { done.fail(e) }) diff --git a/test/internal/node/routing.driver.boltkit.test.js b/test/internal/node/routing.driver.boltkit.test.js index 0c22d8d6c..1ff6d0706 100644 --- a/test/internal/node/routing.driver.boltkit.test.js +++ b/test/internal/node/routing.driver.boltkit.test.js @@ -24,6 +24,7 @@ import RoutingTable from '../../../src/internal/routing-table' import { SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../../../src/error' import lolex from 'lolex' import ServerAddress from '../../../src/internal/server-address' +import { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } from 'constants' describe('#stub-routing routing driver with stub server', () => { let originalTimeout @@ -37,132 +38,103 @@ describe('#stub-routing routing driver with stub server', () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout }) - it('should discover servers', done => { + it('should discover servers', async () => { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/discover_servers_and_read.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session() - session.run('MATCH (n) RETURN n.name').then(() => { - session.close() - // Then - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9002', '127.0.0.1:9003']) - assertHasWriters(driver, ['127.0.0.1:9001']) - - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session() + await session.run('MATCH (n) RETURN n.name') + await session.close() + + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9002', '127.0.0.1:9003']) + assertHasWriters(driver, ['127.0.0.1:9001']) + + await driver.close() + await server.exit() }) - it('should discover IPv6 servers', done => { + it('should discover IPv6 servers', async () => { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/discover_ipv6_servers_and_read.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(() => { - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - assertHasReaders(driver, ['127.0.0.1:9001', '[::1]:9001']) - assertHasWriters(driver, [ - '[2001:db8:a0b:12f0::1]:9002', - '[3731:54:65fe:2::a7]:9003' - ]) - assertHasRouters(driver, [ - '[ff02::1]:9001', - '[684d:1111:222:3333:4444:5555:6:77]:9002', - '[::1]:9003' - ]) - - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - assertHasReaders(driver, ['127.0.0.1:9001', '[::1]:9001']) - assertHasWriters(driver, [ - '[2001:db8:a0b:12f0::1]:9002', - '[3731:54:65fe:2::a7]:9003' - ]) - assertHasRouters(driver, [ - '[ff02::1]:9001', - '[684d:1111:222:3333:4444:5555:6:77]:9002', - '[::1]:9003' - ]) - - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) - - it('should purge connections to stale servers after routing table refresh', done => { + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session({ defaultAccessMode: READ }) + await session.run('MATCH (n) RETURN n.name') + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + assertHasReaders(driver, ['127.0.0.1:9001', '[::1]:9001']) + assertHasWriters(driver, [ + '[2001:db8:a0b:12f0::1]:9002', + '[3731:54:65fe:2::a7]:9003' + ]) + assertHasRouters(driver, [ + '[ff02::1]:9001', + '[684d:1111:222:3333:4444:5555:6:77]:9002', + '[::1]:9003' + ]) + + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + assertHasReaders(driver, ['127.0.0.1:9001', '[::1]:9001']) + assertHasWriters(driver, [ + '[2001:db8:a0b:12f0::1]:9002', + '[3731:54:65fe:2::a7]:9003' + ]) + assertHasRouters(driver, [ + '[ff02::1]:9001', + '[684d:1111:222:3333:4444:5555:6:77]:9002', + '[::1]:9003' + ]) + + await driver.close() + await server.exit() + }) + + it('should purge connections to stale servers after routing table refresh', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9042 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9042') - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(() => { - session.close() - - expect(hasAddressInConnectionPool(driver, '127.0.0.1:9042')).toBeFalsy() - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9005') - ).toBeTruthy() - - driver.close() - router.exit(routerCode => { - reader.exit(readerCode => { - expect(routerCode).toEqual(0) - expect(readerCode).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9042') + const session = driver.session({ defaultAccessMode: READ }) + await session.run('MATCH (n) RETURN n.name') + await session.close() + + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9042')).toBeFalsy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeTruthy() + + await driver.close() + await router.exit() + await reader.exit() }) it('should discover servers using subscribe', done => { @@ -171,830 +143,678 @@ describe('#stub-routing routing driver with stub server', () => { return } // Given - const server = boltStub.start( - './test/resources/boltstub/v3/discover_servers_and_read.script', - 9001 - ) - - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session() - session.run('MATCH (n) RETURN n.name').subscribe({ - onCompleted: () => { - // Then - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9002', '127.0.0.1:9003']) - assertHasWriters(driver, ['127.0.0.1:9001']) - - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - } + boltStub + .start( + './test/resources/boltstub/v3/discover_servers_and_read.script', + 9001 + ) + .then(server => { + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session() + session.run('MATCH (n) RETURN n.name').subscribe({ + onCompleted: () => { + // Then + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9002', '127.0.0.1:9003']) + assertHasWriters(driver, ['127.0.0.1:9001']) + + driver + .close() + .then(() => server.exit()) + .then(() => done()) + } + }) }) - }) }) - it('should handle empty response from server', done => { + it('should handle empty response from server', async () => { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_records.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: neo4j.READ }) - session - .run('MATCH (n) RETURN n.name') - .catch(err => { - expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - - session.close() - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - .catch(err => { - console.log(err) - }) - }) + // When + const session = driver.session({ defaultAccessMode: neo4j.READ }) + + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE + }) + ) + + await session.close() + await driver.close() + await server.exit() }) - it('should acquire read server', done => { + it('should acquire read server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(res => { - session.close() - - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9005') - ).toBeTruthy() - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + // When + const session = driver.session({ defaultAccessMode: READ }) + const res = await session.run('MATCH (n) RETURN n.name') + await session.close() + + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeTruthy() + + expect(res.records[0].get('n.name')).toEqual('Bob') + expect(res.records[1].get('n.name')).toEqual('Alice') + expect(res.records[2].get('n.name')).toEqual('Tina') + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should pick first available route-server', done => { + it('should pick first available route-server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_short_ttl.script', 9999 ) - const nextRouter = boltStub.start( + const nextRouter = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9003 ) - const readServer1 = boltStub.start( + const readServer1 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9004 ) - const readServer2 = boltStub.start( + const readServer2 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9006 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') - // When - const session1 = driver.session({ defaultAccessMode: READ }) - session1.run('MATCH (n) RETURN n.name').then(res => { - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - session1.close() - - const session2 = driver.session({ defaultAccessMode: READ }) - session2.run('MATCH (n) RETURN n.name').then(res => { - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - session2.close() - driver.close() - seedServer.exit(code1 => { - nextRouter.exit(code2 => { - readServer1.exit(code3 => { - readServer2.exit(code4 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) - }) - - it('should round-robin among read servers', done => { + const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') + // When + const session1 = driver.session({ defaultAccessMode: READ }) + const res1 = await session1.run('MATCH (n) RETURN n.name') + // Then + expect(res1.records[0].get('n.name')).toEqual('Bob') + expect(res1.records[1].get('n.name')).toEqual('Alice') + expect(res1.records[2].get('n.name')).toEqual('Tina') + await session1.close() + + const session2 = driver.session({ defaultAccessMode: READ }) + const res2 = await session2.run('MATCH (n) RETURN n.name') + // Then + expect(res2.records[0].get('n.name')).toEqual('Bob') + expect(res2.records[1].get('n.name')).toEqual('Alice') + expect(res2.records[2].get('n.name')).toEqual('Tina') + await session2.close() + + await driver.close() + await seedServer.exit() + await nextRouter.exit() + await readServer1.exit() + await readServer2.exit() + }) + + it('should round-robin among read servers', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer1 = boltStub.start( + const readServer1 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - const readServer2 = boltStub.start( + const readServer2 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9006 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session1 = driver.session({ defaultAccessMode: READ }) - session1.run('MATCH (n) RETURN n.name').then(res => { - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - session1.close() - const session2 = driver.session({ defaultAccessMode: READ }) - session2.run('MATCH (n) RETURN n.name').then(res => { - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - session2.close() - - driver.close() - seedServer.exit(code1 => { - readServer1.exit(code2 => { - readServer2.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session1 = driver.session({ defaultAccessMode: READ }) + const res1 = await session1.run('MATCH (n) RETURN n.name') + // Then + expect(res1.records[0].get('n.name')).toEqual('Bob') + expect(res1.records[1].get('n.name')).toEqual('Alice') + expect(res1.records[2].get('n.name')).toEqual('Tina') + await session1.close() + + const session2 = driver.session({ defaultAccessMode: READ }) + const res2 = await session2.run('MATCH (n) RETURN n.name') + // Then + expect(res2.records[0].get('n.name')).toEqual('Bob') + expect(res2.records[1].get('n.name')).toEqual('Alice') + expect(res2.records[2].get('n.name')).toEqual('Tina') + await session2.close() + + await driver.close() + await seedServer.exit() + await readServer1.exit() + await readServer2.exit() }) - it('should handle missing read server', done => { + it('should handle missing read server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read_dead.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').catch(err => { - expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED) - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: READ }) + + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SESSION_EXPIRED }) - }) + ) + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should acquire write server', done => { + it('should acquire write server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: WRITE }) - session.run("CREATE (n {name:'Bob'})").then(() => { - // Then - driver.close() - seedServer.exit(code1 => { - writeServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: WRITE }) + + // Then + await session.run("CREATE (n {name:'Bob'})") + + await driver.close() + await seedServer.exit() + await writeServer.exit() }) - it('should round-robin among write servers', done => { + it('should round-robin among write servers', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer1 = boltStub.start( + const writeServer1 = await boltStub.start( './test/resources/boltstub/v3/write.script', 9007 ) - const readServer2 = boltStub.start( + const writeServer2 = await boltStub.start( './test/resources/boltstub/v3/write.script', 9008 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session1 = driver.session({ defaultAccessMode: WRITE }) - session1.run("CREATE (n {name:'Bob'})").then(() => { - const session2 = driver.session({ defaultAccessMode: WRITE }) - session2.run("CREATE (n {name:'Bob'})").then(() => { - // Then - driver.close() - seedServer.exit(code1 => { - readServer1.exit(code2 => { - readServer2.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When & Then + const session1 = driver.session({ defaultAccessMode: WRITE }) + await session1.run("CREATE (n {name:'Bob'})") + + const session2 = driver.session({ defaultAccessMode: WRITE }) + await session2.run("CREATE (n {name:'Bob'})") + + await driver.close() + await seedServer.exit() + await writeServer1.exit() + await writeServer2.exit() }) - it('should handle missing write server', done => { + it('should handle missing write server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write_dead.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: WRITE }) - session.run('CREATE ()').catch(err => { - expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED) - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When & Then + const session = driver.session({ defaultAccessMode: WRITE }) + await expectAsync(session.run('CREATE ()')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SESSION_EXPIRED }) - }) + ) + + await driver.close() + await seedServer.exit() + await writeServer.exit() }) - it('should remember endpoints', done => { + it('should remember endpoints', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(() => { - // Then - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) - assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: READ }) + await session.run('MATCH (n) RETURN n.name') + // Then + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should forget endpoints on failure', done => { + it('should forget endpoints on failure', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read_dead.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').catch(() => { - session.close() - // Then - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeFalsy() - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9006']) - assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session({ defaultAccessMode: READ }) + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejected() + await session.close() + + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeFalsy() + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9006']) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should forget endpoints on session acquisition failure', done => { + it('should forget endpoints on session acquisition failure', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').catch(() => { - session.close() - // Then - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeFalsy() - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9006']) - assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - driver.close() - seedServer.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session({ defaultAccessMode: READ }) + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejected() + await session.close() + + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeFalsy() + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9006']) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) + + await driver.close() + await seedServer.exit() }) - it('should rediscover if necessary', done => { + it('should rediscover if necessary', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_rediscover.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session1 = driver.session({ defaultAccessMode: READ }) - session1.run('MATCH (n) RETURN n.name').catch(() => { - const session2 = driver.session({ defaultAccessMode: READ }) - session2.run('MATCH (n) RETURN n.name').then(() => { - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session1 = driver.session({ defaultAccessMode: READ }) + await expectAsync(session1.run('MATCH (n) RETURN n.name')).toBeRejected() + + const session2 = driver.session({ defaultAccessMode: READ }) + await expectAsync(session2.run('MATCH (n) RETURN n.name')).toBeResolved() + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should handle server not able to do routing', done => { + it('should handle server not able to do routing', async () => { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_not_supported.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session() - session.run('MATCH (n) RETURN n.name').catch(err => { - expect(err.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - expect(err.message).toContain('Could not perform discovery') - assertNoRoutingTable(driver) - session.close() - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When & Then + const session = driver.session() + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SERVICE_UNAVAILABLE, + message: jasmine.stringMatching(/Could not perform discovery/) }) - }) + ) + assertNoRoutingTable(driver) + + await session.close() + await driver.close() + await server.exit() }) - it('should handle leader switch while writing', done => { + it('should handle leader switch while writing', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/write_not_a_leader.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session() - session.run('CREATE ()').catch(err => { - // the server at 9007 should have been removed - assertHasWriters(driver, ['127.0.0.1:9008']) - expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED) - session.close() - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session() + await expectAsync(session.run('CREATE ()')).toBeRejectedWith( + jasmine.objectContaining({ code: neo4j.error.SESSION_EXPIRED }) + ) + // the server at 9007 should have been removed + assertHasWriters(driver, ['127.0.0.1:9008']) + + await session.close() + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should handle leader switch while writing on transaction', done => { + it('should handle leader switch while writing on transaction', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/write_tx_not_a_leader.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session() - const tx = session.beginTransaction() - tx.run('CREATE ()') - - tx.commit().catch(err => { - // the server at 9007 should have been removed - assertHasWriters(driver, ['127.0.0.1:9008']) - expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED) - session.close() - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session() + const tx = session.beginTransaction() + tx.run('CREATE ()') + + // Then + await expectAsync(tx.commit()).toBeRejectedWith( + jasmine.objectContaining({ code: neo4j.error.SESSION_EXPIRED }) + ) + // the server at 9007 should have been removed + assertHasWriters(driver, ['127.0.0.1:9008']) + + await session.close() + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should fail if missing write server', done => { + it('should fail if missing write server', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: WRITE }) - session.run('MATCH (n) RETURN n.name').catch(err => { - expect(err.code).toEqual(neo4j.error.SESSION_EXPIRED) - driver.close() - seedServer.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: WRITE }) + + // Then + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejectedWith( + jasmine.objectContaining({ code: neo4j.error.SESSION_EXPIRED }) + ) + + await driver.close() + await seedServer.exit() }) - it('should try next router when current router fails to return a routing table', done => { + it('should try next router when current router fails to return a routing table', async () => { if (!boltStub.supported) { - done() return } - const server1 = boltStub.start( + const server1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_zero_ttl.script', 9999 ) - const server2 = boltStub.start( + const server2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_dead.script', 9091 ) - const server3 = boltStub.start( + const server3 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_dead.script', 9092 ) - const server4 = boltStub.start( + const server4 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_dead.script', 9093 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') - - const session1 = driver.session() - session1.run('MATCH (n) RETURN n').then(result1 => { - expect(result1.summary.server.address).toEqual('127.0.0.1:9999') - session1.close() - - assertHasRouters(driver, [ - '127.0.0.1:9091', - '127.0.0.1:9092', - '127.0.0.1:9093', - '127.0.0.1:9999' - ]) - const memorizingRoutingTable = setUpMemorizingRoutingTable(driver) - - const session2 = driver.session() - session2.run('MATCH (n) RETURN n').then(result2 => { - expect(result2.summary.server.address).toEqual('127.0.0.1:9999') - session2.close() - - // returned routers failed to respond and should have been forgotten - memorizingRoutingTable.assertForgotRouters([ - '127.0.0.1:9091', - '127.0.0.1:9092', - '127.0.0.1:9093' - ]) - assertHasRouters(driver, ['127.0.0.1:9999']) - driver.close() - - server1.exit(code1 => { - server2.exit(code2 => { - server3.exit(code3 => { - server4.exit(code4 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') + + const session1 = driver.session() + const result1 = await session1.run('MATCH (n) RETURN n') + expect(result1.summary.server.address).toEqual('127.0.0.1:9999') + await session1.close() + + assertHasRouters(driver, [ + '127.0.0.1:9091', + '127.0.0.1:9092', + '127.0.0.1:9093', + '127.0.0.1:9999' + ]) + const memorizingRoutingTable = setUpMemorizingRoutingTable(driver) + + const session2 = driver.session() + const result2 = await session2.run('MATCH (n) RETURN n') + expect(result2.summary.server.address).toEqual('127.0.0.1:9999') + await session2.close() + + // returned routers failed to respond and should have been forgotten + memorizingRoutingTable.assertForgotRouters([ + '127.0.0.1:9091', + '127.0.0.1:9092', + '127.0.0.1:9093' + ]) + assertHasRouters(driver, ['127.0.0.1:9999']) + + await driver.close() + await server1.exit() + await server2.exit() + await server3.exit() + await server4.exit() }) - it('should re-use connections', done => { + it('should re-use connections', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_1.script', 9002 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write_twice.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9002') - // When - const session1 = driver.session({ defaultAccessMode: WRITE }) - session1.run("CREATE (n {name:'Bob'})").then(() => { - session1.close().then(() => { - const openConnectionsCount = numberOfOpenConnections(driver) - const session2 = driver.session({ defaultAccessMode: WRITE }) - session2.run('CREATE ()').then(() => { - // driver should have same amount of open connections at this point; - // no new connections should be created, existing connections should be reused - expect(numberOfOpenConnections(driver)).toEqual( - openConnectionsCount - ) - driver.close() - - // all connections should be closed when driver is closed - expect(numberOfOpenConnections(driver)).toEqual(0) - - seedServer.exit(code1 => { - writeServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9002') + // When + const session1 = driver.session({ defaultAccessMode: WRITE }) + await session1.run("CREATE (n {name:'Bob'})") + await session1.close() + const openConnectionsCount = numberOfOpenConnections(driver) + + const session2 = driver.session({ defaultAccessMode: WRITE }) + await session2.run('CREATE ()') + + // driver should have same amount of open connections at this point; + // no new connections should be created, existing connections should be reused + expect(numberOfOpenConnections(driver)).toEqual(openConnectionsCount) + + await driver.close() + // all connections should be closed when driver is closed + expect(numberOfOpenConnections(driver)).toEqual(0) + + await seedServer.exit() + await writeServer.exit() }) - it('should expose server info in cluster', done => { + it('should expose server info in cluster', async () => { if (!boltStub.supported) { - done() return } // Given - const routingServer = boltStub.start( + const routingServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write_with_server_version.script', 9007 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read_with_server_version.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const readSession = driver.session({ defaultAccessMode: READ }) - readSession.run('MATCH (n) RETURN n.name').then(readResult => { - const writeSession = driver.session({ defaultAccessMode: WRITE }) - writeSession.run("CREATE (n {name:'Bob'})").then(writeResult => { - const readServerInfo = readResult.summary.server - const writeServerInfo = writeResult.summary.server - - readSession.close() - writeSession.close() - driver.close() - - routingServer.exit(routingServerExitCode => { - writeServer.exit(writeServerExitCode => { - readServer.exit(readServerExitCode => { - expect(readServerInfo.address).toBe('127.0.0.1:9005') - expect(readServerInfo.version).toBe('Neo4j/8.8.8') - - expect(writeServerInfo.address).toBe('127.0.0.1:9007') - expect(writeServerInfo.version).toBe('Neo4j/9.9.9') - - expect(routingServerExitCode).toEqual(0) - expect(writeServerExitCode).toEqual(0) - expect(readServerExitCode).toEqual(0) - - done() - }) - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const readSession = driver.session({ defaultAccessMode: READ }) + const readResult = await readSession.run('MATCH (n) RETURN n.name') + + const writeSession = driver.session({ defaultAccessMode: WRITE }) + const writeResult = await writeSession.run("CREATE (n {name:'Bob'})") + + // Then + const readServerInfo = readResult.summary.server + expect(readServerInfo.address).toBe('127.0.0.1:9005') + expect(readServerInfo.version).toBe('Neo4j/8.8.8') + + const writeServerInfo = writeResult.summary.server + expect(writeServerInfo.address).toBe('127.0.0.1:9007') + expect(writeServerInfo.version).toBe('Neo4j/9.9.9') + + await readSession.close() + await writeSession.close() + await driver.close() + await routingServer.exit() + await writeServer.exit() + await readServer.exit() }) it('should expose server info in cluster using observer', done => { @@ -1004,590 +824,474 @@ describe('#stub-routing routing driver with stub server', () => { } // Given - const routingServer = boltStub.start( - './test/resources/boltstub/v3/acquire_endpoints.script', - 9001 - ) - const writeServer = boltStub.start( - './test/resources/boltstub/v3/write_with_server_version.script', - 9007 - ) - const readServer = boltStub.start( - './test/resources/boltstub/v3/read_with_server_version.script', - 9005 - ) + boltStub + .start('./test/resources/boltstub/v3/acquire_endpoints.script', 9001) + .then(routingServer => + boltStub + .start( + './test/resources/boltstub/v3/write_with_server_version.script', + 9007 + ) + .then(writeServer => + boltStub + .start( + './test/resources/boltstub/v3/read_with_server_version.script', + 9005 + ) + .then(readServer => { + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const readSession = driver.session({ defaultAccessMode: READ }) - readSession.run('MATCH (n) RETURN n.name').subscribe({ - onNext: () => {}, - onError: () => {}, - onCompleted: readSummary => { - const writeSession = driver.session({ defaultAccessMode: WRITE }) - writeSession.run("CREATE (n {name:'Bob'})").subscribe({ - onNext: () => {}, - onError: () => {}, - onCompleted: writeSummary => { - readSession.close() - writeSession.close() - driver.close() - - routingServer.exit(routingServerExitCode => { - writeServer.exit(writeServerExitCode => { - readServer.exit(readServerExitCode => { - expect(readSummary.server.address).toBe('127.0.0.1:9005') - expect(readSummary.server.version).toBe('Neo4j/8.8.8') - - expect(writeSummary.server.address).toBe('127.0.0.1:9007') - expect(writeSummary.server.version).toBe('Neo4j/9.9.9') - - expect(routingServerExitCode).toEqual(0) - expect(writeServerExitCode).toEqual(0) - expect(readServerExitCode).toEqual(0) - - done() - }) + // When + const readSession = driver.session({ + defaultAccessMode: READ + }) + readSession.run('MATCH (n) RETURN n.name').subscribe({ + onNext: () => {}, + onError: () => {}, + onCompleted: readSummary => { + const writeSession = driver.session({ + defaultAccessMode: WRITE + }) + writeSession.run("CREATE (n {name:'Bob'})").subscribe({ + onNext: () => {}, + onError: () => {}, + onCompleted: writeSummary => { + expect(readSummary.server.address).toBe( + '127.0.0.1:9005' + ) + expect(readSummary.server.version).toBe('Neo4j/8.8.8') + + expect(writeSummary.server.address).toBe( + '127.0.0.1:9007' + ) + expect(writeSummary.server.version).toBe('Neo4j/9.9.9') + + readSession + .close() + .then(() => + writeSession.close().then(() => driver.close()) + ) + .then(() => routingServer.exit()) + .then(() => writeServer.exit()) + .then(() => readServer.exit()) + .then(() => done()) + } + }) + } }) }) - } - }) - } - }) - }) + ) + ) }) - it('should forget routers when fails to connect', done => { + it('should forget routers when fails to connect', async () => { if (!boltStub.supported) { - done() return } - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_zero_ttl.script', 9999 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') - - const session1 = driver.session() - session1.run('MATCH (n) RETURN n').then(result1 => { - expect(result1.summary.server.address).toEqual('127.0.0.1:9999') - session1.close() - - assertHasRouters(driver, [ - '127.0.0.1:9091', - '127.0.0.1:9092', - '127.0.0.1:9093', - '127.0.0.1:9999' - ]) - const memorizingRoutingTable = setUpMemorizingRoutingTable(driver) - - const session2 = driver.session() - session2.run('MATCH (n) RETURN n').then(result2 => { - expect(result2.summary.server.address).toEqual('127.0.0.1:9999') - session2.close() - - memorizingRoutingTable.assertForgotRouters([ - '127.0.0.1:9091', - '127.0.0.1:9092', - '127.0.0.1:9093' - ]) - assertHasRouters(driver, ['127.0.0.1:9999']) - driver.close() - - server.exit(code1 => { - expect(code1).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9999') + + const session1 = driver.session() + const result1 = await session1.run('MATCH (n) RETURN n') + expect(result1.summary.server.address).toEqual('127.0.0.1:9999') + await session1.close() + + assertHasRouters(driver, [ + '127.0.0.1:9091', + '127.0.0.1:9092', + '127.0.0.1:9093', + '127.0.0.1:9999' + ]) + const memorizingRoutingTable = setUpMemorizingRoutingTable(driver) + + const session2 = driver.session() + const result2 = await session2.run('MATCH (n) RETURN n') + expect(result2.summary.server.address).toEqual('127.0.0.1:9999') + await session2.close() + + memorizingRoutingTable.assertForgotRouters([ + '127.0.0.1:9091', + '127.0.0.1:9092', + '127.0.0.1:9093' + ]) + assertHasRouters(driver, ['127.0.0.1:9999']) + + await driver.close() + await server.exit() }) - it('should close connection used for routing table refreshing', done => { + it('should close connection used for routing table refreshing', async () => { if (!boltStub.supported) { - done() return } // server is both router and writer - const server = boltStub.start( + const server = await boltStub.start( './test/resources/boltstub/v3/discover_servers_and_read.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const acquiredConnections = [] - const releasedConnections = [] - setUpPoolToMemorizeAllAcquiredAndReleasedConnections( - driver, - acquiredConnections, - releasedConnections - ) + const acquiredConnections = [] + const releasedConnections = [] + setUpPoolToMemorizeAllAcquiredAndReleasedConnections( + driver, + acquiredConnections, + releasedConnections + ) - const session = driver.session() - session - .run('MATCH (n) RETURN n.name') - .then(() => { - session.close().then(() => { - driver.close() - server.exit(code => { - expect(code).toEqual(0) - - // two connections should have been acquired: one for rediscovery and one for the query - expect(acquiredConnections.length).toEqual(2) - // same two connections should have been released - expect(releasedConnections.length).toEqual(2) - - // verify that acquired connections are those that we released - for (let i = 0; i < acquiredConnections.length; i++) { - expect(acquiredConnections[i]).toBe(releasedConnections[i]) - } - done() - }) - }) - }) - .catch(console.log) - }) + const session = driver.session() + await session.run('MATCH (n) RETURN n.name') + + // two connections should have been acquired: one for rediscovery and one for the query + expect(acquiredConnections.length).toEqual(2) + // same two connections should have been released + expect(releasedConnections.length).toEqual(2) + // verify that acquired connections are those that we released + for (let i = 0; i < acquiredConnections.length; i++) { + expect(acquiredConnections[i]).toBe(releasedConnections[i]) + } + + await session.close() + await driver.close() + await server.exit() }) - it('should throw error when no records', done => { + it('should throw error when no records', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_no_records.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_no_records.script' + )) - it('should throw error when no TTL entry', done => { + it('should throw error when no TTL entry', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_no_ttl_field.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_no_ttl_field.script' + )) - it('should throw error when no servers entry', done => { + it('should throw error when no servers entry', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_no_servers_field.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_no_servers_field.script' + )) - it('should throw error when unparsable TTL entry', done => { + it('should throw error when unparsable TTL entry', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_unparsable_ttl.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_unparsable_ttl.script' + )) - it('should throw error when multiple records', done => { + it('should throw error when multiple records', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_multiple_records.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_multiple_records.script' + )) - it('should throw error on unparsable record', done => { + it('should throw error on unparsable record', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_unparsable_servers.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_unparsable_servers.script' + )) - it('should throw error when no routers', done => { + it('should throw error when no routers', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_no_routers.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_no_routers.script' + )) - it('should throw error when no readers', done => { + it('should throw error when no readers', () => testForProtocolError( - './test/resources/boltstub/v3/acquire_endpoints_no_readers.script', - done - ) - }) + './test/resources/boltstub/v3/acquire_endpoints_no_readers.script' + )) - it('should accept routing table with 1 router, 1 reader and 1 writer', done => { + it('should accept routing table with 1 router, 1 reader and 1 writer', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091'], readers: ['127.0.0.1:9092'], writers: ['127.0.0.1:9999'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 2 routers, 1 reader and 1 writer', done => { + it('should accept routing table with 2 routers, 1 reader and 1 writer', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091', '127.0.0.1:9092'], readers: ['127.0.0.1:9092'], writers: ['127.0.0.1:9999'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 1 router, 2 readers and 1 writer', done => { + it('should accept routing table with 1 router, 2 readers and 1 writer', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091'], readers: ['127.0.0.1:9092', '127.0.0.1:9093'], writers: ['127.0.0.1:9999'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 2 routers, 2 readers and 1 writer', done => { + it('should accept routing table with 2 routers, 2 readers and 1 writer', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091', '127.0.0.1:9092'], readers: ['127.0.0.1:9093', '127.0.0.1:9094'], writers: ['127.0.0.1:9999'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 1 router, 1 reader and 2 writers', done => { + it('should accept routing table with 1 router, 1 reader and 2 writers', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091'], readers: ['127.0.0.1:9092'], writers: ['127.0.0.1:9999', '127.0.0.1:9093'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 2 routers, 1 reader and 2 writers', done => { + it('should accept routing table with 2 routers, 1 reader and 2 writers', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091', '127.0.0.1:9092'], readers: ['127.0.0.1:9093'], writers: ['127.0.0.1:9999', '127.0.0.1:9094'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 1 router, 2 readers and 2 writers', done => { + it('should accept routing table with 1 router, 2 readers and 2 writers', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091'], readers: ['127.0.0.1:9092', '127.0.0.1:9093'], writers: ['127.0.0.1:9999', '127.0.0.1:9094'] }, - 9999, - done - ) - }) + 9999 + )) - it('should accept routing table with 2 routers, 2 readers and 2 writers', done => { + it('should accept routing table with 2 routers, 2 readers and 2 writers', () => testRoutingTableAcceptance( { routers: ['127.0.0.1:9091', '127.0.0.1:9092'], readers: ['127.0.0.1:9093', '127.0.0.1:9094'], writers: ['127.0.0.1:9999', '127.0.0.1:9095'] }, - 9999, - done - ) - }) + 9999 + )) - it('should send and receive bookmark', done => { + it('should send and receive bookmark', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_tx_with_bookmarks.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ bookmarks: ['neo4j:bookmark:v1:tx42'] }) - const tx = session.beginTransaction() - tx.run("CREATE (n {name:'Bob'})").then(() => { - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - session.close() - driver.close() - - router.exit(code1 => { - writer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) - }) + // When + const session = driver.session({ bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const tx = session.beginTransaction() + await tx.run("CREATE (n {name:'Bob'})") + await tx.commit() - it('should send initial bookmark without access mode', done => { - testWriteSessionWithAccessModeAndBookmark( - null, - 'neo4j:bookmark:v1:tx42', - done - ) - }) + // Then + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - it('should use write session mode and initial bookmark', done => { - testWriteSessionWithAccessModeAndBookmark( - WRITE, - 'neo4j:bookmark:v1:tx42', - done - ) + await session.close() + await driver.close() + await router.exit() + await writer.exit() }) - it('should use read session mode and initial bookmark', done => { + it('should send initial bookmark without access mode', () => + testWriteSessionWithAccessModeAndBookmark(null, 'neo4j:bookmark:v1:tx42')) + + it('should use write session mode and initial bookmark', () => + testWriteSessionWithAccessModeAndBookmark(WRITE, 'neo4j:bookmark:v1:tx42')) + + it('should use read session mode and initial bookmark', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/read_tx_with_bookmarks.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ - defaultAccessMode: READ, - bookmarks: ['neo4j:bookmark:v1:tx42'] - }) - const tx = session.beginTransaction() - tx.run('MATCH (n) RETURN n.name AS name').then(result => { - const records = result.records - expect(records.length).toEqual(2) - expect(records[0].get('name')).toEqual('Bob') - expect(records[1].get('name')).toEqual('Alice') - - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - session.close() - driver.close() - - router.exit(code1 => { - writer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const session = driver.session({ + defaultAccessMode: READ, + bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const tx = session.beginTransaction() + const result = await tx.run('MATCH (n) RETURN n.name AS name') + const records = result.records + expect(records.length).toEqual(2) + expect(records[0].get('name')).toEqual('Bob') + expect(records[1].get('name')).toEqual('Alice') + + await tx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + + await session.close() + await driver.close() + await router.exit() + await writer.exit() }) - it('should pass bookmark from transaction to transaction', done => { + it('should pass bookmark from transaction to transaction', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_2.script', 9001 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_read_tx_with_bookmarks.script', 9010 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ bookmarks: ['neo4j:bookmark:v1:tx42'] }) - const writeTx = session.beginTransaction() - writeTx.run("CREATE (n {name:'Bob'})").then(() => { - writeTx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - const readTx = session.beginTransaction() - readTx.run('MATCH (n) RETURN n.name AS name').then(result => { - const records = result.records - expect(records.length).toEqual(1) - expect(records[0].get('name')).toEqual('Bob') - - readTx.commit().then(() => { - expect(session.lastBookmark()).toEqual( - 'neo4j:bookmark:v1:tx424242' - ) + const session = driver.session({ bookmarks: ['neo4j:bookmark:v1:tx42'] }) + const writeTx = session.beginTransaction() + await writeTx.run("CREATE (n {name:'Bob'})") + await writeTx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - session.close() - driver.close() + const readTx = session.beginTransaction() + const result = await readTx.run('MATCH (n) RETURN n.name AS name') + const records = result.records + expect(records.length).toEqual(1) + expect(records[0].get('name')).toEqual('Bob') - router.exit(code1 => { - writer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + await readTx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242') + + await session.close() + await driver.close() + await router.exit() + await writer.exit() }) - it('should retry read transaction until success', done => { + it('should retry read transaction until success', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const brokenReader = boltStub.start( + const brokenReader = await boltStub.start( './test/resources/boltstub/v3/read_tx_dead.script', 9005 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9006 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session() - - let invocations = 0 - const resultPromise = session.readTransaction(tx => { - invocations++ - return tx.run('MATCH (n) RETURN n.name') - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session() - resultPromise.then(result => { - expect(result.records.length).toEqual(3) - expect(invocations).toEqual(2) - - session.close().then(() => { - driver.close() - router.exit(code1 => { - brokenReader.exit(code2 => { - reader.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) + let invocations = 0 + const result = await session.readTransaction(tx => { + invocations++ + return tx.run('MATCH (n) RETURN n.name') }) + + expect(result.records.length).toEqual(3) + expect(invocations).toEqual(2) + + await session.close() + await driver.close() + await router.exit() + await brokenReader.exit() + await reader.exit() }) - it('should retry write transaction until success', done => { + it('should retry write transaction until success', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const brokenWriter = boltStub.start( + const brokenWriter = await boltStub.start( './test/resources/boltstub/v3/write_tx_dead.script', 9007 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_tx.script', 9008 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session() - - let invocations = 0 - const resultPromise = session.writeTransaction(tx => { - invocations++ - return tx.run("CREATE (n {name:'Bob'})") - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session() - resultPromise.then(result => { - expect(result.records.length).toEqual(0) - expect(invocations).toEqual(2) - - session.close().then(() => { - driver.close() - router.exit(code1 => { - brokenWriter.exit(code2 => { - writer.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) + let invocations = 0 + const result = await session.writeTransaction(tx => { + invocations++ + return tx.run("CREATE (n {name:'Bob'})") }) + + expect(result.records.length).toEqual(0) + expect(invocations).toEqual(2) + + await session.close() + await driver.close() + await router.exit() + await brokenWriter.exit() + await writer.exit() }) - it('should retry read transaction until failure', done => { + it('should retry read transaction until failure', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const brokenReader1 = boltStub.start( + const brokenReader1 = await boltStub.start( './test/resources/boltstub/v3/read_tx_dead.script', 9005 ) - const brokenReader2 = boltStub.start( + const brokenReader2 = await boltStub.start( './test/resources/boltstub/v3/read_tx_dead.script', 9006 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session() + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session() - let clock - let invocations = 0 - const resultPromise = session.readTransaction(tx => { + let clock + let invocations = 0 + await expectAsync( + session.readTransaction(tx => { invocations++ if (invocations === 2) { // make retries stop after two invocations @@ -1595,56 +1299,48 @@ describe('#stub-routing routing driver with stub server', () => { } return tx.run('MATCH (n) RETURN n.name') }) - - resultPromise.catch(error => { - removeTimeMocking(clock) // uninstall lolex mocking to make test complete, boltkit uses timers - - expect(error.code).toEqual(SESSION_EXPIRED) - expect(invocations).toEqual(2) - - session.close().then(() => { - driver.close() - router.exit(code1 => { - brokenReader1.exit(code2 => { - brokenReader2.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) + ).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SESSION_EXPIRED }) - }) + ) + + removeTimeMocking(clock) // uninstall lolex mocking to make test complete, boltkit uses timers + + expect(invocations).toEqual(2) + + await session.close() + await driver.close() + await router.exit() + await brokenReader1.exit() + await brokenReader2.exit() }) - it('should retry write transaction until failure', done => { + it('should retry write transaction until failure', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const brokenWriter1 = boltStub.start( + const brokenWriter1 = await boltStub.start( './test/resources/boltstub/v3/write_tx_dead.script', 9007 ) - const brokenWriter2 = boltStub.start( + const brokenWriter2 = await boltStub.start( './test/resources/boltstub/v3/write_tx_dead.script', 9008 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session() + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session() - let clock = null - let invocations = 0 - const resultPromise = session.writeTransaction(tx => { + let clock = null + let invocations = 0 + await expectAsync( + session.writeTransaction(tx => { invocations++ if (invocations === 2) { // make retries stop after two invocations @@ -1652,1290 +1348,953 @@ describe('#stub-routing routing driver with stub server', () => { } return tx.run("CREATE (n {name:'Bob'})") }) - - resultPromise.catch(error => { - removeTimeMocking(clock) // uninstall lolex mocking to make test complete, boltStub uses timers - - expect(error.code).toEqual(SESSION_EXPIRED) - expect(invocations).toEqual(2) - - session.close().then(() => { - driver.close() - router.exit(code1 => { - brokenWriter1.exit(code2 => { - brokenWriter2.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) + ).toBeRejectedWith( + jasmine.objectContaining({ + code: neo4j.error.SESSION_EXPIRED }) - }) + ) + + removeTimeMocking(clock) // uninstall lolex mocking to make test complete, boltStub uses timers + + expect(invocations).toEqual(2) + + await session.close() + await driver.close() + await router.exit() + await brokenWriter1.exit() + await brokenWriter2.exit() }) - it('should retry read transaction and perform rediscovery until success', done => { + it('should retry read transaction and perform rediscovery until success', async () => { if (!boltStub.supported) { - done() return } - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9010 ) - const brokenReader1 = boltStub.start( + const brokenReader1 = await boltStub.start( './test/resources/boltstub/v3/read_tx_dead.script', 9005 ) - const brokenReader2 = boltStub.start( + const brokenReader2 = await boltStub.start( './test/resources/boltstub/v3/read_tx_dead.script', 9006 ) - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_3.script', 9001 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9002 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - const session = driver.session() - - let invocations = 0 - const resultPromise = session.readTransaction(tx => { - invocations++ - return tx.run('MATCH (n) RETURN n.name') - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + const session = driver.session() - resultPromise.then(result => { - expect(result.records.length).toEqual(3) - expect(invocations).toEqual(3) - - session.close().then(() => { - driver.close() - router1.exit(code1 => { - brokenReader1.exit(code2 => { - brokenReader2.exit(code3 => { - router2.exit(code4 => { - reader.exit(code5 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - expect(code5).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + let invocations = 0 + const result = await session.readTransaction(tx => { + invocations++ + return tx.run('MATCH (n) RETURN n.name') }) + + expect(result.records.length).toEqual(3) + expect(invocations).toEqual(3) + + await session.close() + await driver.close() + await Promise.all( + [router1, brokenReader1, brokenReader2, router2, reader].map(s => + s.exit() + ) + ) }) - it('should retry write transaction and perform rediscovery until success', done => { + it('should retry write transaction and perform rediscovery until success', async () => { if (!boltStub.supported) { - done() return } - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9010 ) - const brokenWriter1 = boltStub.start( + const brokenWriter1 = await boltStub.start( './test/resources/boltstub/v3/write_tx_dead.script', 9007 ) - const brokenWriter2 = boltStub.start( + const brokenWriter2 = await boltStub.start( './test/resources/boltstub/v3/write_tx_dead.script', 9008 ) - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_3.script', 9002 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_tx.script', 9009 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - const session = driver.session() - - let invocations = 0 - const resultPromise = session.writeTransaction(tx => { - invocations++ - return tx.run("CREATE (n {name:'Bob'})") - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + const session = driver.session() - resultPromise.then(result => { - expect(result.records.length).toEqual(0) - expect(invocations).toEqual(3) - - session.close().then(() => { - driver.close() - router1.exit(code1 => { - brokenWriter1.exit(code2 => { - brokenWriter2.exit(code3 => { - router2.exit(code4 => { - writer.exit(code5 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - expect(code5).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + let invocations = 0 + const result = await session.writeTransaction(tx => { + invocations++ + return tx.run("CREATE (n {name:'Bob'})") }) + + expect(result.records.length).toEqual(0) + expect(invocations).toEqual(3) + + await session.close() + await driver.close() + await Promise.all( + [router1, brokenWriter1, brokenWriter2, router2, writer].map(s => + s.exit() + ) + ) }) - it('should use seed router for rediscovery when all other routers are dead', done => { + it('should use seed router for rediscovery when all other routers are dead', async () => { if (!boltStub.supported) { - done() return } // use scripts that exit eagerly when they are executed to simulate failed servers - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_and_exit.script', 9010 ) - const tmpReader = boltStub.start( + const tmpReader = await boltStub.start( './test/resources/boltstub/v3/read_and_exit.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - - // run a dummy query to force routing table initialization - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(result => { - expect(result.records.length).toEqual(3) - session.close().then(() => { - // stop existing router and reader - router1.exit(code1 => { - tmpReader.exit(code2 => { - // at this point previously used router and reader should be dead - expect(code1).toEqual(0) - expect(code2).toEqual(0) - - // start new router on the same port with different script that contains itself as reader - const router2 = boltStub.start( - './test/resources/boltstub/v3/acquire_endpoints_self_as_reader.script', - 9010 - ) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - boltStub.run(() => { - session - .readTransaction(tx => - tx.run('MATCH (n) RETURN n.name AS name') - ) - .then(result => { - const records = result.records - expect(records.length).toEqual(2) - expect(records[0].get('name')).toEqual('Bob') - expect(records[1].get('name')).toEqual('Alice') - - session.close().then(() => { - driver.close() - router2.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) + // run a dummy query to force routing table initialization + const session = driver.session({ defaultAccessMode: READ }) + const result1 = await session.run('MATCH (n) RETURN n.name') + expect(result1.records.length).toEqual(3) + await session.close() + // stop existing router and reader + await router1.exit() + await tmpReader.exit() + + // start new router on the same port with different script that contains itself as reader + const router2 = await boltStub.start( + './test/resources/boltstub/v3/acquire_endpoints_self_as_reader.script', + 9010 + ) + + const result2 = await session.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name AS name') + ) + const records = result2.records + expect(records.length).toEqual(2) + expect(records[0].get('name')).toEqual('Bob') + expect(records[1].get('name')).toEqual('Alice') + + await session.close() + await driver.close() + await router2.exit() }) - it('should use resolved seed router addresses for rediscovery when all other routers are dead', done => { + it('should use resolved seed router addresses for rediscovery when all other routers are dead', async () => { if (!boltStub.supported) { - done() return } - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_and_exit.script', 9011 ) // start new router on a different port to emulate host name resolution // this router uses different script that contains itself as reader - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_self_as_reader.script', 9009 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - // make seed address resolve to 3 different addresses (only last one has backing stub server): - setupFakeHostNameResolution(driver, '127.0.0.1:9010', [ - '127.0.0.1:9011', - '127.0.0.1:9012', - '127.0.0.1:9009' - ]) - const session = driver.session() - - session - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name AS name')) - .then(result => { - const records = result.records - expect(records.length).toEqual(2) - expect(records[0].get('name')).toEqual('Bob') - expect(records[1].get('name')).toEqual('Alice') - - session.close().then(() => { - driver.close() - router1.exit(code1 => { - router2.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + // make seed address resolve to 3 different addresses (only last one has backing stub server): + setupFakeHostNameResolution(driver, '127.0.0.1:9010', [ + '127.0.0.1:9011', + '127.0.0.1:9012', + '127.0.0.1:9009' + ]) + const session = driver.session() + + const result = await session.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name AS name') + ) + + const records = result.records + expect(records.length).toEqual(2) + expect(records[0].get('name')).toEqual('Bob') + expect(records[1].get('name')).toEqual('Alice') + + await session.close() + await driver.close() + await router1.exit() + await router2.exit() }) - it('should send routing context to server', done => { + it('should send routing context to server', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_with_context.script', 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver( - 'neo4j://127.0.0.1:9001/?policy=my_policy®ion=china' - ) - const session = driver.session() - session.run('MATCH (n) RETURN n.name AS name').then(result => { - const names = result.records.map(record => record.get('name')) - expect(names).toEqual(['Alice', 'Bob']) - - session.close().then(() => { - driver.close() - router.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver( + 'neo4j://127.0.0.1:9001/?policy=my_policy®ion=china' + ) + const session = driver.session() + const result = await session.run('MATCH (n) RETURN n.name AS name') + const names = result.records.map(record => record.get('name')) + expect(names).toEqual(['Alice', 'Bob']) + + await session.close() + await driver.close() + await router.exit() }) - it('should treat routing table with single router as valid', done => { + it('should treat routing table with single router as valid', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_one_router.script', 9010 ) - const reader1 = boltStub.start( + const reader1 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9003 ) - const reader2 = boltStub.start( + const reader2 = await boltStub.start( './test/resources/boltstub/v3/read.script', 9004 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - const session = driver.session({ defaultAccessMode: READ }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(result1 => { - expect(result1.records.length).toEqual(3) - expect(result1.summary.server.address).toEqual('127.0.0.1:9003') + const result1 = await session.run('MATCH (n) RETURN n.name') + expect(result1.records.length).toEqual(3) + expect(result1.summary.server.address).toEqual('127.0.0.1:9003') - session.run('MATCH (n) RETURN n.name').then(result2 => { - expect(result2.records.length).toEqual(3) - expect(result2.summary.server.address).toEqual('127.0.0.1:9004') + const result2 = await session.run('MATCH (n) RETURN n.name') + expect(result2.records.length).toEqual(3) + expect(result2.summary.server.address).toEqual('127.0.0.1:9004') - session.close().then(() => { - driver.close() - router.exit(code1 => { - reader1.exit(code2 => { - reader2.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + await session.close() + await driver.close() + await router.exit() + await reader1.exit() + await reader2.exit() }) - it('should use routing table without writers for reads', done => { + it('should use routing table without writers for reads', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9001 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(result => { - session.close().then(() => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - - driver.close() - - router.exit(code1 => { - reader.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) + const session = driver.session({ defaultAccessMode: READ }) + const result = await session.run('MATCH (n) RETURN n.name') + await session.close() + expect(result.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + await driver.close() + await router.exit() + await reader.exit() }) - it('should serve reads but fail writes when no writers available', done => { + it('should serve reads but fail writes when no writers available', async () => { if (!boltStub.supported) { - done() return } - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9001 ) - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9002 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - - const readSession = driver.session() - - readSession - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) - .then(result => { - readSession.close().then(() => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const writeSession = driver.session({ defaultAccessMode: WRITE }) - writeSession.run("CREATE (n {name:'Bob'})").catch(error => { - expect(error.code).toEqual(neo4j.error.SESSION_EXPIRED) + const readSession = driver.session() + const result = await readSession.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name') + ) + await readSession.close() + expect(result.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) - driver.close() + const writeSession = driver.session({ defaultAccessMode: WRITE }) + await expectAsync( + writeSession.run("CREATE (n {name:'Bob'})") + ).toBeRejectedWith( + jasmine.objectContaining({ code: neo4j.error.SESSION_EXPIRED }) + ) - router1.exit(code1 => { - router2.exit(code2 => { - reader.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) + await driver.close() + await router1.exit() + await router2.exit() + await reader.exit() }) - it('should accept routing table without writers and then rediscover', done => { + it('should accept routing table without writers and then rediscover', async () => { if (!boltStub.supported) { - done() return } // first router does not have itself in the resulting routing table so connection // towards it will be closed after rediscovery - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9001 ) - let router2 = null - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9005 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const readSession = driver.session() + const readSession = driver.session() - readSession - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) - .then(result => { - readSession.close().then(() => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) + const result1 = await readSession.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name') + ) + await readSession.close() + expect(result1.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) - // start another router which knows about writes, use same address as the initial router - router2 = boltStub.start( - './test/resources/boltstub/v3/acquire_endpoints.script', - 9002 - ) - boltStub.run(() => { - const writeSession = driver.session({ defaultAccessMode: WRITE }) - writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close().then(() => { - expect(result.records).toEqual([]) - - driver.close() - - router1.exit(code1 => { - router2.exit(code2 => { - reader.exit(code3 => { - writer.exit(code4 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) - }) + // start another router which knows about writes, use same address as the initial router + const router2 = await boltStub.start( + './test/resources/boltstub/v3/acquire_endpoints.script', + 9002 + ) + const writeSession = driver.session({ defaultAccessMode: WRITE }) + const result2 = await writeSession.run("CREATE (n {name:'Bob'})") + await writeSession.close() + expect(result2.records).toEqual([]) + + await driver.close() + await Promise.all([router1, router2, reader, writer].map(s => s.exit())) }) - it('should use resolved seed router for discovery after accepting a table without writers', done => { + it('should use resolved seed router for discovery after accepting a table without writers', async () => { if (!boltStub.supported) { - done() return } - const seedRouter = boltStub.start( + const seedRouter = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_writers.script', 9001 ) - const resolvedSeedRouter = boltStub.start( + const resolvedSeedRouter = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9020 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const readSession = driver.session({ defaultAccessMode: READ }) - readSession.run('MATCH (n) RETURN n.name').then(result => { - readSession.close().then(() => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - - setupFakeHostNameResolution(driver, '127.0.0.1:9001', [ - '127.0.0.1:9020' - ]) - - const writeSession = driver.session({ defaultAccessMode: WRITE }) - writeSession.run("CREATE (n {name:'Bob'})").then(result => { - writeSession.close().then(() => { - expect(result.records).toEqual([]) - - driver.close() - - seedRouter.exit(code1 => { - resolvedSeedRouter.exit(code2 => { - reader.exit(code3 => { - writer.exit(code4 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - done() - }) - }) - }) - }) - }) - }) - }) - }) - }) + const readSession = driver.session({ defaultAccessMode: READ }) + const result1 = await readSession.run('MATCH (n) RETURN n.name') + await readSession.close() + expect(result1.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + setupFakeHostNameResolution(driver, '127.0.0.1:9001', ['127.0.0.1:9020']) + + const writeSession = driver.session({ defaultAccessMode: WRITE }) + const result2 = await writeSession.run("CREATE (n {name:'Bob'})") + await writeSession.close() + expect(result2.records).toEqual([]) + + await driver.close() + await Promise.all( + [seedRouter, resolvedSeedRouter, reader, writer].map(s => s.exit()) + ) }) - it('should fail rediscovery on auth error', done => { + it('should fail rediscovery on auth error', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/no_auth.script', 9010 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - const session = driver.session() - session.run('RETURN 1').catch(error => { - expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized') - expect(error.message).toEqual('Some server auth error message') - - session.close().then(() => { - driver.close() - router.exit(code => { - expect(code).toEqual(0) - done() - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + const session = driver.session() + await expectAsync(session.run('RETURN 1')).toBeRejectedWith( + jasmine.objectContaining({ + code: 'Neo.ClientError.Security.Unauthorized', + message: 'Some server auth error message' }) - }) + ) + + await session.close() + await driver.close() + await router.exit() }) - it('should send multiple bookmarks', done => { + it('should send multiple bookmarks', async () => { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9010 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_tx_with_multiple_bookmarks.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - - const bookmarks = [ - 'neo4j:bookmark:v1:tx5', - 'neo4j:bookmark:v1:tx29', - 'neo4j:bookmark:v1:tx94', - 'neo4j:bookmark:v1:tx56', - 'neo4j:bookmark:v1:tx16', - 'neo4j:bookmark:v1:tx68' - ] - const session = driver.session({ defaultAccessMode: WRITE, bookmarks }) - const tx = session.beginTransaction() - - tx.run(`CREATE (n {name:'Bob'})`).then(() => { - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx95') - - session.close() - driver.close() - - router.exit(code1 => { - writer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + + const bookmarks = [ + 'neo4j:bookmark:v1:tx5', + 'neo4j:bookmark:v1:tx29', + 'neo4j:bookmark:v1:tx94', + 'neo4j:bookmark:v1:tx56', + 'neo4j:bookmark:v1:tx16', + 'neo4j:bookmark:v1:tx68' + ] + const session = driver.session({ defaultAccessMode: WRITE, bookmarks }) + const tx = session.beginTransaction() + + await tx.run(`CREATE (n {name:'Bob'})`) + await tx.commit() + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx95') + + await session.close() + await driver.close() + await router.exit() + await writer.exit() }) - it('should forget writer on database unavailable error', done => { + it('should forget writer on database unavailable error', () => testAddressPurgeOnDatabaseError( './test/resources/boltstub/v3/write_database_unavailable.script', `CREATE (n {name:'Bob'})`, - WRITE, - done - ) - }) + WRITE + )) - it('should forget reader on database unavailable error', done => { + it('should forget reader on database unavailable error', () => testAddressPurgeOnDatabaseError( './test/resources/boltstub/v3/read_database_unavailable.script', `RETURN 1`, - READ, - done - ) - }) + READ + )) - it('should use resolver function that returns array during first discovery', done => { - testResolverFunctionDuringFirstDiscovery(['127.0.0.1:9010'], done) - }) + it('should use resolver function that returns array during first discovery', () => + testResolverFunctionDuringFirstDiscovery(['127.0.0.1:9010'])) - it('should use resolver function that returns promise during first discovery', done => { + it('should use resolver function that returns promise during first discovery', () => testResolverFunctionDuringFirstDiscovery( - Promise.resolve(['127.0.0.1:9010']), - done - ) - }) + Promise.resolve(['127.0.0.1:9010']) + )) - it('should fail first discovery when configured resolver function throws', done => { - const failureFunction = () => { - throw new Error('Broken resolver') - } + it('should fail first discovery when configured resolver function throws', () => testResolverFunctionFailureDuringFirstDiscovery( - failureFunction, + () => { + throw new Error('Broken resolver') + }, null, - 'Broken resolver', - done - ) - }) + 'Broken resolver' + )) - it('should fail first discovery when configured resolver function returns no addresses', done => { - const failureFunction = () => { - return [] - } + it('should fail first discovery when configured resolver function returns no addresses', () => testResolverFunctionFailureDuringFirstDiscovery( - failureFunction, + () => [], SERVICE_UNAVAILABLE, - 'No routing servers available', - done - ) - }) + 'No routing servers available' + )) - it('should fail first discovery when configured resolver function returns a string instead of array of addresses', done => { - const failureFunction = () => { - return 'Hello' - } + it('should fail first discovery when configured resolver function returns a string instead of array of addresses', () => testResolverFunctionFailureDuringFirstDiscovery( - failureFunction, + () => 'Hello', null, - 'Configured resolver function should either return an array of addresses', - done - ) - }) + 'Configured resolver function should either return an array of addresses' + )) - it('should use resolver function during rediscovery when existing routers fail', done => { + it('should use resolver function during rediscovery when existing routers fail', async () => { if (!boltStub.supported) { - done() return } - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_two_servers_set_1.script', 9001 ) - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9042 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9005 ) - boltStub.run(() => { - const resolverFunction = address => { - if (address === '127.0.0.1:9000') { - return ['127.0.0.1:9010', '127.0.0.1:9001', '127.0.0.1:9042'] - } - throw new Error(`Unexpected address ${address}`) + const resolverFunction = address => { + if (address === '127.0.0.1:9000') { + return ['127.0.0.1:9010', '127.0.0.1:9001', '127.0.0.1:9042'] } + throw new Error(`Unexpected address ${address}`) + } - const driver = boltStub.newDriver('neo4j://127.0.0.1:9000', { - resolver: resolverFunction - }) - - const session = driver.session({ defaultAccessMode: READ }) - // run a query that should trigger discovery against 9001 and then read from it - session - .run('MATCH (n) RETURN n.name AS name') - .then(result => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Alice', - 'Bob', - 'Eve' - ]) - - // 9001 should now exit and read transaction should fail to read from all existing readers - // it should then rediscover using addresses from resolver, only 9042 of them works and can respond with table containing reader 9005 - session - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) - .then(result => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - - assertHasRouters(driver, [ - '127.0.0.1:9001', - '127.0.0.1:9002', - '127.0.0.1:9003' - ]) - assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) - assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) - - session.close().then(() => { - driver.close() - router1.exit(code1 => { - router2.exit(code2 => { - reader.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) - }) - .catch(done.fail) - }) - .catch(done.fail) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9000', { + resolver: resolverFunction }) - }) - it('should connect to cluster when disableLosslessIntegers is on', done => { + const session = driver.session({ defaultAccessMode: READ }) + // run a query that should trigger discovery against 9001 and then read from it + const result1 = await session.run('MATCH (n) RETURN n.name AS name') + expect(result1.records.map(record => record.get(0))).toEqual([ + 'Alice', + 'Bob', + 'Eve' + ]) + + // 9001 should now exit and read transaction should fail to read from all existing readers + // it should then rediscover using addresses from resolver, only 9042 of them works and can respond with table containing reader 9005 + const result2 = await session.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name') + ) + expect(result2.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + assertHasRouters(driver, [ + '127.0.0.1:9001', + '127.0.0.1:9002', + '127.0.0.1:9003' + ]) + assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006']) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008']) + + await session.close() + await driver.close() + await router1.exit() + await router2.exit() + await reader.exit() + }) + + it('should connect to cluster when disableLosslessIntegers is on', () => testDiscoveryAndReadQueryInAutoCommitTx( './test/resources/boltstub/v3/acquire_endpoints.script', - { disableLosslessIntegers: true }, - done - ) - }) + { disableLosslessIntegers: true } + )) - it('should send read access mode on statement metadata', done => { + it('should send read access mode on statement metadata', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session.run('MATCH (n) RETURN n.name').then(res => { - session.close() - - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: READ }) + const res = await session.run('MATCH (n) RETURN n.name') + await session.close() + + // Then + expect(res.records[0].get('n.name')).toEqual('Bob') + expect(res.records[1].get('n.name')).toEqual('Alice') + expect(res.records[2].get('n.name')).toEqual('Tina') + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should send read access mode on statement metadata with read transaction', done => { + it('should send read access mode on statement metadata with read transaction', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: READ }) - session - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) - .then(res => { - session.close() - - // Then - expect(res.records[0].get('n.name')).toEqual('Bob') - expect(res.records[1].get('n.name')).toEqual('Alice') - expect(res.records[2].get('n.name')).toEqual('Tina') - driver.close() - seedServer.exit(code1 => { - readServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: READ }) + const res = await session.readTransaction(tx => + tx.run('MATCH (n) RETURN n.name') + ) + await session.close() + + // Then + expect(res.records[0].get('n.name')).toEqual('Bob') + expect(res.records[1].get('n.name')).toEqual('Alice') + expect(res.records[2].get('n.name')).toEqual('Tina') + + await driver.close() + await seedServer.exit() + await readServer.exit() }) - it('should not send write access mode on statement metadata', done => { + it('should not send write access mode on statement metadata', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: WRITE }) - session.run("CREATE (n {name:'Bob'})").then(res => { - session.close() - driver.close() - seedServer.exit(code1 => { - writeServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: WRITE }) + await session.run("CREATE (n {name:'Bob'})") + + await session.close() + await driver.close() + await seedServer.exit() + await writeServer.exit() }) - it('should not send write access mode on statement metadata with write transaction', done => { + it('should not send write access mode on statement metadata with write transaction', async () => { if (!boltStub.supported) { - done() return } // Given - const seedServer = boltStub.start( + const seedServer = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( './test/resources/boltstub/v3/write_tx.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ defaultAccessMode: WRITE }) - session - .writeTransaction(tx => tx.run("CREATE (n {name:'Bob'})")) - .then(res => { - session.close() - driver.close() - seedServer.exit(code1 => { - writeServer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ defaultAccessMode: WRITE }) + await session.writeTransaction(tx => tx.run("CREATE (n {name:'Bob'})")) + + await session.close() + await driver.close() + await seedServer.exit() + await writeServer.exit() }) - it('should revert to initial router if the only known router returns invalid routing table', done => { + it('should revert to initial router if the only known router returns invalid routing table', async () => { if (!boltStub.supported) { - done() return } // the first seed to get the routing table // the returned routing table includes a non-reachable read-server and points to only one router // which will return an invalid routing table - const router1 = boltStub.start( + const router1 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_2.script', 9001 ) // returns an empty routing table - const router2 = boltStub.start( + const router2 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_no_servers.script', 9004 ) // returns a normal routing table - const router3 = boltStub.start( + const router3 = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints_three_servers_set_1.script', 9003 ) // ordinary read server - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read_tx.script', 9002 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://my.virtual.host:8080', { - resolver: address => ['127.0.0.1:9001', '127.0.0.1:9003'] - }) - - const session = driver.session({ defaultAccessMode: READ }) - session - .readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) - .then(res => { - session.close() - driver.close() - router1.exit(code1 => { - router2.exit(code2 => { - router3.exit(code3 => { - reader.exit(code4 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - expect(code4).toEqual(0) - done() - }) - }) - }) - }) - }) - .catch(error => done.fail(error)) + const driver = boltStub.newDriver('neo4j://my.virtual.host:8080', { + resolver: address => ['127.0.0.1:9001', '127.0.0.1:9003'] }) + + const session = driver.session({ defaultAccessMode: READ }) + await session.readTransaction(tx => tx.run('MATCH (n) RETURN n.name')) + + await session.close() + await driver.close() + await Promise.all([router1, router2, router3, reader].map(s => s.exit())) }) describe('multi-Database', () => { - function verifyDiscoverAndRead (script, database, done) { + async function verifyDiscoverAndRead (script, database) { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/v4/acquire_endpoints_${database || 'default_database'}.script`, 9001 ) - const readServer = boltStub.start( + const readServer = await boltStub.start( `./test/resources/boltstub/v4/${script}.script`, 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ - database: database, - defaultAccessMode: READ - }) - session.run('MATCH (n) RETURN n.name').then(() => { - session.close() - // Then - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9005') - ).toBeTruthy() - assertHasRouters( - driver, - ['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003'], - database - ) - assertHasReaders( - driver, - ['127.0.0.1:9005', '127.0.0.1:9006'], - database - ) - assertHasWriters( - driver, - ['127.0.0.1:9007', '127.0.0.1:9008'], - database - ) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ + database: database, + defaultAccessMode: READ + }) + await session.run('MATCH (n) RETURN n.name') + await session.close() + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9005')).toBeTruthy() + assertHasRouters( + driver, + ['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003'], + database + ) + assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006'], database) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008'], database) - driver.close() - server.exit(code => { - readServer.exit(readCode => { - expect(code).toEqual(0) - expect(readCode).toEqual(0) - done() - }) - }) - }) - }) + await driver.close() + await server.exit() + await readServer.exit() } - function verifyDiscoverAndWrite (script, database, done) { + async function verifyDiscoverAndWrite (script, database) { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/v4/acquire_endpoints_${database || 'default_database'}.script`, 9001 ) - const writeServer = boltStub.start( + const writeServer = await boltStub.start( `./test/resources/boltstub/v4/${script}.script`, 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ database: database }) - session.run("CREATE (n {name:'Bob'})").then(() => { - session.close() - // Then - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9001') - ).toBeTruthy() - expect( - hasAddressInConnectionPool(driver, '127.0.0.1:9007') - ).toBeTruthy() - assertHasRouters( - driver, - ['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003'], - database - ) - assertHasReaders( - driver, - ['127.0.0.1:9005', '127.0.0.1:9006'], - database - ) - assertHasWriters( - driver, - ['127.0.0.1:9007', '127.0.0.1:9008'], - database - ) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ database: database }) + await session.run("CREATE (n {name:'Bob'})") + await session.close() + // Then + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9001')).toBeTruthy() + expect(hasAddressInConnectionPool(driver, '127.0.0.1:9007')).toBeTruthy() + assertHasRouters( + driver, + ['127.0.0.1:9001', '127.0.0.1:9002', '127.0.0.1:9003'], + database + ) + assertHasReaders(driver, ['127.0.0.1:9005', '127.0.0.1:9006'], database) + assertHasWriters(driver, ['127.0.0.1:9007', '127.0.0.1:9008'], database) - driver.close() - server.exit(code => { - writeServer.exit(writeCode => { - expect(code).toEqual(0) - expect(writeCode).toEqual(0) - done() - }) - }) - }) - }) + await driver.close() + await server.exit() + await writeServer.exit() } - it('should discover servers for default database and read', done => { - verifyDiscoverAndRead('read', '', done) - }) + it('should discover servers for default database and read', () => + verifyDiscoverAndRead('read', '')) - it('should discover servers for aDatabase and read', done => { - verifyDiscoverAndRead('read_from_aDatabase', 'aDatabase', done) - }) + it('should discover servers for aDatabase and read', () => + verifyDiscoverAndRead('read_from_aDatabase', 'aDatabase')) - it('should discover servers for default database and write', done => { - verifyDiscoverAndWrite('write', '', done) - }) + it('should discover servers for default database and write', () => + verifyDiscoverAndWrite('write', '')) - it('should discover servers for aDatabase and write', done => { - verifyDiscoverAndWrite('write_to_aDatabase', 'aDatabase', done) - }) + it('should discover servers for aDatabase and write', () => + verifyDiscoverAndWrite('write_to_aDatabase', 'aDatabase')) - it('should fail discovery if database not found', done => { + it('should fail discovery if database not found', async () => { if (!boltStub.supported) { - done() return } // Given - const server = boltStub.start( + const server = await boltStub.start( `./test/resources/boltstub/v4/acquire_endpoints_db_not_found.script`, 9001 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - // When - const session = driver.session({ database: 'aDatabase' }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + // When + const session = driver.session({ database: 'aDatabase' }) - session.run('CREATE ()').catch(error => { - // Then - expect(error.code).toEqual( - 'Neo.ClientError.Database.DatabaseNotFound' - ) - expect(error.message).toEqual('database not found') - - session.close() - driver.close() - server.exit(code => { - expect(code).toEqual(0) - done() - }) + await expectAsync(session.run('CREATE ()')).toBeRejectedWith( + jasmine.objectContaining({ + code: 'Neo.ClientError.Database.DatabaseNotFound', + message: 'database not found' }) - }) + ) + + await session.close() + await driver.close() + await server.exit() }) - it('should try next server for empty routing table response', done => { + it('should try next server for empty routing table response', async () => { if (!boltStub.supported) { - done() return } // Given - const router1 = boltStub.start( + const router1 = await boltStub.start( `./test/resources/boltstub/v4/acquire_endpoints_aDatabase_no_servers.script`, 9001 ) - const router2 = boltStub.start( + const router2 = await boltStub.start( `./test/resources/boltstub/v4/acquire_endpoints_aDatabase.script`, 9002 ) - const reader1 = boltStub.start( + const reader1 = await boltStub.start( `./test/resources/boltstub/v4/read_from_aDatabase.script`, 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9000', { - resolver: address => [ - 'neo4j://127.0.0.1:9001', - 'neo4j://127.0.0.1:9002' - ] - }) - - // When - const session = driver.session({ - database: 'aDatabase', - defaultAccessMode: READ - }) - session.run('MATCH (n) RETURN n.name').then(result => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - - session.close() - driver.close() - router1.exit(code1 => { - router2.exit(code2 => { - reader1.exit(code3 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - expect(code3).toEqual(0) - done() - }) - }) - }) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9000', { + resolver: address => [ + 'neo4j://127.0.0.1:9001', + 'neo4j://127.0.0.1:9002' + ] }) + + // When + const session = driver.session({ + database: 'aDatabase', + defaultAccessMode: READ + }) + const result = await session.run('MATCH (n) RETURN n.name') + expect(result.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + await session.close() + await driver.close() + await router1.exit() + await router2.exit() + await reader1.exit() }) }) - function testAddressPurgeOnDatabaseError (script, query, accessMode, done) { + async function testAddressPurgeOnDatabaseError (script, query, accessMode) { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9010 ) const serverPort = accessMode === READ ? 9005 : 9007 const serverAddress = '127.0.0.1:' + serverPort - const server = boltStub.start(script, serverPort) - - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') - - const session = driver.session({ defaultAccessMode: accessMode }) - session.run(query).catch(error => { - expect(error.message).toEqual('Database is busy doing store copy') - expect(error.code).toEqual( - 'Neo.TransientError.General.DatabaseUnavailable' - ) - - expect(hasAddressInConnectionPool(driver, serverAddress)).toBeFalsy() - expect(hasRouterInRoutingTable(driver, serverAddress)).toBeFalsy() - expect(hasReaderInRoutingTable(driver, serverAddress)).toBeFalsy() - expect(hasWriterInRoutingTable(driver, serverAddress)).toBeFalsy() - - session.close().then(() => { - driver.close() - - router.exit(code1 => { - server.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) + const server = await boltStub.start(script, serverPort) + + const driver = boltStub.newDriver('neo4j://127.0.0.1:9010') + + const session = driver.session({ defaultAccessMode: accessMode }) + await expectAsync(session.run(query)).toBeRejectedWith( + jasmine.objectContaining({ + code: 'Neo.TransientError.General.DatabaseUnavailable', + message: 'Database is busy doing store copy' }) - }) + ) + + expect(hasAddressInConnectionPool(driver, serverAddress)).toBeFalsy() + expect(hasRouterInRoutingTable(driver, serverAddress)).toBeFalsy() + expect(hasReaderInRoutingTable(driver, serverAddress)).toBeFalsy() + expect(hasWriterInRoutingTable(driver, serverAddress)).toBeFalsy() + + await session.close() + await driver.close() + await router.exit() + await server.exit() } function moveTime30SecondsForward () { @@ -2951,123 +2310,93 @@ describe('#stub-routing routing driver with stub server', () => { } } - function testWriteSessionWithAccessModeAndBookmark ( + async function testWriteSessionWithAccessModeAndBookmark ( accessMode, - bookmark, - done + bookmark ) { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9001 ) - const writer = boltStub.start( + const writer = await boltStub.start( './test/resources/boltstub/v3/write_tx_with_bookmarks.script', 9007 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - const session = driver.session({ - defaultAccessMode: accessMode, - bookmarks: [bookmark] - }) - const tx = session.beginTransaction() - tx.run("CREATE (n {name:'Bob'})").then(() => { - tx.commit().then(() => { - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') - - session.close() - driver.close() - - router.exit(code1 => { - writer.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) + // When + const session = driver.session({ + defaultAccessMode: accessMode, + bookmarks: [bookmark] }) + const tx = session.beginTransaction() + await tx.run("CREATE (n {name:'Bob'})") + await tx.commit() + + // Then + expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + + await session.close() + await driver.close() + await router.exit() + await writer.exit() } - function testDiscoveryAndReadQueryInAutoCommitTx ( + async function testDiscoveryAndReadQueryInAutoCommitTx ( routerScript, - driverConfig, - done + driverConfig ) { if (!boltStub.supported) { - done() return } - const router = boltStub.start(routerScript, 9001) - const reader = boltStub.start( + const router = await boltStub.start(routerScript, 9001) + const reader = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001', driverConfig) - - const session = driver.session({ defaultAccessMode: READ }) - session - .run('MATCH (n) RETURN n.name') - .then(result => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - session.close() - driver.close() - router.exit(code1 => { - reader.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - .catch(done.fail) - }) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001', driverConfig) + + const session = driver.session({ defaultAccessMode: READ }) + const result = await session.run('MATCH (n) RETURN n.name') + expect(result.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + await session.close() + await driver.close() + await router.exit() + await reader.exit() } - function testForProtocolError (scriptFile, done) { + async function testForProtocolError (scriptFile) { if (!boltStub.supported) { - done() return } - const server = boltStub.start(scriptFile, 9001) - - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') - - const session = driver.session() - session.run('MATCH (n) RETURN n.name').catch(error => { - expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) + const server = await boltStub.start(scriptFile, 9001) + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + const session = driver.session() - session.close() - driver.close() + await expectAsync(session.run('MATCH (n) RETURN n.name')).toBeRejectedWith( + jasmine.objectContaining({ code: neo4j.error.SERVICE_UNAVAILABLE }) + ) - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + await session.close() + await driver.close() + await server.exit() } - function testRoutingTableAcceptance (clusterMembers, port, done) { + async function testRoutingTableAcceptance (clusterMembers, port) { if (!boltStub.supported) { - done() return } @@ -3077,28 +2406,21 @@ describe('#stub-routing routing driver with stub server', () => { readers: joinStrings(readers), writers: joinStrings(writers) } - const server = boltStub.startWithTemplate( + const server = await boltStub.startWithTemplate( './test/resources/boltstub/v3/acquire_endpoints_template.script', params, port ) - boltStub.run(() => { - const driver = boltStub.newDriver('neo4j://127.0.0.1:' + port) + const driver = boltStub.newDriver('neo4j://127.0.0.1:' + port) - const session = driver.session() - session.run('MATCH (n) RETURN n.name').then(result => { - expect(result.summary.server.address).toEqual('127.0.0.1:' + port) - - session.close() - driver.close() + const session = driver.session() + const result = await session.run('MATCH (n) RETURN n.name') + expect(result.summary.server.address).toEqual('127.0.0.1:' + port) - server.exit(code => { - expect(code).toEqual(0) - done() - }) - }) - }) + await session.close() + await driver.close() + await server.exit() } function setUpPoolToMemorizeAllAcquiredAndReleasedConnections ( @@ -3222,69 +2544,63 @@ describe('#stub-routing routing driver with stub server', () => { return Object.keys(driver._connectionProvider._openConnections).length } - function testResolverFunctionDuringFirstDiscovery (resolutionResult, done) { + async function testResolverFunctionDuringFirstDiscovery (resolutionResult) { if (!boltStub.supported) { - done() return } - const router = boltStub.start( + const router = await boltStub.start( './test/resources/boltstub/v3/acquire_endpoints.script', 9010 ) - const reader = boltStub.start( + const reader = await boltStub.start( './test/resources/boltstub/v3/read.script', 9005 ) - boltStub.run(() => { - const resolverFunction = address => { - if (address === 'neo4j.com:7687') { - return resolutionResult - } - throw new Error(`Unexpected address ${address}`) + const resolverFunction = address => { + if (address === 'neo4j.com:7687') { + return resolutionResult } + throw new Error(`Unexpected address ${address}`) + } - const driver = boltStub.newDriver('neo4j://neo4j.com', { - resolver: resolverFunction - }) - - const session = driver.session({ defaultAccessMode: READ }) - session - .run('MATCH (n) RETURN n.name') - .then(result => { - expect(result.records.map(record => record.get(0))).toEqual([ - 'Bob', - 'Alice', - 'Tina' - ]) - session.close().then(() => { - driver.close() - - router.exit(code1 => { - reader.exit(code2 => { - expect(code1).toEqual(0) - expect(code2).toEqual(0) - done() - }) - }) - }) - }) - .catch(done.fail) + const driver = boltStub.newDriver('neo4j://neo4j.com', { + resolver: resolverFunction }) + + const session = driver.session({ defaultAccessMode: READ }) + const result = await session.run('MATCH (n) RETURN n.name') + + expect(result.records.map(record => record.get(0))).toEqual([ + 'Bob', + 'Alice', + 'Tina' + ]) + + await session.close() + await driver.close() + await router.exit() + await reader.exit() } - function testResolverFunctionFailureDuringFirstDiscovery ( + async function testResolverFunctionFailureDuringFirstDiscovery ( failureFunction, expectedCode, - expectedMessage, - done + expectedMessage ) { if (!boltStub.supported) { - done() return } + const expectedError = {} + if (expectedCode) { + expectedError.code = expectedCode + } + if (expectedMessage) { + expectedError.message = jasmine.stringMatching(expectedMessage) + } + const resolverFunction = address => { if (address === 'neo4j.com:8989') { return failureFunction() @@ -3292,23 +2608,17 @@ describe('#stub-routing routing driver with stub server', () => { throw new Error('Unexpected address') } - const driver = boltStub.newDriver('neo4j://neo4j.com:8989', { + const driver = await boltStub.newDriver('neo4j://neo4j.com:8989', { resolver: resolverFunction }) const session = driver.session() - session - .run('RETURN 1') - .then(result => done.fail(result)) - .catch(error => { - if (expectedCode) { - expect(error.code).toEqual(expectedCode) - } - if (expectedMessage) { - expect(error.message.indexOf(expectedMessage)).toBeGreaterThan(-1) - } - done() - }) + await expectAsync(session.run('RETURN 1')).toBeRejectedWith( + jasmine.objectContaining(expectedError) + ) + + await session.close() + await driver.close() } class MemorizingRoutingTable extends RoutingTable { diff --git a/test/internal/node/tls.test.js b/test/internal/node/tls.test.js index ea77c75a0..ebeaf299d 100644 --- a/test/internal/node/tls.test.js +++ b/test/internal/node/tls.test.js @@ -31,9 +31,9 @@ describe('#integration trust', () => { beforeAll(async () => { const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) try { - serverVersion = await ServerVersion.fromDriver(driver) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) } finally { - driver.close() + await driver.close() } }) @@ -46,9 +46,9 @@ describe('#integration trust', () => { describe('trust-all-certificates', () => { let driver - afterEach(() => { + afterEach(async () => { if (driver) { - driver.close() + await driver.close() } }) @@ -73,9 +73,9 @@ describe('#integration trust', () => { describe('trust-custom-ca-signed-certificates', () => { let driver - afterEach(() => { + afterEach(async () => { if (driver) { - driver.close() + await driver.close() } }) @@ -116,9 +116,9 @@ describe('#integration trust', () => { describe('trust-system-ca-signed-certificates', () => { let driver - afterEach(() => { + afterEach(async () => { if (driver) { - driver.close() + await driver.close() } }) diff --git a/test/internal/pool.test.js b/test/internal/pool.test.js index 947e69e19..1e08ae4b4 100644 --- a/test/internal/pool.test.js +++ b/test/internal/pool.test.js @@ -22,8 +22,8 @@ import PoolConfig from '../../src/internal/pool-config' import ServerAddress from '../../src/internal/server-address' import { newError, SERVICE_UNAVAILABLE } from '../../src/error' -describe('#unit Pool', () => { - it('allocates if pool is empty', done => { +describe('#unit Pool', async () => { + it('allocates if pool is empty', async () => { // Given let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') @@ -33,23 +33,16 @@ describe('#unit Pool', () => { }) // When - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) + const r0 = await pool.acquire(address) + const r1 = await pool.acquire(address) // Then - Promise.all([p0, p1]).then(values => { - const r0 = values[0] - const r1 = values[1] - - expect(r0.id).toBe(0) - expect(r1.id).toBe(1) - expect(r0).not.toBe(r1) - - done() - }) + expect(r0.id).toBe(0) + expect(r1.id).toBe(1) + expect(r0).not.toBe(r1) }) - it('pools if resources are returned', done => { + it('pools if resources are returned', async () => { // Given a pool that allocates let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') @@ -59,26 +52,18 @@ describe('#unit Pool', () => { }) // When - const p0 = pool.acquire(address).then(r0 => { - r0.close() - return r0 - }) - const p1 = p0.then(r0 => pool.acquire(address)) + const r0 = await pool.acquire(address) + await r0.close() - // Then - Promise.all([p0, p1]).then(values => { - const r0 = values[0] - const r1 = values[1] - - expect(r0.id).toBe(0) - expect(r1.id).toBe(0) - expect(r0).toBe(r1) + const r1 = await pool.acquire(address) - done() - }) + // Then + expect(r0.id).toBe(0) + expect(r1.id).toBe(0) + expect(r0).toBe(r1) }) - it('handles multiple keys', done => { + it('handles multiple keys', async () => { // Given a pool that allocates let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -89,32 +74,24 @@ describe('#unit Pool', () => { }) // When - const p0 = pool.acquire(address1) - const p1 = pool.acquire(address2) - const p01 = Promise.all([p0, p1]).then(values => values[0].close()) - const p2 = p01.then(() => pool.acquire(address1)) - const p3 = p01.then(() => pool.acquire(address2)) + const r0 = await pool.acquire(address1) + const r1 = await pool.acquire(address2) + await r0.close() - // Then - Promise.all([p0, p1, p2, p3]).then(values => { - const r0 = values[0] - const r1 = values[1] - const r2 = values[2] - const r3 = values[3] - - expect(r0.id).toBe(0) - expect(r1.id).toBe(1) - expect(r2.id).toBe(0) - expect(r3.id).toBe(2) + const r2 = await pool.acquire(address1) + const r3 = await pool.acquire(address2) - expect(r0).toBe(r2) - expect(r1).not.toBe(r3) + // Then + expect(r0.id).toBe(0) + expect(r1.id).toBe(1) + expect(r2.id).toBe(0) + expect(r3.id).toBe(2) - done() - }) + expect(r0).toBe(r2) + expect(r1).not.toBe(r3) }) - it('frees if validate returns false', done => { + it('frees if validate returns false', async () => { // Given a pool that allocates let counter = 0 let destroyed = [] @@ -122,34 +99,28 @@ describe('#unit Pool', () => { const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => { - destroyed.push(resource) + destroy: res => { + destroyed.push(res) + return Promise.resolve() }, - validate: resource => false, + validate: res => false, config: new PoolConfig(1000, 60000) }) // When - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) + const r0 = await pool.acquire(address) + const r1 = await pool.acquire(address) // Then - Promise.all([p0, p1]).then(values => { - const r0 = values[0] - const r1 = values[1] - - r0.close() - r1.close() + await r0.close() + await r1.close() - expect(destroyed.length).toBe(2) - expect(destroyed[0].id).toBe(r0.id) - expect(destroyed[1].id).toBe(r1.id) - - done() - }) + expect(destroyed.length).toBe(2) + expect(destroyed[0].id).toBe(r0.id) + expect(destroyed[1].id).toBe(r1.id) }) - it('purges keys', done => { + it('purges keys', async () => { // Given a pool that allocates let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -159,46 +130,37 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) // When - const p0 = pool.acquire(address1) - const p1 = pool.acquire(address2) - const p01 = Promise.all([p0, p1]).then(values => { - values.forEach(v => v.close()) + const r0 = await pool.acquire(address1) + const r1 = await pool.acquire(address2) - expect(pool.has(address1)).toBeTruthy() - expect(pool.has(address2)).toBeTruthy() + await r0.close() + await r1.close() - pool.purge(address1) + expect(pool.has(address1)).toBeTruthy() + expect(pool.has(address2)).toBeTruthy() - expect(pool.has(address1)).toBeFalsy() - expect(pool.has(address2)).toBeTruthy() - }) + await pool.purge(address1) - const p2 = p01.then(() => pool.acquire(address1)) - const p3 = p01.then(() => pool.acquire(address2)) + expect(pool.has(address1)).toBeFalsy() + expect(pool.has(address2)).toBeTruthy() + + const r2 = await pool.acquire(address1) + const r3 = await pool.acquire(address2) // Then - Promise.all([p0, p1, p2, p3]).then(values => { - const r0 = values[0] - const r1 = values[1] - const r2 = values[2] - const r3 = values[3] - - expect(r0.id).toBe(0) - expect(r0.destroyed).toBeTruthy() - expect(r1.id).toBe(1) - expect(r2.id).toBe(2) - expect(r3.id).toBe(1) - - done() - }) + expect(r0.id).toBe(0) + expect(r0.destroyed).toBeTruthy() + expect(r1.id).toBe(1) + expect(r2.id).toBe(2) + expect(r3.id).toBe(1) }) - it('clears out resource counters even after purge', done => { + it('clears out resource counters even after purge', async () => { // Given a pool that allocates let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -208,48 +170,44 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) // When - const p00 = pool.acquire(address1) - const p01 = pool.acquire(address1) - const p10 = pool.acquire(address2) - const p11 = pool.acquire(address2) - const p12 = pool.acquire(address2) + const r00 = await pool.acquire(address1) + const r01 = await pool.acquire(address1) + const r10 = await pool.acquire(address2) + const r11 = await pool.acquire(address2) + const r12 = await pool.acquire(address2) - Promise.all([p00, p01, p10, p11, p12]).then(values => { - expect(pool.activeResourceCount(address1)).toEqual(2) - expect(pool.activeResourceCount(address2)).toEqual(3) + expect(pool.activeResourceCount(address1)).toEqual(2) + expect(pool.activeResourceCount(address2)).toEqual(3) - expect(pool.has(address1)).toBeTruthy() - expect(pool.has(address2)).toBeTruthy() + expect(pool.has(address1)).toBeTruthy() + expect(pool.has(address2)).toBeTruthy() - values[0].close() + await r00.close() - expect(pool.activeResourceCount(address1)).toEqual(1) + expect(pool.activeResourceCount(address1)).toEqual(1) - pool.purge(address1) + await pool.purge(address1) - expect(pool.activeResourceCount(address1)).toEqual(1) + expect(pool.activeResourceCount(address1)).toEqual(1) - values[1].close() + await r01.close() - expect(pool.activeResourceCount(address1)).toEqual(0) - expect(pool.activeResourceCount(address2)).toEqual(3) + expect(pool.activeResourceCount(address1)).toEqual(0) + expect(pool.activeResourceCount(address2)).toEqual(3) - expect(values[0].destroyed).toBeTruthy() - expect(values[1].destroyed).toBeTruthy() + expect(r00.destroyed).toBeTruthy() + expect(r01.destroyed).toBeTruthy() - expect(pool.has(address1)).toBeFalsy() - expect(pool.has(address2)).toBeTruthy() - - done() - }) + expect(pool.has(address1)).toBeFalsy() + expect(pool.has(address2)).toBeTruthy() }) - it('destroys resource when key was purged', done => { + it('destroys resource when key was purged', async () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ @@ -257,28 +215,24 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) - const p0 = pool.acquire(address) - p0.then(r0 => { - expect(pool.has(address)).toBeTruthy() - expect(r0.id).toEqual(0) - - pool.purge(address) - expect(pool.has(address)).toBeFalsy() - expect(r0.destroyed).toBeFalsy() + const r0 = await pool.acquire(address) + expect(pool.has(address)).toBeTruthy() + expect(r0.id).toEqual(0) - r0.close() - expect(pool.has(address)).toBeFalsy() - expect(r0.destroyed).toBeTruthy() + await pool.purge(address) + expect(pool.has(address)).toBeFalsy() + expect(r0.destroyed).toBeFalsy() - done() - }) + await r0.close() + expect(pool.has(address)).toBeFalsy() + expect(r0.destroyed).toBeTruthy() }) - it('purges all keys', done => { + it('close purges all keys', async () => { let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -290,7 +244,7 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) @@ -302,19 +256,59 @@ describe('#unit Pool', () => { pool.acquire(address2), pool.acquire(address3) ] + const values = await Promise.all(acquiredResources) + await Promise.all(values.map(resource => resource.close())) - Promise.all(acquiredResources).then(values => { - values.forEach(resource => resource.close()) - - pool.purgeAll() + await pool.close() - values.forEach(resource => expect(resource.destroyed).toBeTruthy()) + values.forEach(resource => expect(resource.destroyed).toBeTruthy()) + }) - done() + it('should fail to acquire when closed', async () => { + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (server, release) => + Promise.resolve(new Resource(server, 0, release)), + destroy: res => { + return Promise.resolve() + } }) + + // Close the pool + await pool.close() + + await expectAsync(pool.acquire(address)).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Pool is closed/) + }) + ) }) - it('purges keys other than the ones to keep', done => { + it('should fail to acquire when closed with idle connections', async () => { + const address = ServerAddress.fromUrl('bolt://localhost:7687') + + const pool = new Pool({ + create: (server, release) => + Promise.resolve(new Resource(server, 0, release)), + destroy: res => { + return Promise.resolve() + } + }) + + // Acquire and release a resource + const resource = await pool.acquire(address) + await resource.close() + + // Close the pool + await pool.close() + + await expectAsync(pool.acquire(address)).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Pool is closed/) + }) + ) + }) + it('purges keys other than the ones to keep', async () => { let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -326,7 +320,7 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) @@ -338,23 +332,20 @@ describe('#unit Pool', () => { pool.acquire(address2), pool.acquire(address3) ] + const values = await Promise.all(acquiredResources) - Promise.all(acquiredResources).then(values => { - expect(pool.has(address1)).toBeTruthy() - expect(pool.has(address2)).toBeTruthy() - expect(pool.has(address3)).toBeTruthy() + expect(pool.has(address1)).toBeTruthy() + expect(pool.has(address2)).toBeTruthy() + expect(pool.has(address3)).toBeTruthy() - pool.keepAll([address1, address3]) + await pool.keepAll([address1, address3]) - expect(pool.has(address1)).toBeTruthy() - expect(pool.has(address3)).toBeTruthy() - expect(pool.has(address2)).toBeFalsy() - - done() - }) + expect(pool.has(address1)).toBeTruthy() + expect(pool.has(address3)).toBeTruthy() + expect(pool.has(address2)).toBeFalsy() }) - it('purges all keys if addresses to keep is empty', done => { + it('purges all keys if addresses to keep is empty', async () => { let counter = 0 const address1 = ServerAddress.fromUrl('bolt://localhost:7687') @@ -366,7 +357,7 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() } }) @@ -378,23 +369,20 @@ describe('#unit Pool', () => { pool.acquire(address2), pool.acquire(address3) ] + const values = await Promise.all(acquiredResources) - Promise.all(acquiredResources).then(values => { - expect(pool.has(address1)).toBeTruthy() - expect(pool.has(address2)).toBeTruthy() - expect(pool.has(address3)).toBeTruthy() - - pool.keepAll([]) + expect(pool.has(address1)).toBeTruthy() + expect(pool.has(address2)).toBeTruthy() + expect(pool.has(address3)).toBeTruthy() - expect(pool.has(address1)).toBeFalsy() - expect(pool.has(address3)).toBeFalsy() - expect(pool.has(address2)).toBeFalsy() + await pool.keepAll([]) - done() - }) + expect(pool.has(address1)).toBeFalsy() + expect(pool.has(address3)).toBeFalsy() + expect(pool.has(address2)).toBeFalsy() }) - it('skips broken connections during acquire', done => { + it('skips broken connections during acquire', async () => { let validated = false let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') @@ -403,7 +391,7 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true - return true + return Promise.resolve() }, validate: () => { if (validated) { @@ -414,24 +402,14 @@ describe('#unit Pool', () => { } }) - const p0 = pool.acquire(address) - const p1 = p0.then(r0 => { - r0.close() + const r0 = await pool.acquire(address) + await r0.close() + const r1 = await pool.acquire(address) - return pool.acquire(address) - }) - - Promise.all([p0, p1]).then(values => { - const r0 = values[0] - const r1 = values[1] - - expect(r1).not.toBe(r0) - - done() - }) + expect(r1).not.toBe(r0) }) - it('reports presence of the key', done => { + it('reports presence of the key', async () => { const existingAddress = ServerAddress.fromUrl('bolt://localhost:7687') const absentAddress = ServerAddress.fromUrl('bolt://localhost:7688') @@ -440,15 +418,11 @@ describe('#unit Pool', () => { Promise.resolve(new Resource(server, 42, release)) }) - const p0 = pool.acquire(existingAddress) - const p1 = pool.acquire(existingAddress) + const r1 = await pool.acquire(existingAddress) + const r0 = await pool.acquire(existingAddress) - Promise.all([p0, p1]).then(() => { - expect(pool.has(existingAddress)).toBeTruthy() - expect(pool.has(absentAddress)).toBeFalsy() - - done() - }) + expect(pool.has(existingAddress)).toBeTruthy() + expect(pool.has(absentAddress)).toBeFalsy() }) it('reports zero active resources when empty', () => { @@ -468,27 +442,26 @@ describe('#unit Pool', () => { ).toEqual(0) }) - it('reports active resources', done => { + it('reports active resources', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, 42, release)) }) - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) - const p2 = pool.acquire(address) - - Promise.all([p0, p1, p2]).then(values => { - values.forEach(v => expect(v).toBeDefined()) + const acquiredResources = [ + pool.acquire(address), + pool.acquire(address), + pool.acquire(address) + ] + const values = await Promise.all(acquiredResources) - expect(pool.activeResourceCount(address)).toEqual(3) + values.forEach(v => expect(v).toBeDefined()) - done() - }) + expect(pool.activeResourceCount(address)).toEqual(3) }) - it('reports active resources when they are acquired', done => { + it('reports active resources when they are acquired', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => @@ -496,153 +469,119 @@ describe('#unit Pool', () => { }) // three new resources are created and returned to the pool - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) - const p2 = pool.acquire(address) - const p012 = Promise.all([p0, p1, p2]).then(values => { - values.forEach(v => v.close()) - return values - }) + const r0 = await pool.acquire(address) + const r1 = await pool.acquire(address) + const r2 = await pool.acquire(address) + await [r0, r1, r2].map(v => v.close()) // three idle resources are acquired from the pool - const p3 = p012.then(() => pool.acquire(address)) - const p4 = p012.then(() => pool.acquire(address)) - const p5 = p012.then(() => pool.acquire(address)) - - Promise.all([p012, p3, p4, p5]).then(values => { - const r0 = values[0][0] - const r1 = values[0][1] - const r2 = values[0][2] - - expect(values).toContain(r0) - expect(values).toContain(r1) - expect(values).toContain(r2) + const acquiredResources = [ + pool.acquire(address), + pool.acquire(address), + pool.acquire(address) + ] + const resources = await Promise.all(acquiredResources) - expect(pool.activeResourceCount(address)).toEqual(3) + expect(resources).toContain(r0) + expect(resources).toContain(r1) + expect(resources).toContain(r2) - done() - }) + expect(pool.activeResourceCount(address)).toEqual(3) }) - it('does not report resources that are returned to the pool', done => { + it('does not report resources that are returned to the pool', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, 42, release)) }) - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) - const p2 = pool.acquire(address) - const p012 = Promise.all([p0, p1, p2]).then(values => { - const r0 = values[0] - const r1 = values[1] - const r2 = values[2] - - expect(pool.activeResourceCount(address)).toEqual(3) + const r0 = await pool.acquire(address) + const r1 = await pool.acquire(address) + const r2 = await pool.acquire(address) + expect(pool.activeResourceCount(address)).toEqual(3) - r0.close() - expect(pool.activeResourceCount(address)).toEqual(2) + await r0.close() + expect(pool.activeResourceCount(address)).toEqual(2) - r1.close() - expect(pool.activeResourceCount(address)).toEqual(1) + await r1.close() + expect(pool.activeResourceCount(address)).toEqual(1) - r2.close() - expect(pool.activeResourceCount(address)).toEqual(0) + await r2.close() + expect(pool.activeResourceCount(address)).toEqual(0) - return values - }) + const r3 = await pool.acquire(address) + expect(pool.activeResourceCount(address)).toEqual(1) - p012 - .then(() => pool.acquire(address)) - .then(r3 => { - expect(pool.activeResourceCount(address)).toEqual(1) - - r3.close() - expect(pool.activeResourceCount(address)).toEqual(0) - - done() - }) + await r3.close() + expect(pool.activeResourceCount(address)).toEqual(0) }) - it('should wait for a returned connection when max pool size is reached', done => { + it('should wait for a returned connection when max pool size is reached', async () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, - validate: resource => true, + destroy: res => Promise.resolve(), + validate: res => true, config: new PoolConfig(2, 5000) }) - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) + const r0 = await pool.acquire(address) + const r1 = await pool.acquire(address) - Promise.all([p0, p1]).then(values => { - const r1 = values[0] - - pool.acquire(address).then(r2 => { - expect(r2).toBe(r1) - - done() - }) + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r1.close() + }, 1000) - setTimeout(() => { - expectNumberOfAcquisitionRequests(pool, address, 1) - r1.close() - }, 1000) - }) + const r2 = await pool.acquire(address) + expect(r2).toBe(r1) }) - it('should time out when max pool size is reached', done => { + it('should time out when max pool size is reached', async () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, - validate: resource => true, + destroy: res => Promise.resolve(), + validate: res => true, config: new PoolConfig(2, 1000) }) - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) + await pool.acquire(address) + await pool.acquire(address) - Promise.all([p0, p1]).then(() => { - pool.acquire(address).catch(error => { - expect(error.message).toContain('timed out') - expectNumberOfAcquisitionRequests(pool, address, 0) - done() - }) - }) + await expectAsync(pool.acquire(address)).toBeRejectedWith( + jasmine.stringMatching('acquisition timed out') + ) + expectNumberOfAcquisitionRequests(pool, address, 0) }) - it('should not time out if max pool size is not set', done => { + it('should not time out if max pool size is not set', async () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, - validate: resource => true + destroy: res => Promise.resolve(), + validate: res => true }) - const p0 = pool.acquire(address) - const p1 = pool.acquire(address) - Promise.all([p0, p1]).then(values => { - pool.acquire(address).then(r2 => { - expect(r2.id).toEqual(2) - expectNoPendingAcquisitionRequests(pool) - done() - }) - }) + await pool.acquire(address) + await pool.acquire(address) + + const r2 = await pool.acquire(address) + expect(r2.id).toEqual(2) + expectNoPendingAcquisitionRequests(pool) }) - it('should work fine when resources released together with acquisition timeout', done => { + it('should work fine when resources released together with acquisition timeout', async () => { const acquisitionTimeout = 1000 let counter = 0 @@ -650,44 +589,42 @@ describe('#unit Pool', () => { const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, - validate: () => true, + destroy: res => Promise.resolve(), + validate: res => true, config: new PoolConfig(2, acquisitionTimeout) }) - pool.acquire(address).then(resource1 => { - expect(resource1.id).toEqual(0) - - pool.acquire(address).then(resource2 => { - expect(resource2.id).toEqual(1) - - // try to release both resources around the time acquisition fails with timeout - // double-release used to cause deletion of acquire requests in the pool and failure of the timeout - // such background failure made this test fail, not the existing assertions - setTimeout(() => { - resource1.close() - resource2.close() - }, acquisitionTimeout) - - pool - .acquire(address) - .then(someResource => { - expect(someResource).toBeDefined() - expect(someResource).not.toBeNull() - expectNoPendingAcquisitionRequests(pool) - done() // ok, promise got resolved before the timeout - }) - .catch(error => { - expect(error).toBeDefined() - expect(error).not.toBeNull() - expectNoPendingAcquisitionRequests(pool) - done() // also ok, timeout fired before promise got resolved - }) + const resource1 = await pool.acquire(address) + expect(resource1.id).toEqual(0) + + const resource2 = await pool.acquire(address) + expect(resource2.id).toEqual(1) + + // try to release both resources around the time acquisition fails with timeout + // double-release used to cause deletion of acquire requests in the pool and failure of the timeout + // such background failure made this test fail, not the existing assertions + setTimeout(() => { + resource1.close() + resource2.close() + }, acquisitionTimeout) + + // Remember that both code paths are ok with this test, either a success with a valid resource + // or a time out error due to acquisition timeout being kicked in. + await pool + .acquire(address) + .then(someResource => { + expect(someResource).toBeDefined() + expect(someResource).not.toBeNull() + expectNoPendingAcquisitionRequests(pool) + }) + .catch(error => { + expect(error).toBeDefined() + expect(error).not.toBeNull() + expectNoPendingAcquisitionRequests(pool) }) - }) }) - it('should resolve pending acquisition request when single invalid resource returned', done => { + it('should resolve pending acquisition request when single invalid resource returned', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const acquisitionTimeout = 1000 let counter = 0 @@ -695,36 +632,28 @@ describe('#unit Pool', () => { const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, + destroy: res => Promise.resolve(), validate: resourceValidOnlyOnceValidationFunction, config: new PoolConfig(1, acquisitionTimeout) }) - pool.acquire(address).then(resource1 => { - expect(resource1.id).toEqual(0) - expect(pool.activeResourceCount(address)).toEqual(1) - - // release the resource before the acquisition timeout, it should be treated as invalid - setTimeout(() => { - expectNumberOfAcquisitionRequests(pool, address, 1) - resource1.close() - }, acquisitionTimeout / 2) - - pool - .acquire(address) - .then(resource2 => { - expect(resource2.id).toEqual(1) - expectNoPendingAcquisitionRequests(pool) - expect(pool.activeResourceCount(address)).toEqual(1) - done() - }) - .catch(error => { - done.fail(error) - }) - }) + const resource1 = await pool.acquire(address) + expect(resource1.id).toEqual(0) + expect(pool.activeResourceCount(address)).toEqual(1) + + // release the resource before the acquisition timeout, it should be treated as invalid + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + resource1.close() + }, acquisitionTimeout / 2) + + const resource2 = await pool.acquire(address) + expect(resource2.id).toEqual(1) + expectNoPendingAcquisitionRequests(pool) + expect(pool.activeResourceCount(address)).toEqual(1) }) - it('should work fine when invalid resources released and acquisition attempt pending', done => { + it('should work fine when invalid resources released and acquisition attempt pending', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const acquisitionTimeout = 1000 let counter = 0 @@ -732,42 +661,33 @@ describe('#unit Pool', () => { const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: resource => {}, + destroy: res => Promise.resolve(), validate: resourceValidOnlyOnceValidationFunction, config: new PoolConfig(2, acquisitionTimeout) }) - pool.acquire(address).then(resource1 => { - expect(resource1.id).toEqual(0) - expect(pool.activeResourceCount(address)).toEqual(1) - - pool.acquire(address).then(resource2 => { - expect(resource2.id).toEqual(1) - expect(pool.activeResourceCount(address)).toEqual(2) - - // release both resources before the acquisition timeout, they should be treated as invalid - setTimeout(() => { - expectNumberOfAcquisitionRequests(pool, address, 1) - resource1.close() - resource2.close() - }, acquisitionTimeout / 2) - - pool - .acquire(address) - .then(resource3 => { - expect(resource3.id).toEqual(2) - expectNoPendingAcquisitionRequests(pool) - expect(pool.activeResourceCount(address)).toEqual(1) - done() - }) - .catch(error => { - done.fail(error) - }) - }) - }) + const resource1 = await pool.acquire(address) + expect(resource1.id).toEqual(0) + expect(pool.activeResourceCount(address)).toEqual(1) + + const resource2 = await pool.acquire(address) + expect(resource2.id).toEqual(1) + expect(pool.activeResourceCount(address)).toEqual(2) + + // release both resources before the acquisition timeout, they should be treated as invalid + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + resource1.close() + resource2.close() + }, acquisitionTimeout / 2) + + const resource3 = await pool.acquire(address) + expect(resource3.id).toEqual(2) + expectNoPendingAcquisitionRequests(pool) + expect(pool.activeResourceCount(address)).toEqual(1) }) - it('should set-up idle observer on acquire and release', done => { + it('should set-up idle observer on acquire and release', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') let resourceCount = 0 let installIdleObserverCount = 0 @@ -776,8 +696,8 @@ describe('#unit Pool', () => { const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), - destroy: resource => {}, - validate: resource => true, + destroy: res => Promise.resolve(), + validate: res => true, installIdleObserver: (resource, observer) => { installIdleObserverCount++ }, @@ -786,39 +706,31 @@ describe('#unit Pool', () => { } }) - const p01 = pool.acquire(address) - const p02 = pool.acquire(address) - const p03 = pool.acquire(address) - Promise.all([p01, p02, p03]).then(resources => { - resources[0].close() - resources[1].close() - resources[2].close() - - expect(installIdleObserverCount).toEqual(3) - expect(removeIdleObserverCount).toEqual(0) + const r1 = await pool.acquire(address) + const r2 = await pool.acquire(address) + const r3 = await pool.acquire(address) + await [r1, r2, r3].map(r => r.close()) - const p11 = pool.acquire(address) - const p12 = pool.acquire(address) - const p13 = pool.acquire(address) + expect(installIdleObserverCount).toEqual(3) + expect(removeIdleObserverCount).toEqual(0) - Promise.all([p11, p12, p13]).then(resources => { - expect(installIdleObserverCount).toEqual(3) - expect(removeIdleObserverCount).toEqual(3) + await pool.acquire(address) + await pool.acquire(address) + await pool.acquire(address) - done() - }) - }) + expect(installIdleObserverCount).toEqual(3) + expect(removeIdleObserverCount).toEqual(3) }) - it('should clean-up resource when connection fails while idle', done => { + it('should clean-up resource when connection fails while idle', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') let resourceCount = 0 const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), - destroy: resource => {}, - validate: resource => true, + destroy: res => Promise.resolve(), + validate: res => true, installIdleObserver: (resource, observer) => { resource['observer'] = observer }, @@ -827,42 +739,38 @@ describe('#unit Pool', () => { } }) - pool.acquire(address).then(resource1 => { - pool.acquire(address).then(resource2 => { - expect(pool.activeResourceCount(address)).toBe(2) - - resource1.close() - expect(pool.activeResourceCount(address)).toBe(1) + const resource1 = await pool.acquire(address) + const resource2 = await pool.acquire(address) + expect(pool.activeResourceCount(address)).toBe(2) - resource2.close() - expect(pool.activeResourceCount(address)).toBe(0) + await resource1.close() + expect(pool.activeResourceCount(address)).toBe(1) - expect(pool.has(address)).toBeTruthy() + await resource2.close() + expect(pool.activeResourceCount(address)).toBe(0) - resource1['observer'].onError( - newError('connection reset', SERVICE_UNAVAILABLE) - ) - resource2['observer'].onError( - newError('connection reset', SERVICE_UNAVAILABLE) - ) + expect(pool.has(address)).toBeTruthy() - expect(pool.activeResourceCount(address)).toBe(0) - expectNoIdleResources(pool, address) + resource1['observer'].onError( + newError('connection reset', SERVICE_UNAVAILABLE) + ) + resource2['observer'].onError( + newError('connection reset', SERVICE_UNAVAILABLE) + ) - done() - }) - }) + expect(pool.activeResourceCount(address)).toBe(0) + expectNoIdleResources(pool, address) }) - it('should clean-up idle observer on purge', done => { + it('should clean-up idle observer on purge', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') let resourceCount = 0 const pool = new Pool({ create: (server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), - destroy: resource => {}, - validate: resource => true, + destroy: res => Promise.resolve(), + validate: res => true, installIdleObserver: (resource, observer) => { resource['observer'] = observer }, @@ -871,19 +779,15 @@ describe('#unit Pool', () => { } }) - pool.acquire(address).then(resource1 => { - pool.acquire(address).then(resource2 => { - resource1.close() - resource2.close() + const resource1 = await pool.acquire(address) + const resource2 = await pool.acquire(address) + await resource1.close() + await resource2.close() - pool.purge(address) + await pool.purge(address) - expect(resource1['observer']).toBeFalsy() - expect(resource2['observer']).toBeFalsy() - - done() - }) - }) + expect(resource1['observer']).toBeFalsy() + expect(resource2['observer']).toBeFalsy() }) }) diff --git a/test/internal/routing-util.test.js b/test/internal/routing-util.test.js index ad0c0b0de..254194e53 100644 --- a/test/internal/routing-util.test.js +++ b/test/internal/routing-util.test.js @@ -441,6 +441,7 @@ describe('#unit RoutingUtil', () => { close () { this._closed = true + return Promise.resolve() } isClosed () { diff --git a/test/internal/server-version.test.js b/test/internal/server-version.test.js index 7130d1537..a9b35de2b 100644 --- a/test/internal/server-version.test.js +++ b/test/internal/server-version.test.js @@ -152,37 +152,22 @@ describe('#unit ServerVersion', () => { }) describe('#integration ServerVersion', () => { - it('should fetch version using driver', done => { + it('should fetch version using driver', async () => { const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) - ServerVersion.fromDriver(driver) - .then(version => { - driver.close() - expect(version).not.toBeNull() - expect(version).toBeDefined() - expect(version instanceof ServerVersion).toBeTruthy() - done() - }) - .catch(error => { - driver.close() - done.fail(error) - }) + const version = await ServerVersion.fromDriver(driver) + await driver.close() + + expect(version).not.toBeNull() + expect(version).toBeDefined() + expect(version instanceof ServerVersion).toBeTruthy() }) - it('should fail to fetch version using incorrect driver', done => { + it('should fail to fetch version using incorrect driver', async () => { const driver = neo4j.driver('bolt://localhost:4242', sharedNeo4j.authToken) // use wrong port - ServerVersion.fromDriver(driver) - .then(version => { - driver.close() - done.fail( - 'Should not be able to fetch version: ' + JSON.stringify(version) - ) - }) - .catch(error => { - expect(error).not.toBeNull() - expect(error).toBeDefined() - driver.close() - done() - }) + + await expectAsync(ServerVersion.fromDriver(driver)).toBeRejected() + + await driver.close() }) }) diff --git a/test/internal/shared-neo4j.js b/test/internal/shared-neo4j.js index 1b46dd113..49646de4c 100644 --- a/test/internal/shared-neo4j.js +++ b/test/internal/shared-neo4j.js @@ -18,6 +18,7 @@ */ import neo4j from '../../src' +import { ServerVersion } from '../../src/internal/server-version' class UnsupportedPlatform { pathJoin () { @@ -255,6 +256,16 @@ function stopNeo4j (neo4jDir) { } } +async function cleanupAndGetVersion (driver) { + const session = driver.session({ defaultAccessMode: neo4j.session.WRITE }) + try { + const result = await session.run('MATCH (n) DETACH DELETE n') + return ServerVersion.fromString(result.summary.server.version) + } finally { + await session.close() + } +} + function findExistingNeo4jDirStrict (dir) { const neo4jDir = findExistingNeo4jDir(dir, null) if (!neo4jDir) { @@ -327,6 +338,11 @@ class RunCommandResult { } } +const debugLogging = { + level: 'debug', + logger: (level, message) => console.warn(`${level}: ${message}`) +} + export default { start: start, stop: stop, @@ -335,5 +351,7 @@ export default { neo4jKeyPath: neo4jKeyPath, username: username, password: password, - authToken: authToken + authToken: authToken, + logging: debugLogging, + cleanupAndGetVersion: cleanupAndGetVersion } diff --git a/test/result.test.js b/test/result.test.js index a04dcf4a1..503d86cde 100644 --- a/test/result.test.js +++ b/test/result.test.js @@ -24,15 +24,15 @@ import utils from './internal/test-utils' describe('#integration result stream', () => { let driver, session - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() - session.run('MATCH (n) DETACH DELETE n').then(done) + await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { - driver.close() + afterEach(async () => { + await driver.close() }) it('should allow chaining `then`, returning a new thing in each', done => { diff --git a/test/rx/navigation.test.js b/test/rx/navigation.test.js index 80898df53..824960cad 100644 --- a/test/rx/navigation.test.js +++ b/test/rx/navigation.test.js @@ -39,13 +39,7 @@ describe('#integration-rx navigation', () => { originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 - const normalSession = driver.session() - try { - const result = await normalSession.run('MATCH (n) DETACH DELETE n') - serverVersion = ServerVersion.fromString(result.summary.server.version) - } finally { - await normalSession.close() - } + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) afterEach(async () => { @@ -53,7 +47,7 @@ describe('#integration-rx navigation', () => { if (session) { await session.close().toPromise() } - driver.close() + await driver.close() }) it('should return keys', () => shouldReturnKeys(serverVersion, session)) @@ -151,7 +145,7 @@ describe('#integration-rx navigation', () => { if (session) { await session.close().toPromise() } - driver.close() + await driver.close() }) it('should return keys', () => shouldReturnKeys(serverVersion, txc)) diff --git a/test/rx/session.test.js b/test/rx/session.test.js index 10a2606a3..be57ac5eb 100644 --- a/test/rx/session.test.js +++ b/test/rx/session.test.js @@ -33,27 +33,20 @@ describe('#integration rx-session', () => { let serverVersion beforeEach(async () => { + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000 driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.rxSession() - const normalSession = driver.session() - try { - const result = await normalSession.run('MATCH (n) DETACH DELETE n') - serverVersion = ServerVersion.fromString(result.summary.server.version) - } finally { - await normalSession.close() - } - - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000 + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) afterEach(async () => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout if (session) { await session.close().toPromise() } - driver.close() - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout + await driver.close() }) it('should be able to run a simple statement', async () => { diff --git a/test/rx/summary.test.js b/test/rx/summary.test.js index ad1c69a33..a4852e740 100644 --- a/test/rx/summary.test.js +++ b/test/rx/summary.test.js @@ -34,14 +34,7 @@ describe('#integration-rx summary', () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.rxSession() - const normalSession = driver.session() - try { - const result = await normalSession.run('MATCH (n) DETACH DELETE n') - serverVersion = ServerVersion.fromString(result.summary.server.version) - } finally { - await normalSession.close() - } - + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) await dropConstraintsAndIndices(driver) }) @@ -49,7 +42,7 @@ describe('#integration-rx summary', () => { if (session) { await session.close().toPromise() } - driver.close() + await driver.close() }) it('should return non-null summary', () => @@ -148,7 +141,7 @@ describe('#integration-rx summary', () => { if (session) { await session.close().toPromise() } - driver.close() + await driver.close() }) it('should return non-null summary', () => diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js index f8e6bb4ff..db16e3339 100644 --- a/test/rx/transaction.test.js +++ b/test/rx/transaction.test.js @@ -44,20 +44,14 @@ describe('#integration-rx transaction', () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.rxSession() - const normalSession = driver.session() - try { - const result = await normalSession.run('MATCH (n) DETACH DELETE n') - serverVersion = ServerVersion.fromString(result.summary.server.version) - } finally { - await normalSession.close() - } + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) afterEach(async () => { if (session) { await session.close().toPromise() } - driver.close() + await driver.close() }) it('should commit an empty transaction', async () => { diff --git a/test/session.test.js b/test/session.test.js index b205db634..6877b1bb0 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -38,21 +38,18 @@ describe('#integration session', () => { let serverVersion let originalTimeout - beforeEach(done => { + beforeEach(async () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000 - session.run('MATCH (n) DETACH DELETE n').then(result => { - serverVersion = ServerVersion.fromString(result.summary.server.version) - done() - }) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { + afterEach(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - driver.close() + await driver.close() }) it('close should return promise', done => { @@ -116,10 +113,10 @@ describe('#integration session', () => { const tx = session.beginTransaction() tx.run('INVALID QUERY').catch(() => { tx.rollback().then(() => { - session.close().then(() => { - driver.close() - done() - }) + session + .close() + .then(() => driver.close()) + .then(() => done()) }) }) }) @@ -364,8 +361,7 @@ describe('#integration session', () => { expect(node.properties.prop).toEqual('prop') }, onCompleted: () => { - session.close() - done() + session.close().then(() => done()) }, onError: error => { console.log(error) @@ -447,20 +443,20 @@ describe('#integration session', () => { const readSession = driver.session({ defaultAccessMode: neo4j.session.READ }) - readSession.run('RETURN 1').then(() => { - readSession.close() - done() - }) + readSession + .run('RETURN 1') + .then(() => readSession.close()) + .then(() => done()) }) it('should allow creation of a ' + neo4j.session.WRITE + ' session', done => { const writeSession = driver.session({ defaultAccessMode: neo4j.session.WRITE }) - writeSession.run('CREATE ()').then(() => { - writeSession.close() - done() - }) + writeSession + .run('CREATE ()') + .then(() => writeSession.close()) + .then(() => done()) }) it('should fail for illegal session mode', () => { @@ -912,11 +908,11 @@ describe('#integration session', () => { session .run('MATCH (:Knight)-[:DEFENDS]->() RETURN count(*)') .then(result => { - session.close() const count = result.records[0].get(0).toInt() expect(count).toEqual(1) - done() }) + .then(() => session.close()) + .then(() => done()) }, onError: error => { console.log(error) @@ -942,7 +938,7 @@ describe('#integration session', () => { const count = result.records[0].get(0).toInt() expect(count).toEqual(nodeCount) } finally { - session.close() + await session.close() } }) @@ -1014,11 +1010,7 @@ describe('#integration session', () => { expect(numberOfAcquiredConnectionsFromPool()).toEqual(2) }, onCompleted: () => { - otherSession.close().then(() => { - session.close().then(() => { - done() - }) - }) + otherSession.close().then(() => session.close().then(() => done())) }, onError: error => { console.log(error) @@ -1160,9 +1152,9 @@ describe('#integration session', () => { expect(result.records[0].get(0)).toEqual('424242') expect(usedTransactions.length).toEqual(3) usedTransactions.forEach(tx => expect(tx.isOpen()).toBeFalsy()) - session.close() - done() }) + .then(() => session.close()) + .then(() => done()) .catch(error => { done.fail(error) }) @@ -1179,18 +1171,21 @@ describe('#integration session', () => { }) resultPromise - .then(result => { - session.close() - done.fail('Retries should not succeed: ' + JSON.stringify(result)) - }) + .then(result => + session + .close() + .then(() => + done.fail('Retries should not succeed: ' + JSON.stringify(result)) + ) + ) .catch(error => { - session.close() expect(error).toBeDefined() expect(error).not.toBeNull() expect(usedTransactions.length).toEqual(1) expect(usedTransactions[0].isOpen()).toBeFalsy() - done() }) + .then(() => session.close()) + .then(() => done()) } function countNodes (label, propertyKey, propertyValue) { @@ -1208,9 +1203,10 @@ describe('#integration session', () => { function withQueryInTmpSession (driver, callback) { const tmpSession = driver.session() - return tmpSession.run('RETURN 1').then(() => { - tmpSession.close().then(() => callback()) - }) + return tmpSession + .run('RETURN 1') + .then(() => tmpSession.close()) + .then(() => callback()) } function newSessionWithConnection (connection) { @@ -1248,9 +1244,9 @@ describe('#integration session', () => { .run('MATCH (n) RETURN n.id') .then(result => { const ids = result.records.map(record => record.get(0).toNumber()) - session.close() resolve(ids) }) + .then(() => session.close()) .catch(error => { reject(error) }) @@ -1296,12 +1292,9 @@ describe('#integration session', () => { const session = localDriver.session() session .run('RETURN 1') - .then(() => { - localDriver.close() - done.fail('Query did not fail') - }) + .then(() => localDriver.close()) + .then(() => done.fail('Query did not fail')) .catch(error => { - localDriver.close() expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) // in some environments non-routable address results in immediate 'connection refused' error and connect @@ -1311,9 +1304,9 @@ describe('#integration session', () => { 'Failed to establish connection in 1000ms' ) } - - done() }) + .then(() => localDriver.close()) + .then(() => done()) } function testUnsupportedQueryParameter (value, done) { diff --git a/test/spatial-types.test.js b/test/spatial-types.test.js index 76899a661..3168ec0c7 100644 --- a/test/spatial-types.test.js +++ b/test/spatial-types.test.js @@ -35,46 +35,35 @@ describe('#integration spatial-types', () => { let session let serverVersion - beforeAll(done => { + beforeAll(() => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) driverWithNativeNumbers = neo4j.driver( 'bolt://localhost', sharedNeo4j.authToken, { disableLosslessIntegers: true } ) - ServerVersion.fromDriver(driver).then(version => { - serverVersion = version - done() - }) }) - afterAll(() => { + afterAll(async () => { if (driver) { - driver.close() + await driver.close() driver = null } if (driverWithNativeNumbers) { - driverWithNativeNumbers.close() + await driverWithNativeNumbers.close() driverWithNativeNumbers = null } }) - beforeEach(done => { + beforeEach(async () => { session = driver.session() - session - .run('MATCH (n) DETACH DELETE n') - .then(() => { - done() - }) - .catch(error => { - done.fail(error) - }) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { + afterEach(async () => { if (session) { - session.close() + await session.close() session = null } }) @@ -287,16 +276,17 @@ describe('#integration spatial-types', () => { return } - session.run(query).then(result => { - const records = result.records - expect(records.length).toEqual(1) - - const point = records[0].get(0) - pointChecker(point) + session + .run(query) + .then(result => { + const records = result.records + expect(records.length).toEqual(1) - session.close() - done() - }) + const point = records[0].get(0) + pointChecker(point) + }) + .then(() => session.close()) + .then(() => done()) } function testSendingAndReceivingOfPoints (done, originalValue) { @@ -314,10 +304,9 @@ describe('#integration spatial-types', () => { const receivedPoint = records[0].get(0) expect(receivedPoint).toEqual(originalValue) - - session.close() - done() }) + .then(() => session.close()) + .then(() => done()) } function neo4jDoesNotSupportPoints (done) { diff --git a/test/stress.test.js b/test/stress.test.js index 7268dba3b..8547217ba 100644 --- a/test/stress.test.js +++ b/test/stress.test.js @@ -52,7 +52,7 @@ describe('#integration stress tests', () => { let originalTimeout let driver - beforeEach(done => { + beforeEach(async () => { originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = TEST_MODE.maxRunTimeMs @@ -61,16 +61,13 @@ describe('#integration stress tests', () => { } driver = neo4j.driver(DATABASE_URI, sharedNeo4j.authToken, config) - cleanupDb(driver).then(() => done()) + await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(done => { + afterEach(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - cleanupDb(driver).then(() => { - driver.close() - done() - }) + await driver.close() }) it('basic', done => { @@ -458,15 +455,16 @@ describe('#integration stress tests', () => { function fetchClusterAddresses (context) { const session = context.driver.session() - return session.run('CALL dbms.cluster.overview()').then(result => { - session.close() - const records = result.records + return session.run('CALL dbms.cluster.overview()').then(result => + session.close().then(() => { + const records = result.records - const followers = addressesWithRole(records, 'FOLLOWER') - const readReplicas = addressesWithRole(records, 'READ_REPLICA') + const followers = addressesWithRole(records, 'FOLLOWER') + const readReplicas = addressesWithRole(records, 'READ_REPLICA') - return new ClusterAddresses(followers, readReplicas) - }) + return new ClusterAddresses(followers, readReplicas) + }) + ) } function addressesWithRole (records, role) { @@ -556,9 +554,7 @@ describe('#integration stress tests', () => { const session = driver.session() return session .run('MATCH (n) DETACH DELETE n') - .then(() => { - session.close() - }) + .then(() => session.close()) .catch(error => { console.log('Error clearing the database: ', error) }) diff --git a/test/summary.test.js b/test/summary.test.js index e0427b419..79c218fcd 100644 --- a/test/summary.test.js +++ b/test/summary.test.js @@ -28,12 +28,11 @@ describe('#integration result summary', () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() - const result = await session.run('MATCH (n) DETACH DELETE n') - serverVersion = ServerVersion.fromString(result.summary.server.version) + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) - afterEach(() => { - driver.close() + afterEach(async () => { + await driver.close() }) it('should get result summary', done => { diff --git a/test/temporal-types.test.js b/test/temporal-types.test.js index 0eddcb883..aaa66696f 100644 --- a/test/temporal-types.test.js +++ b/test/temporal-types.test.js @@ -55,7 +55,7 @@ describe('#integration temporal-types', () => { let session let serverVersion - beforeAll(done => { + beforeAll(() => { originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 @@ -65,35 +65,25 @@ describe('#integration temporal-types', () => { sharedNeo4j.authToken, { disableLosslessIntegers: true } ) - - ServerVersion.fromDriver(driver).then(version => { - serverVersion = version - done() - }) }) - afterAll(() => { + afterAll(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout if (driver) { - driver.close() + await driver.close() driver = null } if (driverWithNativeNumbers) { - driverWithNativeNumbers.close() + await driverWithNativeNumbers.close() driverWithNativeNumbers = null } }) beforeEach(async () => { session = driver.session() - - try { - await session.run('MATCH (n) DETACH DELETE n') - } finally { - await session.close() - } + serverVersion = await sharedNeo4j.cleanupAndGetVersion(driver) }) afterEach(async () => { diff --git a/test/transaction.test.js b/test/transaction.test.js index 4505ae793..3dd72b3f5 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -28,7 +28,7 @@ describe('#integration transaction', () => { let serverVersion let originalTimeout - beforeEach(done => { + beforeEach(async () => { // make jasmine timeout high enough to test unreachable bookmarks originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL jasmine.DEFAULT_TIMEOUT_INTERVAL = 40000 @@ -36,15 +36,14 @@ describe('#integration transaction', () => { driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) session = driver.session() - session.run('MATCH (n) DETACH DELETE n').then(result => { - serverVersion = ServerVersion.fromString(result.summary.server.version) - done() - }) + const result = await session.run('MATCH (n) DETACH DELETE n') + serverVersion = ServerVersion.fromString(result.summary.server.version) }) - afterEach(() => { + afterEach(async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout - driver.close() + await session.close() + await driver.close() }) it('should commit simple case', done => { @@ -120,7 +119,6 @@ describe('#integration transaction', () => { const tx = session.beginTransaction() tx.run('THIS IS NOT CYPHER').catch(error => { expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError') - driver.close() done() }) }) @@ -130,7 +128,6 @@ describe('#integration transaction', () => { tx.run('THIS IS NOT CYPHER').subscribe({ onError: error => { expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError') - driver.close() done() } }) @@ -147,7 +144,6 @@ describe('#integration transaction', () => { tx.run('CREATE (:TXNode2)').catch(() => { tx.commit().catch(commitError => { expect(commitError.error).toBeDefined() - driver.close() done() }) }) @@ -163,7 +159,6 @@ describe('#integration transaction', () => { tx.run('THIS IS NOT CYPHER').catch(() => { tx.commit().catch(error => { expect(error.error).toBeDefined() - driver.close() done() }) }) @@ -171,17 +166,21 @@ describe('#integration transaction', () => { .catch(console.log) }) - it('should handle when committing when another statement fails', done => { + it('should handle when committing when another statement fails', async () => { // When const tx = session.beginTransaction() - tx.run('CREATE (:TXNode1)').then(() => { - tx.commit().catch(error => { - expect(error).toBeDefined() - driver.close() - done() + + await expectAsync(tx.run('CREATE (:TXNode1)')).toBeResolved() + await expectAsync(tx.run('THIS IS NOT CYHER')).toBeRejectedWith( + jasmine.objectContaining({ + message: jasmine.stringMatching(/Invalid input/) }) - }) - tx.run('THIS IS NOT CYPHER') + ) + await expectAsync(tx.commit()).toBeRejectedWith( + jasmine.objectContaining({ + error: jasmine.stringMatching(/Cannot commit statements/) + }) + ) }) it('should handle rollbacks', done => { @@ -219,7 +218,6 @@ describe('#integration transaction', () => { .then(() => { tx.commit().catch(error => { expect(error.error).toBeDefined() - driver.close() done() }) }) @@ -236,7 +234,6 @@ describe('#integration transaction', () => { .then(() => { tx.run('RETURN 42').catch(error => { expect(error.error).toBeDefined() - driver.close() done() }) }) @@ -245,16 +242,19 @@ describe('#integration transaction', () => { .catch(console.log) }) - it('should fail when running when a previous statement failed', done => { + it('should fail running when a previous statement failed', async () => { const tx = session.beginTransaction() - tx.run('THIS IS NOT CYPHER').catch(() => { - tx.run('RETURN 42').catch(error => { - expect(error.error).toBeDefined() - driver.close() - done() + + await expectAsync(tx.run('THIS IS NOT CYPHER')).toBeRejectedWith( + jasmine.stringMatching(/Invalid input/) + ) + + await expectAsync(tx.run('RETURN 42')).toBeRejectedWith( + jasmine.objectContaining({ + error: jasmine.stringMatching(/Cannot run statement/) }) - }) - tx.rollback() + ) + await tx.rollback() }) it('should fail when trying to roll back a rolled back transaction', done => { @@ -265,7 +265,6 @@ describe('#integration transaction', () => { .then(() => { tx.rollback().catch(error => { expect(error.error).toBeDefined() - driver.close() done() }) }) @@ -478,7 +477,7 @@ describe('#integration transaction', () => { }) }) - it('should fail nicely for illegal statement', () => { + it('should fail nicely for illegal statement', async () => { const tx = session.beginTransaction() expect(() => tx.run()).toThrowError(TypeError) @@ -603,26 +602,29 @@ describe('#integration transaction', () => { const session = localDriver.session() const tx = session.beginTransaction() tx.run('RETURN 1') - .then(() => { - tx.rollback() - session.close() - done.fail('Query did not fail') - }) - .catch(error => { - tx.rollback() - session.close() + .then(() => + tx + .rollback() + .then(() => session.close()) + .then(() => done.fail('Query did not fail')) + ) + .catch(error => + tx + .rollback() + .then(() => session.close()) + .then(() => { + expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) - expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE) + // in some environments non-routable address results in immediate 'connection refused' error and connect + // timeout is not fired; skip message assertion for such cases, it is important for connect attempt to not hang + if (error.message.indexOf('Failed to establish connection') === 0) { + expect(error.message).toEqual( + 'Failed to establish connection in 1000ms' + ) + } - // in some environments non-routable address results in immediate 'connection refused' error and connect - // timeout is not fired; skip message assertion for such cases, it is important for connect attempt to not hang - if (error.message.indexOf('Failed to establish connection') === 0) { - expect(error.message).toEqual( - 'Failed to establish connection in 1000ms' - ) - } - - done() - }) + done() + }) + ) } }) diff --git a/test/types.test.js b/test/types.test.js index dcefd9628..efd20569d 100644 --- a/test/types.test.js +++ b/test/types.test.js @@ -115,9 +115,9 @@ describe('#integration node values', () => { expect(node.properties).toEqual({ name: 'Lisa' }) expect(node.labels).toEqual(['User']) expect(node.identity).toEqual(result.records[0].get('id(n)')) - driver.close() - done() }) + .then(() => driver.close()) + .then(() => done()) }) }) @@ -136,9 +136,9 @@ describe('#integration relationship values', () => { expect(rel.properties).toEqual({ name: 'Lisa' }) expect(rel.type).toEqual('User') expect(rel.identity).toEqual(result.records[0].get('id(r)')) - driver.close() - done() }) + .then(() => driver.close()) + .then(() => done()) }) }) @@ -169,9 +169,9 @@ describe('#integration path values', () => { // Which is the inverse of the relationship itself! expect(segment.relationship.properties).toEqual({ since: 1234 }) } - driver.close() - done() }) + .then(() => driver.close()) + .then(() => done()) .catch(err => { console.log(err) }) @@ -180,17 +180,9 @@ describe('#integration path values', () => { describe('#integration byte arrays', () => { let originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL - let serverSupportsByteArrays = false - beforeAll(done => { + beforeAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 - - const tmpDriver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) - ServerVersion.fromDriver(tmpDriver).then(version => { - tmpDriver.close() - serverSupportsByteArrays = version.compareTo(VERSION_3_2_0) >= 0 - done() - }) }) afterAll(() => { @@ -198,67 +190,37 @@ describe('#integration byte arrays', () => { }) it('should support returning empty byte array if server supports byte arrays', done => { - if (!serverSupportsByteArrays) { - done() - return - } - testValue(new Int8Array(0))(done) }) it('should support returning empty byte array if server supports byte arrays', done => { - if (!serverSupportsByteArrays) { - done() - return - } - testValues([new Int8Array(0)])(done) }) it('should support returning short byte arrays if server supports byte arrays', done => { - if (!serverSupportsByteArrays) { - done() - return - } - testValues(randomByteArrays(100, 1, 255))(done) }) it('should support returning medium byte arrays if server supports byte arrays', done => { - if (!serverSupportsByteArrays) { - done() - return - } - testValues(randomByteArrays(50, 256, 65535))(done) }) it('should support returning long byte arrays if server supports byte arrays', done => { - if (!serverSupportsByteArrays) { - done() - return - } - testValues(randomByteArrays(10, 65536, 2 * 65536))(done) }) it('should fail to return byte array if server does not support byte arrays', done => { - if (serverSupportsByteArrays) { - done() - return - } - const driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) const session = driver.session() session .run('RETURN {array}', { array: randomByteArray(42) }) .catch(error => { - driver.close() expect(error.message).toEqual( 'Byte arrays are not supported by the database this driver is connected to' ) - done() }) + .then(() => driver.close()) + .then(() => done()) }) }) @@ -268,14 +230,9 @@ function testValue (actual, expected) { const queryPromise = runReturnQuery(driver, actual, expected) queryPromise - .then(() => { - driver.close() - done() - }) - .catch(error => { - driver.close() - console.log(error) - }) + .then(() => driver.close()) + .then(() => done()) + .catch(error => done.fail(error)) } } @@ -288,14 +245,9 @@ function testValues (values) { ) queriesPromise - .then(() => { - driver.close() - done() - }) - .catch(error => { - driver.close() - console.log(error) - }) + .then(() => driver.close()) + .then(() => done()) + .catch(error => done.fail(error)) } } @@ -306,7 +258,9 @@ function runReturnQuery (driver, actual, expected) { .run('RETURN {val} as v', { val: actual }) .then(result => { expect(result.records[0].get('v')).toEqual(expected || actual) - session.close() + }) + .then(() => session.close()) + .then(() => { resolve() }) .catch(error => { diff --git a/test/types/driver.test.ts b/test/types/driver.test.ts index aa577282d..bd168cbf9 100644 --- a/test/types/driver.test.ts +++ b/test/types/driver.test.ts @@ -100,7 +100,7 @@ session1 }) .then(() => session1.close()) -const close: void = driver.close() +const close: Promise = driver.close() driver.verifyConnectivity().then((serverInfo: ServerInfo) => { console.log(serverInfo.version) diff --git a/types/driver.d.ts b/types/driver.d.ts index 2701fb16d..66221d38d 100644 --- a/types/driver.d.ts +++ b/types/driver.d.ts @@ -83,7 +83,7 @@ declare interface Driver { database?: string }): RxSession - close(): void + close(): Promise verifyConnectivity(): Promise } From 2589eb6daf9a982343445a4828fc888ca09a7fcf Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Thu, 22 Aug 2019 18:41:39 +0100 Subject: [PATCH 14/14] Pass specified bookmark down to the discovery invocation --- src/internal/bookmark.js | 72 +- src/internal/connection-holder.js | 12 +- src/internal/connection-provider-direct.js | 2 +- src/internal/connection-provider-routing.js | 380 ++++---- src/internal/connection-provider-single.js | 2 +- src/internal/connection-provider.js | 2 +- src/internal/rediscovery.js | 78 +- src/internal/routing-util.js | 46 +- src/session.js | 4 +- test/internal/bookmark.test.js | 49 +- .../connection-provider-direct.test.js | 19 +- .../connection-provider-routing.test.js | 853 ++++++++++-------- test/internal/fake-connection.js | 5 +- .../node/direct.driver.boltkit.test.js | 8 +- .../node/routing.driver.boltkit.test.js | 81 +- test/internal/routing-util.test.js | 107 +++ .../boltstub/v2/read_tx_with_bookmarks.script | 2 +- .../v2/write_read_tx_with_bookmarks.script | 4 +- .../v2/write_tx_with_bookmarks.script | 2 +- .../boltstub/v3/read_with_bookmark.script | 12 + ...e_endpoints_aDatabase_with_bookmark.script | 10 + .../read_from_aDatabase_with_bookmark.script | 12 + test/rx/transaction.test.js | 2 +- test/session.test.js | 6 +- test/transaction.test.js | 6 +- 25 files changed, 1065 insertions(+), 711 deletions(-) create mode 100644 test/resources/boltstub/v3/read_with_bookmark.script create mode 100644 test/resources/boltstub/v4/acquire_endpoints_aDatabase_with_bookmark.script create mode 100644 test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script diff --git a/src/internal/bookmark.js b/src/internal/bookmark.js index 5d6bdc672..59a7f8c26 100644 --- a/src/internal/bookmark.js +++ b/src/internal/bookmark.js @@ -19,11 +19,7 @@ import * as util from './util' -const BOOKMARK_KEY = 'bookmark' const BOOKMARKS_KEY = 'bookmarks' -const BOOKMARK_PREFIX = 'neo4j:bookmark:v1:tx' - -const UNKNOWN_BOOKMARK_VALUE = -1 export default class Bookmark { /** @@ -32,7 +28,6 @@ export default class Bookmark { */ constructor (values) { this._values = asStringArray(values) - this._maxValue = maxBookmark(this._values) } static empty () { @@ -44,15 +39,7 @@ export default class Bookmark { * @return {boolean} returns `true` bookmark has a value, `false` otherwise. */ isEmpty () { - return this._maxValue === null - } - - /** - * Get maximum value of this bookmark as string. - * @return {string|null} the maximum value or `null` if it is not defined. - */ - maxBookmarkAsString () { - return this._maxValue + return this._values.length === 0 } /** @@ -77,7 +64,6 @@ export default class Bookmark { // bookmark that is why driver has to parse and compare given list of bookmarks. This functionality will // eventually be removed. return { - [BOOKMARK_KEY]: this._maxValue, [BOOKMARKS_KEY]: this._values } } @@ -87,7 +73,7 @@ const EMPTY_BOOKMARK = new Bookmark(null) /** * Converts given value to an array. - * @param {string|string[]} [value=undefined] argument to convert. + * @param {string|string[]|Array} [value=undefined] argument to convert. * @return {string[]} value converted to an array. */ function asStringArray (value) { @@ -101,13 +87,14 @@ function asStringArray (value) { if (Array.isArray(value)) { const result = [] - for (let i = 0; i < value.length; i++) { - const element = value[i] + 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( - `Bookmark should be a string, given: '${element}'` + `Bookmark value should be a string, given: '${element}'` ) } result.push(element) @@ -122,40 +109,17 @@ function asStringArray (value) { } /** - * Find latest bookmark in the given array of bookmarks. - * @param {string[]} bookmarks array of bookmarks. - * @return {string|null} latest bookmark value. - */ -function maxBookmark (bookmarks) { - if (!bookmarks || bookmarks.length === 0) { - return null - } - - let maxBookmark = bookmarks[0] - let maxValue = bookmarkValue(maxBookmark) - - for (let i = 1; i < bookmarks.length; i++) { - const bookmark = bookmarks[i] - const value = bookmarkValue(bookmark) - - if (value > maxValue) { - maxBookmark = bookmark - maxValue = value - } - } - - return maxBookmark -} - -/** - * Calculate numeric value for the given bookmark. - * @param {string} bookmark argument to get numeric value for. - * @return {number} value of the bookmark. + * 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 bookmarkValue (bookmark) { - if (bookmark && bookmark.indexOf(BOOKMARK_PREFIX) === 0) { - const result = parseInt(bookmark.substring(BOOKMARK_PREFIX.length)) - return result || UNKNOWN_BOOKMARK_VALUE - } - return UNKNOWN_BOOKMARK_VALUE +function flattenArray (values) { + return values.reduce( + (dest, value) => + Array.isArray(value) + ? dest.concat(flattenArray(value)) + : dest.concat(value), + [] + ) } diff --git a/src/internal/connection-holder.js b/src/internal/connection-holder.js index 84a7457f1..60eaac442 100644 --- a/src/internal/connection-holder.js +++ b/src/internal/connection-holder.js @@ -20,6 +20,7 @@ import { newError } from '../error' import { assertString } from './util' import { ACCESS_MODE_WRITE } from './constants' +import Bookmark from './bookmark' /** * Utility to lazily initialize connections and return them back to the pool when unused. @@ -34,10 +35,12 @@ export default class ConnectionHolder { constructor ({ mode = ACCESS_MODE_WRITE, database = '', + bookmark, connectionProvider } = {}) { this._mode = mode this._database = database ? assertString(database, 'database') : '' + this._bookmark = bookmark || Bookmark.empty() this._connectionProvider = connectionProvider this._referenceCount = 0 this._connectionPromise = Promise.resolve(null) @@ -65,10 +68,11 @@ export default class ConnectionHolder { */ initializeConnection () { if (this._referenceCount === 0) { - this._connectionPromise = this._connectionProvider.acquireConnection( - this._mode, - this._database - ) + this._connectionPromise = this._connectionProvider.acquireConnection({ + accessMode: this._mode, + database: this._database, + bookmark: this._bookmark + }) } this._referenceCount++ } diff --git a/src/internal/connection-provider-direct.js b/src/internal/connection-provider-direct.js index b791ce543..e51f77127 100644 --- a/src/internal/connection-provider-direct.js +++ b/src/internal/connection-provider-direct.js @@ -27,7 +27,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { this._address = address } - acquireConnection (accessMode, database) { + acquireConnection ({ accessMode, database, bookmarks } = {}) { return this._connectionPool .acquire(this._address) .then(connection => new DelegateConnection(connection, null)) diff --git a/src/internal/connection-provider-routing.js b/src/internal/connection-provider-routing.js index 84255231c..37484f6c5 100644 --- a/src/internal/connection-provider-routing.js +++ b/src/internal/connection-provider-routing.js @@ -30,6 +30,7 @@ import PooledConnectionProvider from './connection-provider-pooled' import ConnectionErrorHandler from './connection-error-handler' import DelegateConnection from './connection-delegate' import LeastConnectedLoadBalancingStrategy from './least-connected-load-balancing-strategy' +import Bookmark from './bookmark' const UNAUTHORIZED_ERROR_CODE = 'Neo.ClientError.Security.Unauthorized' const DATABASE_NOT_FOUND_ERROR_CODE = @@ -95,55 +96,56 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - acquireConnection (accessMode, database) { + async acquireConnection ({ accessMode, database, bookmark } = {}) { + let name + let address + const databaseSpecificErrorHandler = new ConnectionErrorHandler( SESSION_EXPIRED, (error, address) => this._handleUnavailability(error, address, database), (error, address) => this._handleWriteFailure(error, address, database) ) - return this._freshRoutingTable(accessMode, database || DEFAULT_DB_NAME) - .then(routingTable => { - let name - let address + const routingTable = await this._freshRoutingTable({ + accessMode, + database: database || DEFAULT_DB_NAME, + bookmark + }) - 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) - } + // 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) + } - if (!address) { - throw newError( - `Failed to obtain connection towards ${name} server. Known routing table is: ${routingTable}`, - SESSION_EXPIRED - ) - } + // 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 + ) + } - return this._acquireConnectionToServer( - address, - name, - routingTable - ).catch(error => { - const transformed = databaseSpecificErrorHandler.handleAndTransformError( - error, - address - ) - throw transformed - }) - }) - .then( - connection => - new DelegateConnection(connection, databaseSpecificErrorHandler) + try { + const connection = await this._acquireConnectionToServer( + address, + name, + routingTable ) + + return new DelegateConnection(connection, databaseSpecificErrorHandler) + } catch (error) { + const transformed = databaseSpecificErrorHandler.handleAndTransformError( + error, + address + ) + throw transformed + } } forget (address, database) { @@ -174,211 +176,227 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return this._connectionPool.acquire(address) } - _freshRoutingTable (accessMode, database) { + _freshRoutingTable ({ accessMode, database, bookmark } = {}) { const currentRoutingTable = this._routingTables[database] || new RoutingTable({ database }) if (!currentRoutingTable.isStaleFor(accessMode)) { - return Promise.resolve(currentRoutingTable) + return currentRoutingTable } this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable) + return this._refreshRoutingTable(currentRoutingTable, bookmark) } - _refreshRoutingTable (currentRoutingTable) { + _refreshRoutingTable (currentRoutingTable, bookmark) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { return this._fetchRoutingTableFromSeedRouterFallbackToKnownRouters( knownRouters, - currentRoutingTable + currentRoutingTable, + bookmark ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( knownRouters, - currentRoutingTable + currentRoutingTable, + bookmark ) } - _fetchRoutingTableFromSeedRouterFallbackToKnownRouters ( + async _fetchRoutingTableFromSeedRouterFallbackToKnownRouters ( knownRouters, - currentRoutingTable + currentRoutingTable, + bookmark ) { // we start with seed router, no routers were probed before const seenRouters = [] - return this._fetchRoutingTableUsingSeedRouter( + let newRoutingTable = await this._fetchRoutingTableUsingSeedRouter( seenRouters, this._seedRouter, - currentRoutingTable + currentRoutingTable, + bookmark ) - .then(newRoutingTable => { - if (newRoutingTable) { - this._useSeedRouter = false - return newRoutingTable - } - // seed router did not return a valid routing table - try to use other known routers - return this._fetchRoutingTableUsingKnownRouters( - knownRouters, - currentRoutingTable - ) - }) - .then(newRoutingTable => - this._applyRoutingTableIfPossible(currentRoutingTable, newRoutingTable) + if (newRoutingTable) { + this._useSeedRouter = false + } else { + // seed router did not return a valid routing table - try to use other known routers + newRoutingTable = await this._fetchRoutingTableUsingKnownRouters( + knownRouters, + currentRoutingTable, + bookmark ) + } + + return await this._applyRoutingTableIfPossible( + currentRoutingTable, + newRoutingTable + ) } - _fetchRoutingTableFromKnownRoutersFallbackToSeedRouter ( + async _fetchRoutingTableFromKnownRoutersFallbackToSeedRouter ( knownRouters, - currentRoutingTable + currentRoutingTable, + bookmark ) { - return this._fetchRoutingTableUsingKnownRouters( + let newRoutingTable = await this._fetchRoutingTableUsingKnownRouters( knownRouters, - currentRoutingTable + currentRoutingTable, + bookmark ) - .then(newRoutingTable => { - if (newRoutingTable) { - return newRoutingTable - } - // none of the known routers returned a valid routing table - try to use seed router address for rediscovery - return this._fetchRoutingTableUsingSeedRouter( - knownRouters, - this._seedRouter, - currentRoutingTable - ) - }) - .then(newRoutingTable => - this._applyRoutingTableIfPossible(currentRoutingTable, newRoutingTable) + if (!newRoutingTable) { + // none of the known routers returned a valid routing table - try to use seed router address for rediscovery + newRoutingTable = await this._fetchRoutingTableUsingSeedRouter( + knownRouters, + this._seedRouter, + currentRoutingTable, + bookmark ) + } + + return await this._applyRoutingTableIfPossible( + currentRoutingTable, + newRoutingTable + ) } - _fetchRoutingTableUsingKnownRouters (knownRouters, currentRoutingTable) { - return this._fetchRoutingTable(knownRouters, currentRoutingTable).then( - newRoutingTable => { - if (newRoutingTable) { - // one of the known routers returned a valid routing table - use it - return newRoutingTable - } + async _fetchRoutingTableUsingKnownRouters ( + knownRouters, + currentRoutingTable, + bookmark + ) { + const newRoutingTable = await this._fetchRoutingTable( + knownRouters, + currentRoutingTable, + bookmark + ) - // 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 - ) + if (newRoutingTable) { + // one of the known routers returned a valid routing table - use it + return newRoutingTable + } - return 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 } - _fetchRoutingTableUsingSeedRouter (seenRouters, seedRouter, routingTable) { - const resolvedAddresses = this._resolveSeedRouter(seedRouter) - return resolvedAddresses.then(resolvedRouterAddresses => { - // filter out all addresses that we've already tried - const newAddresses = resolvedRouterAddresses.filter( - address => seenRouters.indexOf(address) < 0 - ) - return this._fetchRoutingTable(newAddresses, routingTable) - }) + async _fetchRoutingTableUsingSeedRouter ( + seenRouters, + seedRouter, + routingTable, + bookmark + ) { + 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, bookmark) } - _resolveSeedRouter (seedRouter) { - const customResolution = this._hostNameResolver.resolve(seedRouter) - const dnsResolutions = customResolution.then(resolvedAddresses => { - return Promise.all( - resolvedAddresses.map(address => { - return this._dnsResolver.resolve(address) - }) - ) - }) - return dnsResolutions.then(results => { - return [].concat.apply([], results) - }) + 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) } - _fetchRoutingTable (routerAddresses, routingTable) { + _fetchRoutingTable (routerAddresses, routingTable, bookmark) { return routerAddresses.reduce( - (refreshedTablePromise, currentRouter, currentIndex) => { - return refreshedTablePromise.then(newRoutingTable => { - if (newRoutingTable) { - // valid routing table was fetched - just return it, try next router otherwise - return newRoutingTable - } 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 - ) - } + async (refreshedTablePromise, currentRouter, currentIndex) => { + const newRoutingTable = await refreshedTablePromise - // try next router - return this._createSessionForRediscovery(currentRouter).then( - session => { - if (session) { - return this._rediscovery - .lookupRoutingTableOnRouter( - session, - routingTable.database, - currentRouter - ) - .catch(error => { - if (error && error.code === DATABASE_NOT_FOUND_ERROR_CODE) { - // not finding the target database is a sign of a configuration issue - throw error - } - this._log.warn( - `unable to fetch routing table because of an error ${error}` - ) - return null - }) - } 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 - } - } + if (newRoutingTable) { + // valid routing table was fetched - just return it, try next router otherwise + return newRoutingTable + } 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 = await this._createSessionForRediscovery( + currentRouter, + bookmark + ) + if (session) { + try { + return await this._rediscovery.lookupRoutingTableOnRouter( + session, + routingTable.database, + currentRouter + ) + } catch (error) { + if (error && error.code === DATABASE_NOT_FOUND_ERROR_CODE) { + // not finding the target database is a sign of a configuration issue + throw error + } + this._log.warn( + `unable to fetch routing table because of an error ${error}` + ) + return null + } + } 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 + } }, Promise.resolve(null) ) } - _createSessionForRediscovery (routerAddress) { - return this._connectionPool - .acquire(routerAddress) - .then(connection => { - const connectionProvider = new SingleConnectionProvider(connection) - - const version = ServerVersion.fromString(connection.version) - if (version.compareTo(VERSION_4_0_0) < 0) { - return new Session({ mode: WRITE, connectionProvider }) - } + async _createSessionForRediscovery (routerAddress, bookmark) { + try { + const connection = await this._connectionPool.acquire(routerAddress) + const connectionProvider = new SingleConnectionProvider(connection) + const version = ServerVersion.fromString(connection.version) + if (version.compareTo(VERSION_4_0_0) < 0) { return new Session({ - mode: READ, - database: SYSTEM_DB_NAME, + mode: WRITE, + bookmark: Bookmark.empty(), connectionProvider }) + } + + return new Session({ + mode: READ, + database: SYSTEM_DB_NAME, + bookmark, + connectionProvider }) - .catch(error => { - // unable to acquire connection towards the given router - if (error && error.code === UNAUTHORIZED_ERROR_CODE) { - // auth error and not finding system database is a sign of a configuration issue - // discovery should not proceed - throw error - } - return null - }) + } catch (error) { + // unable to acquire connection towards the given router + if (error && error.code === UNAUTHORIZED_ERROR_CODE) { + // auth error and not finding system database is a sign of a configuration issue + // discovery should not proceed + throw error + } + return null + } } async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable) { diff --git a/src/internal/connection-provider-single.js b/src/internal/connection-provider-single.js index e13c5132c..5d518c294 100644 --- a/src/internal/connection-provider-single.js +++ b/src/internal/connection-provider-single.js @@ -25,7 +25,7 @@ export default class SingleConnectionProvider extends ConnectionProvider { this._connection = connection } - acquireConnection (mode, database) { + acquireConnection ({ accessMode, database, bookmarks } = {}) { const connection = this._connection this._connection = null return Promise.resolve(connection) diff --git a/src/internal/connection-provider.js b/src/internal/connection-provider.js index 515390647..70d7d9035 100644 --- a/src/internal/connection-provider.js +++ b/src/internal/connection-provider.js @@ -18,7 +18,7 @@ */ export default class ConnectionProvider { - acquireConnection (accessMode, database) { + acquireConnection ({ accessMode, database, bookmarks } = {}) { throw new Error('not implemented') } diff --git a/src/internal/rediscovery.js b/src/internal/rediscovery.js index d1e572a31..ab8741d65 100644 --- a/src/internal/rediscovery.js +++ b/src/internal/rediscovery.js @@ -18,6 +18,7 @@ */ import RoutingTable from './routing-table' +import RoutingUtil from './routing-util' import { newError, PROTOCOL_ERROR } from '../error' export default class Rediscovery { @@ -36,49 +37,50 @@ export default class Rediscovery { * @param {string} routerAddress the URL of the router. * @return {Promise} promise resolved with new routing table or null when connection error happened. */ - lookupRoutingTableOnRouter (session, database, routerAddress) { - return this._routingUtil - .callRoutingProcedure(session, database, routerAddress) - .then(records => { - if (records === null) { - // connection error happened, unable to retrieve routing table from this router, next one should be queried - return null - } + async lookupRoutingTableOnRouter (session, database, routerAddress) { + const records = await this._routingUtil.callRoutingProcedure( + session, + database, + routerAddress + ) + if (records === null) { + // connection error happened, unable to retrieve routing table from this router, next one should be queried + return null + } - if (records.length !== 1) { - throw newError( - 'Illegal response from router "' + - routerAddress + - '". ' + - 'Received ' + - records.length + - ' records but expected only one.\n' + - JSON.stringify(records), - PROTOCOL_ERROR - ) - } + if (records.length !== 1) { + throw newError( + 'Illegal response from router "' + + routerAddress + + '". ' + + 'Received ' + + records.length + + ' records but expected only one.\n' + + JSON.stringify(records), + PROTOCOL_ERROR + ) + } - const record = records[0] + const record = records[0] - const expirationTime = this._routingUtil.parseTtl(record, routerAddress) - const { routers, readers, writers } = this._routingUtil.parseServers( - record, - routerAddress - ) + const expirationTime = this._routingUtil.parseTtl(record, routerAddress) + const { routers, readers, writers } = this._routingUtil.parseServers( + record, + routerAddress + ) - Rediscovery._assertNonEmpty(routers, 'routers', routerAddress) - Rediscovery._assertNonEmpty(readers, 'readers', routerAddress) - // case with no writers is processed higher in the promise chain because only RoutingDriver knows - // how to deal with such table and how to treat router that returned such table + Rediscovery._assertNonEmpty(routers, 'routers', routerAddress) + Rediscovery._assertNonEmpty(readers, 'readers', routerAddress) + // case with no writers is processed higher in the promise chain because only RoutingDriver knows + // how to deal with such table and how to treat router that returned such table - return new RoutingTable({ - database, - routers, - readers, - writers, - expirationTime - }) - }) + return new RoutingTable({ + database, + routers, + readers, + writers, + expirationTime + }) } static _assertNonEmpty (serverAddressesArray, serversName, routerAddress) { diff --git a/src/internal/routing-util.js b/src/internal/routing-util.js index 5e1e0cdad..f7b0ec06d 100644 --- a/src/internal/routing-util.js +++ b/src/internal/routing-util.js @@ -21,6 +21,7 @@ import { newError, PROTOCOL_ERROR, SERVICE_UNAVAILABLE } from '../error' import Integer, { int } from '../integer' import { ServerVersion, VERSION_4_0_0 } from './server-version' import Bookmark from './bookmark' +import Session from '../session' import TxConfig from './tx-config' import ServerAddress from './server-address' @@ -43,24 +44,31 @@ export default class RoutingUtil { * @return {Promise} promise resolved with records returned by the procedure call or null if * connection error happened. */ - callRoutingProcedure (session, database, routerAddress) { - return this._callAvailableRoutingProcedure(session, database) - .then(result => session.close().then(() => result.records)) - .catch(error => { - if (error.code === DATABASE_NOT_FOUND_CODE) { - 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 - ) - } else { - // return nothing when failed to connect because code higher in the callstack is still able to retry with a - // different session towards a different router - return null - } - }) + async callRoutingProcedure (session, database, routerAddress) { + try { + const result = await this._callAvailableRoutingProcedure( + session, + database + ) + + await session.close() + + return result.records + } catch (error) { + if (error.code === DATABASE_NOT_FOUND_CODE) { + 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 + ) + } else { + // return nothing when failed to connect because code higher in the callstack is still able to retry with a + // different session towards a different router + return null + } + } } parseTtl (record, routerAddress) { @@ -146,7 +154,7 @@ export default class RoutingUtil { } return connection.protocol().run(query, params, { - bookmark: Bookmark.empty(), + bookmark: session._lastBookmark, txConfig: TxConfig.empty(), mode: session._mode, database: session._database, diff --git a/src/session.js b/src/session.js index 32bd94e52..b3e109401 100644 --- a/src/session.js +++ b/src/session.js @@ -64,11 +64,13 @@ class Session { this._readConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_READ, database, + bookmark, connectionProvider }) this._writeConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_WRITE, database, + bookmark, connectionProvider }) this._open = true @@ -190,7 +192,7 @@ class Session { * @return {string|null} a reference to a previous transaction */ lastBookmark () { - return this._lastBookmark.maxBookmarkAsString() + return this._lastBookmark.values() } /** diff --git a/test/internal/bookmark.test.js b/test/internal/bookmark.test.js index 1bd097a1a..61c748af2 100644 --- a/test/internal/bookmark.test.js +++ b/test/internal/bookmark.test.js @@ -23,7 +23,7 @@ describe('#unit Bookmark', () => { const bookmark = new Bookmark('neo4j:bookmark:v1:tx412') expect(bookmark.isEmpty()).toBeFalsy() - expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx412') + expect(bookmark.values()).toEqual(['neo4j:bookmark:v1:tx412']) }) it('should be possible to construct bookmark from string array', () => { @@ -34,35 +34,60 @@ describe('#unit Bookmark', () => { ]) expect(bookmark.isEmpty()).toBeFalsy() - expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx3') + expect(bookmark.values()).toEqual([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2', + 'neo4j:bookmark:v1:tx3' + ]) }) it('should be possible to construct bookmark from null', () => { const bookmark = new Bookmark(null) expect(bookmark.isEmpty()).toBeTruthy() - expect(bookmark.maxBookmarkAsString()).toBeNull() + expect(bookmark.values()).toEqual([]) }) it('should be possible to construct bookmark from undefined', () => { const bookmark = new Bookmark(undefined) expect(bookmark.isEmpty()).toBeTruthy() - expect(bookmark.maxBookmarkAsString()).toBeNull() + expect(bookmark.values()).toEqual([]) }) it('should be possible to construct bookmark from an empty string', () => { const bookmark = new Bookmark('') expect(bookmark.isEmpty()).toBeTruthy() - expect(bookmark.maxBookmarkAsString()).toBeNull() + expect(bookmark.values()).toEqual([]) }) it('should be possible to construct bookmark from empty array', () => { const bookmark = new Bookmark([]) expect(bookmark.isEmpty()).toBeTruthy() - expect(bookmark.maxBookmarkAsString()).toBeNull() + expect(bookmark.values()).toEqual([]) + }) + + it('should be possible to construct bookmark from nested arrays', () => { + const bookmark = new Bookmark([ + 'neo4j:bookmark:v1:tx1', + ['neo4j:bookmark:v1:tx2'], + [ + ['neo4j:bookmark:v1:tx3', 'neo4j:bookmark:v1:tx4'], + ['neo4j:bookmark:v1:tx5', 'neo4j:bookmark:v1:tx6'] + ] + ]) + + expect(bookmark.isEmpty()).toBeFalsy() + expect(bookmark.values()).toEqual([ + 'neo4j:bookmark:v1:tx1', + 'neo4j:bookmark:v1:tx2', + 'neo4j:bookmark:v1:tx3', + 'neo4j:bookmark:v1:tx4', + 'neo4j:bookmark:v1:tx5', + 'neo4j:bookmark:v1:tx6' + ]) }) it('should not be possible to construct bookmark from object', () => { @@ -86,10 +111,10 @@ describe('#unit Bookmark', () => { const bookmark = new Bookmark('neo4j:bookmark:v1:txWrong') expect(bookmark.isEmpty()).toBeFalsy() - expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:txWrong') + expect(bookmark.values()).toEqual(['neo4j:bookmark:v1:txWrong']) }) - it('should skip unparsable bookmarks', () => { + it('should keep unparsable bookmarks', () => { const bookmark = new Bookmark([ 'neo4j:bookmark:v1:tx42', 'neo4j:bookmark:v1:txWrong', @@ -97,7 +122,11 @@ describe('#unit Bookmark', () => { ]) expect(bookmark.isEmpty()).toBeFalsy() - expect(bookmark.maxBookmarkAsString()).toEqual('neo4j:bookmark:v1:tx4242') + expect(bookmark.values()).toEqual([ + 'neo4j:bookmark:v1:tx42', + 'neo4j:bookmark:v1:txWrong', + 'neo4j:bookmark:v1:tx4242' + ]) }) it('should turn into empty transaction params when empty', () => { @@ -112,7 +141,6 @@ describe('#unit Bookmark', () => { expect(bookmark.isEmpty()).toBeFalsy() expect(bookmark.asBeginTransactionParameters()).toEqual({ - bookmark: 'neo4j:bookmark:v1:tx142', bookmarks: ['neo4j:bookmark:v1:tx142'] }) }) @@ -127,7 +155,6 @@ describe('#unit Bookmark', () => { expect(bookmark.isEmpty()).toBeFalsy() expect(bookmark.asBeginTransactionParameters()).toEqual({ - bookmark: 'neo4j:bookmark:v1:tx42', bookmarks: [ 'neo4j:bookmark:v1:tx1', 'neo4j:bookmark:v1:tx3', diff --git a/test/internal/connection-provider-direct.test.js b/test/internal/connection-provider-direct.test.js index 15d549992..12c84e041 100644 --- a/test/internal/connection-provider-direct.test.js +++ b/test/internal/connection-provider-direct.test.js @@ -31,13 +31,15 @@ describe('#unit DirectConnectionProvider', () => { const pool = newPool() const connectionProvider = newDirectConnectionProvider(address, pool) - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection).toBeDefined() - expect(connection.address).toEqual(address) - expect(pool.has(address)).toBeTruthy() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection).toBeDefined() + expect(connection.address).toEqual(address) + expect(pool.has(address)).toBeTruthy() - done() - }) + done() + }) }) it('acquires connection and returns a DelegateConnection', async () => { @@ -45,7 +47,10 @@ describe('#unit DirectConnectionProvider', () => { const pool = newPool() const connectionProvider = newDirectConnectionProvider(address, pool) - const conn = await connectionProvider.acquireConnection(READ, '') + const conn = await connectionProvider.acquireConnection({ + accessMode: READ, + database: '' + }) expect(conn instanceof DelegateConnection).toBeTruthy() }) }) diff --git a/test/internal/connection-provider-routing.test.js b/test/internal/connection-provider-routing.test.js index df320d999..ca66bd712 100644 --- a/test/internal/connection-provider-routing.test.js +++ b/test/internal/connection-provider-routing.test.js @@ -197,10 +197,16 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection(READ, '') + const conn1 = await connectionProvider.acquireConnection({ + accessMode: READ, + database: '' + }) expect(conn1 instanceof DelegateConnection).toBeTruthy() - const conn2 = await connectionProvider.acquireConnection(WRITE, '') + const conn2 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: '' + }) expect(conn2 instanceof DelegateConnection).toBeTruthy() }) @@ -218,17 +224,21 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(server3) - expect(pool.has(server3)).toBeTruthy() - - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(server4) - expect(pool.has(server4)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(server3) + expect(pool.has(server3)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(server4) + expect(pool.has(server4)).toBeTruthy() + + done() + }) }) - }) }) it('acquires write connection with up-to-date routing table', done => { @@ -245,17 +255,21 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(server5) - expect(pool.has(server5)).toBeTruthy() - - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(server6) - expect(pool.has(server6)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(server5) + expect(pool.has(server5)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(server6) + expect(pool.has(server6)).toBeTruthy() + + done() + }) }) - }) }) it('throws for illegal access mode', done => { @@ -268,10 +282,12 @@ describe('#unit RoutingConnectionProvider', () => { ) ]) - connectionProvider.acquireConnection('WRONG', '').catch(error => { - expect(error.message).toEqual('Illegal mode WRONG') - done() - }) + connectionProvider + .acquireConnection({ accessMode: 'WRONG', database: '' }) + .catch(error => { + expect(error.message).toEqual('Illegal mode WRONG') + done() + }) }) it('refreshes stale routing table to get read connection', done => { @@ -296,17 +312,21 @@ describe('#unit RoutingConnectionProvider', () => { { '': { 'server1:7687': updatedRoutingTable } } ) - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverC) - expect(pool.has(serverC)).toBeTruthy() - - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverD) - expect(pool.has(serverD)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverC) + expect(pool.has(serverC)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverD) + expect(pool.has(serverD)).toBeTruthy() + + done() + }) }) - }) }) it('refreshes stale routing table to get write connection', done => { @@ -331,17 +351,21 @@ describe('#unit RoutingConnectionProvider', () => { { '': { 'server1:7687': updatedRoutingTable } } ) - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverE) - expect(pool.has(serverE)).toBeTruthy() - - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverF) - expect(pool.has(serverF)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverE) + expect(pool.has(serverE)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverF) + expect(pool.has(serverF)).toBeTruthy() + + done() + }) }) - }) }) it('refreshes stale routing table to get read connection when one router fails', done => { @@ -371,17 +395,21 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverC) - expect(pool.has(serverC)).toBeTruthy() - - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverD) - expect(pool.has(serverD)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverC) + expect(pool.has(serverC)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverD) + expect(pool.has(serverD)).toBeTruthy() + + done() + }) }) - }) }) it('refreshes stale routing table to get write connection when one router fails', done => { @@ -411,17 +439,21 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverE) - expect(pool.has(serverE)).toBeTruthy() - - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverF) - expect(pool.has(serverF)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverE) + expect(pool.has(serverE)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverF) + expect(pool.has(serverF)).toBeTruthy() + + done() + }) }) - }) }) it('refreshes routing table without readers to get read connection', done => { @@ -451,17 +483,21 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverC) - expect(pool.has(serverC)).toBeTruthy() - - connectionProvider.acquireConnection(READ, '').then(connection => { - expect(connection.address).toEqual(serverD) - expect(pool.has(serverD)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverC) + expect(pool.has(serverC)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverD) + expect(pool.has(serverD)).toBeTruthy() + + done() + }) }) - }) }) it('refreshes routing table without writers to get write connection', done => { @@ -491,17 +527,21 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverE) - expect(pool.has(serverE)).toBeTruthy() - - connectionProvider.acquireConnection(WRITE, '').then(connection => { - expect(connection.address).toEqual(serverF) - expect(pool.has(serverF)).toBeTruthy() - - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverE) + expect(pool.has(serverE)).toBeTruthy() + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection => { + expect(connection.address).toEqual(serverF) + expect(pool.has(serverF)).toBeTruthy() + + done() + }) }) - }) }) it('throws when all routers return nothing while getting read connection', done => { @@ -524,10 +564,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + done() + }) }) it('throws when all routers return nothing while getting write connection', done => { @@ -550,10 +592,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - done() - }) + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + done() + }) }) it('throws when all routers return routing tables without readers while getting read connection', done => { @@ -582,10 +626,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SESSION_EXPIRED) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SESSION_EXPIRED) + done() + }) }) it('throws when all routers return routing tables without writers while getting write connection', done => { @@ -614,10 +660,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SESSION_EXPIRED) - done() - }) + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SESSION_EXPIRED) + done() + }) }) it('throws when stale routing table without routers while getting read connection', done => { @@ -634,10 +682,12 @@ describe('#unit RoutingConnectionProvider', () => { newPool() ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + done() + }) }) it('throws when stale routing table without routers while getting write connection', done => { @@ -654,10 +704,12 @@ describe('#unit RoutingConnectionProvider', () => { newPool() ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - done() - }) + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + done() + }) }) it('updates routing table after refresh', done => { @@ -686,24 +738,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').then(() => { - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC, serverD], - [serverE, serverF] - ) - expectPoolToNotContain(pool, [ - server1, - server2, - server3, - server4, - server5, - server6 - ]) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(() => { + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC, serverD], + [serverE, serverF] + ) + expectPoolToNotContain(pool, [ + server1, + server2, + server3, + server4, + server5, + server6 + ]) + done() + }) }) it('forgets all routers when they fail while acquiring read connection', done => { @@ -720,17 +774,19 @@ describe('#unit RoutingConnectionProvider', () => { newPool() ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - expectRoutingTable( - connectionProvider, - '', - [], - [server4, server5], - [server6, server7] - ) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + expectRoutingTable( + connectionProvider, + '', + [], + [server4, server5], + [server6, server7] + ) + done() + }) }) it('forgets all routers when they fail while acquiring write connection', done => { @@ -747,17 +803,19 @@ describe('#unit RoutingConnectionProvider', () => { newPool() ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - expectRoutingTable( - connectionProvider, - '', - [], - [server4, server5], - [server6, server7] - ) - done() - }) + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + expectRoutingTable( + connectionProvider, + '', + [], + [server4, server5], + [server6, server7] + ) + done() + }) }) it('uses seed router address when all existing routers fail', done => { @@ -790,22 +848,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').then(connection1 => { - expect(connection1.address).toEqual(serverD) - - connectionProvider.acquireConnection(WRITE, '').then(connection2 => { - expect(connection2.address).toEqual(serverF) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB, serverC], - [serverD, serverE], - [serverF, serverG] - ) - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverD) + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverF) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB, serverC], + [serverD, serverE], + [serverF, serverG] + ) + done() + }) }) - }) }) it('uses resolved seed router address when all existing routers fail', done => { @@ -838,22 +900,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection1 => { - expect(connection1.address).toEqual(serverE) - - connectionProvider.acquireConnection(READ, '').then(connection2 => { - expect(connection2.address).toEqual(serverC) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC, serverD], - [serverE, serverF] - ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverE) + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverC) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC, serverD], + [serverE, serverF] + ) + done() + }) }) - }) }) it('uses resolved seed router address that returns correct routing table when all existing routers fail', done => { @@ -886,22 +952,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection1 => { - expect(connection1.address).toEqual(serverD) - - connectionProvider.acquireConnection(WRITE, '').then(connection2 => { - expect(connection2.address).toEqual(serverE) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC], - [serverD, serverE] - ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverD) + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverE) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC], + [serverD, serverE] + ) + done() + }) }) - }) }) it('fails when both existing routers and seed router fail to return a routing table', done => { @@ -927,18 +997,9 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - - expectRoutingTable( - connectionProvider, - '', - [], // all routers were forgotten because they failed - [server4, server5], - [server6] - ) - - connectionProvider.acquireConnection(WRITE, '').catch(error => { + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { expect(error.code).toEqual(SERVICE_UNAVAILABLE) expectRoutingTable( @@ -949,9 +1010,22 @@ describe('#unit RoutingConnectionProvider', () => { [server6] ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + + expectRoutingTable( + connectionProvider, + '', + [], // all routers were forgotten because they failed + [server4, server5], + [server6] + ) + + done() + }) }) - }) }) it('fails when both existing routers and resolved seed router fail to return a routing table', done => { @@ -976,18 +1050,9 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - - expectRoutingTable( - connectionProvider, - '', - [], // all routers were forgotten because they failed - [server3], - [server4] - ) - - connectionProvider.acquireConnection(READ, '').catch(error => { + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { expect(error.code).toEqual(SERVICE_UNAVAILABLE) expectRoutingTable( @@ -998,9 +1063,22 @@ describe('#unit RoutingConnectionProvider', () => { [server4] ) - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + + expectRoutingTable( + connectionProvider, + '', + [], // all routers were forgotten because they failed + [server3], + [server4] + ) + + done() + }) }) - }) }) it('fails when both existing routers and all resolved seed routers fail to return a routing table', done => { @@ -1027,18 +1105,9 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SERVICE_UNAVAILABLE) - - expectRoutingTable( - connectionProvider, - '', - [], // all known seed servers failed to return routing tables and were forgotten - [server4], - [server5] - ) - - connectionProvider.acquireConnection(WRITE, '').catch(error => { + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { expect(error.code).toEqual(SERVICE_UNAVAILABLE) expectRoutingTable( @@ -1049,9 +1118,22 @@ describe('#unit RoutingConnectionProvider', () => { [server5] ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SERVICE_UNAVAILABLE) + + expectRoutingTable( + connectionProvider, + '', + [], // all known seed servers failed to return routing tables and were forgotten + [server4], + [server5] + ) + + done() + }) }) - }) }) it('uses seed router when no existing routers', done => { @@ -1081,22 +1163,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection1 => { - expect(connection1.address).toEqual(serverD) - - connectionProvider.acquireConnection(READ, '').then(connection2 => { - expect(connection2.address).toEqual(serverC) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC], - [serverD] - ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverD) + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverC) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC], + [serverD] + ) + done() + }) }) - }) }) it('uses resolved seed router when no existing routers', done => { @@ -1126,22 +1212,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').then(connection1 => { - expect(connection1.address).toEqual(serverC) - - connectionProvider.acquireConnection(WRITE, '').then(connection2 => { - expect(connection2.address).toEqual(serverF) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC, serverD], - [serverF, serverE] - ) - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverC) + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverF) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC, serverD], + [serverF, serverE] + ) + done() + }) }) - }) }) it('uses resolved seed router that returns routing table when no existing routers exist', done => { @@ -1173,22 +1263,26 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').then(connection1 => { - expect(connection1.address).toEqual(serverF) - - connectionProvider.acquireConnection(READ, '').then(connection2 => { - expect(connection2.address).toEqual(serverD) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB, serverC], - [serverD, serverE], - [serverF] - ) - done() + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverF) + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverD) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB, serverC], + [serverD, serverE], + [serverF] + ) + done() + }) }) - }) }) it('ignores already probed routers after seed router resolution', done => { @@ -1229,29 +1323,33 @@ describe('#unit RoutingConnectionProvider', () => { usedRouterArrays ) - connectionProvider.acquireConnection(READ, '').then(connection1 => { - expect(connection1.address).toEqual(serverC) - - connectionProvider.acquireConnection(WRITE, '').then(connection2 => { - expect(connection2.address).toEqual(serverE) - - // two sets of routers probed: - // 1) existing routers server1 & server2 - // 2) resolved routers server01 & server02 - expect(usedRouterArrays.length).toEqual(2) - expect(usedRouterArrays[0]).toEqual([server1, server2]) - expect(usedRouterArrays[1]).toEqual([server01, server02]) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC, serverD], - [serverE, serverF] - ) - done() + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverC) + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverE) + + // two sets of routers probed: + // 1) existing routers server1 & server2 + // 2) resolved routers server01 & server02 + expect(usedRouterArrays.length).toEqual(2) + expect(usedRouterArrays[0]).toEqual([server1, server2]) + expect(usedRouterArrays[1]).toEqual([server01, server02]) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC, serverD], + [serverE, serverF] + ) + done() + }) }) - }) }) it('throws session expired when refreshed routing table has no readers', done => { @@ -1280,10 +1378,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(READ, '').catch(error => { - expect(error.code).toEqual(SESSION_EXPIRED) - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .catch(error => { + expect(error.code).toEqual(SESSION_EXPIRED) + done() + }) }) it('throws session expired when refreshed routing table has no writers', done => { @@ -1312,10 +1412,12 @@ describe('#unit RoutingConnectionProvider', () => { } ) - connectionProvider.acquireConnection(WRITE, '').catch(error => { - expect(error.code).toEqual(SESSION_EXPIRED) - done() - }) + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .catch(error => { + expect(error.code).toEqual(SESSION_EXPIRED) + done() + }) }) it('should use resolved seed router after accepting table with no writers', done => { @@ -1357,35 +1459,41 @@ describe('#unit RoutingConnectionProvider', () => { // override default use of seed router connectionProvider._useSeedRouter = false - connectionProvider.acquireConnection(READ, '').then(connection1 => { - expect(connection1.address).toEqual(serverC) - - connectionProvider.acquireConnection(READ, '').then(connection2 => { - expect(connection2.address).toEqual(serverD) - - expectRoutingTable( - connectionProvider, - '', - [serverA, serverB], - [serverC, serverD], - [] - ) - - connectionProvider.acquireConnection(WRITE, '').then(connection3 => { - expect(connection3.address).toEqual(serverEE) - - expectRoutingTable( - connectionProvider, - '', - [serverAA, serverBB], - [serverCC, serverDD], - [serverEE] - ) - - done() - }) + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection1 => { + expect(connection1.address).toEqual(serverC) + + connectionProvider + .acquireConnection({ accessMode: READ, database: '' }) + .then(connection2 => { + expect(connection2.address).toEqual(serverD) + + expectRoutingTable( + connectionProvider, + '', + [serverA, serverB], + [serverC, serverD], + [] + ) + + connectionProvider + .acquireConnection({ accessMode: WRITE, database: '' }) + .then(connection3 => { + expect(connection3.address).toEqual(serverEE) + + expectRoutingTable( + connectionProvider, + '', + [serverAA, serverBB], + [serverCC, serverDD], + [serverEE] + ) + + done() + }) + }) }) - }) }) describe('multi-database', () => { @@ -1404,17 +1512,17 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection( - READ, - 'databaseA' - ) + const conn1 = await connectionProvider.acquireConnection({ + accessMode: READ, + database: 'databaseA' + }) expect(conn1 instanceof DelegateConnection).toBeTruthy() expect(conn1.address).toBe(server1) - const conn2 = await connectionProvider.acquireConnection( - READ, - 'databaseB' - ) + const conn2 = await connectionProvider.acquireConnection({ + accessMode: READ, + database: 'databaseB' + }) expect(conn2 instanceof DelegateConnection).toBeTruthy() expect(conn2.address).toBe(serverA) }) @@ -1434,17 +1542,17 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection( - WRITE, - 'databaseA' - ) + const conn1 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseA' + }) expect(conn1 instanceof DelegateConnection).toBeTruthy() expect(conn1.address).toBe(server2) - const conn2 = await connectionProvider.acquireConnection( - WRITE, - 'databaseB' - ) + const conn2 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseB' + }) expect(conn2 instanceof DelegateConnection).toBeTruthy() expect(conn2.address).toBe(serverB) }) @@ -1459,7 +1567,10 @@ describe('#unit RoutingConnectionProvider', () => { ) try { - await connectionProvider.acquireConnection(WRITE, 'databaseX') + await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseX' + }) } catch (error) { expect(error instanceof Neo4jError).toBeTruthy() expect(error.code).toBe(SERVICE_UNAVAILABLE) @@ -1492,10 +1603,10 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection( - READ, - 'databaseB' - ) + const conn1 = await connectionProvider.acquireConnection({ + accessMode: READ, + database: 'databaseB' + }) // when conn1._errorHandler.handleAndTransformError( @@ -1539,10 +1650,10 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection( - WRITE, - 'databaseB' - ) + const conn1 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseB' + }) // when conn1._errorHandler.handleAndTransformError( @@ -1586,10 +1697,10 @@ describe('#unit RoutingConnectionProvider', () => { pool ) - const conn1 = await connectionProvider.acquireConnection( - WRITE, - 'databaseB' - ) + const conn1 = await connectionProvider.acquireConnection({ + accessMode: WRITE, + database: 'databaseB' + }) // when conn1._errorHandler.handleAndTransformError( @@ -1681,9 +1792,9 @@ function setupRoutingConnectionProviderToRememberRouters ( const originalFetch = connectionProvider._fetchRoutingTable.bind( connectionProvider ) - const rememberingFetch = (routerAddresses, routingTable) => { + const rememberingFetch = (routerAddresses, routingTable, bookmark) => { routersArray.push(routerAddresses) - return originalFetch(routerAddresses, routingTable) + return originalFetch(routerAddresses, routingTable, bookmark) } connectionProvider._fetchRoutingTable = rememberingFetch } diff --git a/test/internal/fake-connection.js b/test/internal/fake-connection.js index 40d6abcc1..a16d99baa 100644 --- a/test/internal/fake-connection.js +++ b/test/internal/fake-connection.js @@ -18,6 +18,7 @@ */ import Connection from '../../src/internal/connection' +import { textChangeRangeIsUnchanged } from 'typescript' /** * This class is like a mock of {@link Connection} that tracks invocations count. @@ -39,6 +40,7 @@ export default class FakeConnection extends Connection { this.releaseInvoked = 0 this.seenStatements = [] this.seenParameters = [] + this.seenProtocolOptions = [] this._server = {} } @@ -69,9 +71,10 @@ export default class FakeConnection extends Connection { protocol () { // return fake protocol object that simply records seen statements and parameters return { - run: (statement, parameters) => { + run: (statement, parameters, protocolOptions) => { this.seenStatements.push(statement) this.seenParameters.push(parameters) + this.seenProtocolOptions.push(protocolOptions) } } } diff --git a/test/internal/node/direct.driver.boltkit.test.js b/test/internal/node/direct.driver.boltkit.test.js index dc3411141..06d595e41 100644 --- a/test/internal/node/direct.driver.boltkit.test.js +++ b/test/internal/node/direct.driver.boltkit.test.js @@ -89,7 +89,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(records[1].get('name')).toEqual('Alice') await tx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) await session.close() await driver.close() @@ -125,7 +125,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(records.length).toEqual(0) await tx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) await session.close() await driver.close() @@ -161,7 +161,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(records1.length).toEqual(0) await writeTx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) const readTx = session.beginTransaction() const result2 = await readTx.run('MATCH (n) RETURN n.name AS name') @@ -170,7 +170,7 @@ describe('#stub-direct direct driver with stub server', () => { expect(records2[0].get('name')).toEqual('Bob') await readTx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx424242']) await session.close() await driver.close() diff --git a/test/internal/node/routing.driver.boltkit.test.js b/test/internal/node/routing.driver.boltkit.test.js index 1ff6d0706..1f0efe2b9 100644 --- a/test/internal/node/routing.driver.boltkit.test.js +++ b/test/internal/node/routing.driver.boltkit.test.js @@ -1106,7 +1106,7 @@ describe('#stub-routing routing driver with stub server', () => { await tx.commit() // Then - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) await session.close() await driver.close() @@ -1148,7 +1148,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[1].get('name')).toEqual('Alice') await tx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) await session.close() await driver.close() @@ -1176,7 +1176,7 @@ describe('#stub-routing routing driver with stub server', () => { const writeTx = session.beginTransaction() await writeTx.run("CREATE (n {name:'Bob'})") await writeTx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) const readTx = session.beginTransaction() const result = await readTx.run('MATCH (n) RETURN n.name AS name') @@ -1185,7 +1185,7 @@ describe('#stub-routing routing driver with stub server', () => { expect(records[0].get('name')).toEqual('Bob') await readTx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx424242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx424242']) await session.close() await driver.close() @@ -1824,7 +1824,7 @@ describe('#stub-routing routing driver with stub server', () => { await tx.run(`CREATE (n {name:'Bob'})`) await tx.commit() - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx95') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx95']) await session.close() await driver.close() @@ -2260,6 +2260,75 @@ describe('#stub-routing routing driver with stub server', () => { await router2.exit() await reader1.exit() }) + + it('should use provided bookmarks for the discovery', async () => { + if (!boltStub.supported) { + return + } + + // Given + const server = await boltStub.start( + './test/resources/boltstub/v4/acquire_endpoints_aDatabase_with_bookmark.script', + 9001 + ) + const readServer = await boltStub.start( + `./test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script`, + 9005 + ) + + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session({ + database: 'aDatabase', + defaultAccessMode: READ, + bookmarks: ['system:1111', 'aDatabase:5555'] + }) + const result = await session.run('MATCH (n) RETURN n.name') + + // Then + expect(result.records.length).toBe(3) + expect(session.lastBookmark()).toEqual(['aDatabase:6666']) + + await session.close() + await driver.close() + await server.exit() + await readServer.exit() + }) + + it('should ignore provided bookmarks for the discovery', async () => { + if (!boltStub.supported) { + return + } + + // Given + const server = await boltStub.start( + './test/resources/boltstub/v3/acquire_endpoints.script', + 9001 + ) + const readServer = await boltStub.start( + `./test/resources/boltstub/v3/read_with_bookmark.script`, + 9005 + ) + + const driver = boltStub.newDriver('neo4j://127.0.0.1:9001') + + // When + const session = driver.session({ + defaultAccessMode: READ, + bookmarks: ['system:1111', 'aDatabase:5555'] + }) + const result = await session.run('MATCH (n) RETURN n.name') + + // Then + expect(result.records.length).toBe(3) + expect(session.lastBookmark()).toEqual(['aDatabase:6666']) + + await session.close() + await driver.close() + await server.exit() + await readServer.exit() + }) }) async function testAddressPurgeOnDatabaseError (script, query, accessMode) { @@ -2339,7 +2408,7 @@ describe('#stub-routing routing driver with stub server', () => { await tx.commit() // Then - expect(session.lastBookmark()).toEqual('neo4j:bookmark:v1:tx4242') + expect(session.lastBookmark()).toEqual(['neo4j:bookmark:v1:tx4242']) await session.close() await driver.close() diff --git a/test/internal/routing-util.test.js b/test/internal/routing-util.test.js index 254194e53..94d5ef2aa 100644 --- a/test/internal/routing-util.test.js +++ b/test/internal/routing-util.test.js @@ -29,6 +29,11 @@ import { import lolex from 'lolex' import FakeConnection from './fake-connection' import ServerAddress from '../../src/internal/server-address' +import Bookmark from '../../src/internal/bookmark' +import { + ACCESS_MODE_READ, + ACCESS_MODE_WRITE +} from '../../src/internal/constants' const ROUTER_ADDRESS = ServerAddress.fromUrl('test.router.com:4242') @@ -169,6 +174,93 @@ describe('#unit RoutingUtil', () => { ) }) + describe('should pass session access mode while invoking routing procedure', () => { + async function verifyAccessMode (mode) { + const connection = new FakeConnection().withServerVersion('Neo4j/4.0.0') + const session = FakeSession.withFakeConnection(connection).withMode(mode) + + await callRoutingProcedure(session, '', {}) + + expect(connection.seenStatements).toEqual([ + 'CALL dbms.routing.getRoutingTable($context, $database)' + ]) + expect(connection.seenParameters).toEqual([ + { context: {}, database: null } + ]) + expect(connection.seenProtocolOptions).toEqual([ + jasmine.objectContaining({ + mode + }) + ]) + } + + it('READ', () => verifyAccessMode(ACCESS_MODE_READ)) + + it('WRITE', () => verifyAccessMode(ACCESS_MODE_WRITE)) + }) + + describe('should pass session database while invoking routing procedure', () => { + async function verifyDatabase (database) { + const connection = new FakeConnection().withServerVersion('Neo4j/4.0.0') + const session = FakeSession.withFakeConnection(connection).withDatabase( + database + ) + + await callRoutingProcedure(session, '', {}) + + expect(connection.seenStatements).toEqual([ + 'CALL dbms.routing.getRoutingTable($context, $database)' + ]) + expect(connection.seenParameters).toEqual([ + { context: {}, database: null } + ]) + expect(connection.seenProtocolOptions).toEqual([ + jasmine.objectContaining({ + database + }) + ]) + } + it('systemdb', () => verifyDatabase('systemdb')) + + it('someOtherDb', () => verifyDatabase('someOtherDb')) + }) + + describe('should pass session bookmark while invoking routing procedure', () => { + async function verifyBookmark (bookmark) { + const connection = new FakeConnection().withServerVersion('Neo4j/4.0.0') + const session = FakeSession.withFakeConnection(connection).withBookmark( + bookmark + ) + + await callRoutingProcedure(session, '', {}) + + expect(connection.seenStatements).toEqual([ + 'CALL dbms.routing.getRoutingTable($context, $database)' + ]) + expect(connection.seenParameters).toEqual([ + { context: {}, database: null } + ]) + expect(connection.seenProtocolOptions).toEqual([ + jasmine.objectContaining({ + bookmark + }) + ]) + } + it('empty', () => verifyBookmark(Bookmark.empty())) + + it('single', () => verifyBookmark(new Bookmark('bookmark1'))) + + it('single item', () => verifyBookmark(new Bookmark(['bookmark1']))) + + it('multiple items', () => + verifyBookmark(new Bookmark(['bookmark1', 'bookmark2', 'bookmark3']))) + + it('nested items', () => + verifyBookmark( + new Bookmark(['bookmark1', ['bookmark2'], ['bookmark3', 'bookmark4']]) + )) + }) + it('should parse valid ttl', () => { const clock = lolex.install() try { @@ -439,6 +531,21 @@ describe('#unit RoutingUtil', () => { return Promise.resolve() } + withBookmark (bookmark) { + this._lastBookmark = bookmark + return this + } + + withDatabase (database) { + this._database = database || '' + return this + } + + withMode (mode) { + this._mode = mode + return this + } + close () { this._closed = true return Promise.resolve() diff --git a/test/resources/boltstub/v2/read_tx_with_bookmarks.script b/test/resources/boltstub/v2/read_tx_with_bookmarks.script index 92d479451..fb57da8a6 100644 --- a/test/resources/boltstub/v2/read_tx_with_bookmarks.script +++ b/test/resources/boltstub/v2/read_tx_with_bookmarks.script @@ -2,7 +2,7 @@ !: AUTO INIT !: AUTO RESET -C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} +C: RUN "BEGIN" {"bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {} SUCCESS {} diff --git a/test/resources/boltstub/v2/write_read_tx_with_bookmarks.script b/test/resources/boltstub/v2/write_read_tx_with_bookmarks.script index 907ce86d5..32f447b5c 100644 --- a/test/resources/boltstub/v2/write_read_tx_with_bookmarks.script +++ b/test/resources/boltstub/v2/write_read_tx_with_bookmarks.script @@ -2,7 +2,7 @@ !: AUTO INIT !: AUTO RESET -C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} +C: RUN "BEGIN" {"bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {"fields": []} SUCCESS {} @@ -14,7 +14,7 @@ C: RUN "COMMIT" {} PULL_ALL S: SUCCESS {"fields": []} SUCCESS {} -C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx4242", "bookmarks": ["neo4j:bookmark:v1:tx4242"]} +C: RUN "BEGIN" {"bookmarks": ["neo4j:bookmark:v1:tx4242"]} PULL_ALL S: SUCCESS {"fields": []} SUCCESS {} diff --git a/test/resources/boltstub/v2/write_tx_with_bookmarks.script b/test/resources/boltstub/v2/write_tx_with_bookmarks.script index 17ceb54c6..f2dba1a8e 100644 --- a/test/resources/boltstub/v2/write_tx_with_bookmarks.script +++ b/test/resources/boltstub/v2/write_tx_with_bookmarks.script @@ -2,7 +2,7 @@ !: AUTO INIT !: AUTO RESET -C: RUN "BEGIN" {"bookmark": "neo4j:bookmark:v1:tx42", "bookmarks": ["neo4j:bookmark:v1:tx42"]} +C: RUN "BEGIN" {"bookmarks": ["neo4j:bookmark:v1:tx42"]} PULL_ALL S: SUCCESS {"fields": []} SUCCESS {} diff --git a/test/resources/boltstub/v3/read_with_bookmark.script b/test/resources/boltstub/v3/read_with_bookmark.script new file mode 100644 index 000000000..68140b69a --- /dev/null +++ b/test/resources/boltstub/v3/read_with_bookmark.script @@ -0,0 +1,12 @@ +!: BOLT 3 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: RUN "MATCH (n) RETURN n.name" {} {"mode": "r", "bookmarks": ["system:1111", "aDatabase:5555"]} + PULL_ALL +S: SUCCESS {"fields": ["n.name"]} + RECORD ["Bob"] + RECORD ["Alice"] + RECORD ["Tina"] + SUCCESS { "bookmark": "aDatabase:6666" } diff --git a/test/resources/boltstub/v4/acquire_endpoints_aDatabase_with_bookmark.script b/test/resources/boltstub/v4/acquire_endpoints_aDatabase_with_bookmark.script new file mode 100644 index 000000000..1dbad46ca --- /dev/null +++ b/test/resources/boltstub/v4/acquire_endpoints_aDatabase_with_bookmark.script @@ -0,0 +1,10 @@ +!: BOLT 4 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {}, "database": "aDatabase"} {"mode": "r", "db": "system", "bookmarks": ["system:1111", "aDatabase:5555"]} + PULL {"n": -1} +S: SUCCESS {"fields": ["ttl", "servers"]} + RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9007","127.0.0.1:9008"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9005","127.0.0.1:9006"], "role": "READ"},{"addresses": ["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"], "role": "ROUTE"}]] + SUCCESS { "bookmark": "system:2222"} diff --git a/test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script b/test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script new file mode 100644 index 000000000..76425b105 --- /dev/null +++ b/test/resources/boltstub/v4/read_from_aDatabase_with_bookmark.script @@ -0,0 +1,12 @@ +!: BOLT 4 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: RUN "MATCH (n) RETURN n.name" {} {"mode": "r", "db": "aDatabase", "bookmarks": ["system:1111", "aDatabase:5555"]} + PULL {"n": -1} +S: SUCCESS {"fields": ["n.name"]} + RECORD ["Bob"] + RECORD ["Alice"] + RECORD ["Tina"] + SUCCESS { "bookmark": "aDatabase:6666" } diff --git a/test/rx/transaction.test.js b/test/rx/transaction.test.js index db16e3339..cf7fe258f 100644 --- a/test/rx/transaction.test.js +++ b/test/rx/transaction.test.js @@ -419,7 +419,7 @@ describe('#integration-rx transaction', () => { await verifyCanCommit(txc2) const bookmark2 = session.lastBookmark() - expect(bookmark0).toBeFalsy() + expect(bookmark0).toEqual([]) expect(bookmark1).toBeTruthy() expect(bookmark1).not.toEqual(bookmark0) expect(bookmark2).toBeTruthy() diff --git a/test/session.test.js b/test/session.test.js index 6877b1bb0..721975435 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -580,7 +580,7 @@ describe('#integration session', () => { it('should update last bookmark after every read tx commit', done => { // new session without initial bookmark session = driver.session() - expect(session.lastBookmark()).toBeNull() + expect(session.lastBookmark()).toEqual([]) const tx = session.beginTransaction() tx.run('RETURN 42 as answer').then(result => { @@ -629,7 +629,7 @@ describe('#integration session', () => { it('should commit read transaction', done => { // new session without initial bookmark session = driver.session() - expect(session.lastBookmark()).toBeNull() + expect(session.lastBookmark()).toEqual([]) const resultPromise = session.readTransaction(tx => tx.run('RETURN 42 AS answer') @@ -930,7 +930,7 @@ describe('#integration session', () => { } expect(_.uniq(bookmarks).length).toEqual(nodeCount) - bookmarks.forEach(bookmark => expect(_.isString(bookmark)).toBeTruthy()) + bookmarks.forEach(bookmark => expect(_.isArray(bookmark)).toBeTruthy()) const session = driver.session({ defaultAccessMode: READ, bookmarks }) try { diff --git a/test/transaction.test.js b/test/transaction.test.js index 3dd72b3f5..6c61063dc 100644 --- a/test/transaction.test.js +++ b/test/transaction.test.js @@ -276,7 +276,7 @@ describe('#integration transaction', () => { it('should provide bookmark on commit', done => { // new session without initial bookmark session = driver.session() - expect(session.lastBookmark()).toBeNull() + expect(session.lastBookmark()).toEqual([]) const tx = session.beginTransaction() tx.run('CREATE (:TXNode1)') @@ -296,7 +296,7 @@ describe('#integration transaction', () => { it('should have bookmark when tx is rolled back', done => { // new session without initial bookmark session = driver.session() - expect(session.lastBookmark()).toBeNull() + expect(session.lastBookmark()).toEqual([]) const tx1 = session.beginTransaction() tx1.run('CREATE ()').then(() => { @@ -327,7 +327,7 @@ describe('#integration transaction', () => { it('should have no bookmark when tx fails', done => { // new session without initial bookmark session = driver.session() - expect(session.lastBookmark()).toBeNull() + expect(session.lastBookmark()).toEqual([]) const tx1 = session.beginTransaction()