diff --git a/.evergreen/buildvariants-and-tasks.in.yml b/.evergreen/buildvariants-and-tasks.in.yml index 010e6b8d925..1c3989548c6 100644 --- a/.evergreen/buildvariants-and-tasks.in.yml +++ b/.evergreen/buildvariants-and-tasks.in.yml @@ -246,14 +246,6 @@ buildvariants: - name: e2e-coverage-<%= group.number %> <% } %> - - name: e2e-multiple-connections - display_name: E2E Multiple Connections - run_on: ubuntu2004-large - tasks: - <% for(const group of E2E_TEST_GROUPS) { %> - - name: e2e-multiple-connections-<%= group.number %> - <% } %> - - name: csfle-tests display_name: CSFLE Tests run_on: ubuntu2004-large @@ -340,21 +332,6 @@ tasks: debug: 'compass-e2e-tests*,electron*,hadron*,mongo*' <% } %> -<% for(const group of E2E_TEST_GROUPS) { %> - - name: e2e-multiple-connections-<%= group.number %> - tags: ['required-for-publish', 'run-on-pr'] - commands: - - func: prepare - - func: install - - func: bootstrap - - func: test-multiple-connections - vars: - e2e_test_groups: <%= E2E_TEST_GROUPS.length %> - e2e_test_group: <%= group.number %> - debug: 'compass-e2e-tests*,electron*,hadron*,mongo*' - mongodb_version: latest-enterprise -<% } %> - - name: generate-vulnerability-report tags: ['required-for-publish', 'run-on-pr'] commands: diff --git a/.evergreen/buildvariants-and-tasks.yml b/.evergreen/buildvariants-and-tasks.yml index 87aa8c6166e..c14cd01bda6 100644 --- a/.evergreen/buildvariants-and-tasks.yml +++ b/.evergreen/buildvariants-and-tasks.yml @@ -220,13 +220,6 @@ buildvariants: - name: e2e-coverage-1 - name: e2e-coverage-2 - name: e2e-coverage-3 - - name: e2e-multiple-connections - display_name: E2E Multiple Connections - run_on: ubuntu2004-large - tasks: - - name: e2e-multiple-connections-1 - - name: e2e-multiple-connections-2 - - name: e2e-multiple-connections-3 - name: csfle-tests display_name: CSFLE Tests run_on: ubuntu2004-large @@ -338,48 +331,6 @@ tasks: e2e_test_groups: 3 e2e_test_group: 3 debug: compass-e2e-tests*,electron*,hadron*,mongo* - - name: e2e-multiple-connections-1 - tags: - - required-for-publish - - run-on-pr - commands: - - func: prepare - - func: install - - func: bootstrap - - func: test-multiple-connections - vars: - e2e_test_groups: 3 - e2e_test_group: 1 - debug: compass-e2e-tests*,electron*,hadron*,mongo* - mongodb_version: latest-enterprise - - name: e2e-multiple-connections-2 - tags: - - required-for-publish - - run-on-pr - commands: - - func: prepare - - func: install - - func: bootstrap - - func: test-multiple-connections - vars: - e2e_test_groups: 3 - e2e_test_group: 2 - debug: compass-e2e-tests*,electron*,hadron*,mongo* - mongodb_version: latest-enterprise - - name: e2e-multiple-connections-3 - tags: - - required-for-publish - - run-on-pr - commands: - - func: prepare - - func: install - - func: bootstrap - - func: test-multiple-connections - vars: - e2e_test_groups: 3 - e2e_test_group: 3 - debug: compass-e2e-tests*,electron*,hadron*,mongo* - mongodb_version: latest-enterprise - name: generate-vulnerability-report tags: - required-for-publish diff --git a/.evergreen/compass_package.sh b/.evergreen/compass_package.sh index 078d9946cda..f8ca1996b38 100755 --- a/.evergreen/compass_package.sh +++ b/.evergreen/compass_package.sh @@ -8,7 +8,7 @@ if [[ "$OSTYPE" == "cygwin" ]]; then fi echo "Creating signed release build..." -npm run package-compass-nocompile $COMPASS_DISTRIBUTION +npm run package-compass-nocompile npm run generate-first-party-deps-json ls -la packages/compass/dist diff --git a/.evergreen/functions.yml b/.evergreen/functions.yml index 6971bbc7afd..0e8412d7f4a 100644 --- a/.evergreen/functions.yml +++ b/.evergreen/functions.yml @@ -301,12 +301,13 @@ functions: shell: bash env: <<: *compass-env + HADRON_DISTRIBUTION: ${compass_distribution} script: | set -e # Load environment variables eval $(.evergreen/print-compass-env.sh) # Generates and expansion file with build target metadata in packages/compass/expansions.yml - npm run --workspace mongodb-compass build-info -- ${target_platform} ${target_arch} --format=yaml --flatten ${compass_distribution} --out expansions.raw.yml + npm run --workspace mongodb-compass build-info -- ${target_platform} ${target_arch} --format=yaml --flatten --out expansions.raw.yml # the 'author' key conflicts with evergreen's own expansion grep -v '^author:' < packages/compass/expansions.raw.yml > packages/compass/expansions.yml - command: expansions.update @@ -405,7 +406,7 @@ functions: <<: *compass-env DEBUG: ${debug} npm_config_loglevel: ${npm_loglevel} - COMPASS_DISTRIBUTION: ${compass_distribution} + HADRON_DISTRIBUTION: ${compass_distribution} script: | set -e @@ -443,7 +444,7 @@ functions: <<: *compass-env DEBUG: ${debug} npm_config_loglevel: ${npm_loglevel} - COMPASS_DISTRIBUTION: ${compass_distribution} + HADRON_DISTRIBUTION: ${compass_distribution} SIGNING_SERVER_HOSTNAME: ${SIGNING_SERVER_HOSTNAME} SIGNING_SERVER_PRIVATE_KEY: ${SIGNING_SERVER_PRIVATE_KEY} SIGNING_SERVER_PRIVATE_KEY_CYGPATH: ${SIGNING_SERVER_PRIVATE_KEY_CYGPATH} @@ -586,6 +587,7 @@ functions: E2E_TEST_GROUPS: ${e2e_test_groups} E2E_TEST_GROUP: ${e2e_test_group} ATLAS_LOCAL_VERSION: latest + HADRON_DISTRIBUTION: compass script: | set -e # Load environment variables @@ -600,30 +602,6 @@ functions: tar czf coverage.tgz packages/compass-e2e-tests/coverage - test-multiple-connections: - - command: shell.exec - # Fail the task if it's idle for 10 mins - timeout_secs: 600 - params: - working_dir: src - shell: bash - env: - <<: *compass-env - DEBUG: ${debug|} - MONGODB_VERSION: ${mongodb_version|} - MONGODB_RUNNER_VERSION: ${mongodb_version|} - E2E_TEST_GROUPS: ${e2e_test_groups} - E2E_TEST_GROUP: ${e2e_test_group} - script: | - set -e - # Load environment variables - eval $(.evergreen/print-compass-env.sh) - - echo "Running E2E tests for multiple connections" - - npm run --unsafe-perm --workspace compass-e2e-tests test-ci -- -- --test-multiple-connections - - test-packaged-app: - command: shell.exec # Fail the task if it's idle for 10 mins @@ -946,6 +924,7 @@ functions: DEBUG: ${debug} SNYK_TOKEN: ${snyk_token} JIRA_API_TOKEN: ${jira_api_token} + HADRON_DISTRIBUTION: compass script: | set -e # Load environment variables @@ -977,28 +956,3 @@ functions: # Fails if the report failed and is not a patch, including release branches: exit $return_code fi - - generative-ai-accuracy-tests: - - command: shell.exec - # Fail the task if it's idle for 10 mins - timeout_secs: 600 - params: - working_dir: src - shell: bash - env: - <<: *compass-env - ATLAS_PUBLIC_KEY: ${atlas_public_key} - ATLAS_PRIVATE_KEY: ${atlas_private_key} - AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING: ${accuracy_results_mdb_connection_string} - script: | - set -e - # Load environment variables - eval $(.evergreen/print-compass-env.sh) - - set +e - npm run --workspace @mongodb-js/compass-generative-ai ai-accuracy-tests - return_code=$? - set -e - - # Fail when the accuracy tests fail: - exit $return_code diff --git a/.evergreen/generative-ai-accuracy-test-empty.yml b/.evergreen/generative-ai-accuracy-test-empty.yml deleted file mode 100644 index 8e6c25f9bf3..00000000000 --- a/.evergreen/generative-ai-accuracy-test-empty.yml +++ /dev/null @@ -1,9 +0,0 @@ -# This evergreen .yml is intentionally left empty. -# We want the compass-generative-ai-accuracy evergreen project to run -# daily on the latest commit. This file is here so that we have a -# .yml config to point the evergreen project to so that -# it can uses the most recent commits when the daily run happens. - -# The .yml that runs those tests is `generative-ai-accuracy-test.yml`. -# We don't want it to run on every commit as that would be too many -# requests to our ai model (expensive). diff --git a/.evergreen/generative-ai-accuracy-test.yml b/.evergreen/generative-ai-accuracy-test.yml deleted file mode 100644 index 14a9302260d..00000000000 --- a/.evergreen/generative-ai-accuracy-test.yml +++ /dev/null @@ -1,30 +0,0 @@ -# This evergreen .yml is only used in periodic builds. -# We don't want it to run on every commit as that would be -# too many requests to our ai model (expensive). - -stepback: false -exec_timeout_secs: 5400 -ignore: - - AUTHORS - - THIRD-PARTY-NOTICES.md -include: - # Referenced from project root. - - filename: .evergreen/functions.yml - -tasks: - - name: test-generative-ai-accuracy - tags: [] - commands: - - func: prepare - - func: install - - func: bootstrap - - func: generative-ai-accuracy-tests - vars: - debug: 'compass*,electron*,hadron*,mongo*' - -buildvariants: - - name: test-generative-ai-accuracy - display_name: Generative AI accuracy tests run against cloud-dev mms - run_on: ubuntu2004-large - tasks: - - name: test-generative-ai-accuracy diff --git a/.snyk b/.snyk index f72ecf87a19..61ab32b5bb8 100644 --- a/.snyk +++ b/.snyk @@ -7,7 +7,7 @@ ignore: reason: >- Not applicable as we do not use a valueFormatter or cellRenderer function - expires: 2024-08-17T18:27:24.346Z + expires: 2024-11-15T18:27:24.346Z created: 2024-01-18T18:27:24.353Z SNYK-JS-AXIOS-6032459: - '*': diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 078a1b7df3b..f2fba00eb3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,10 +56,10 @@ In addition to running lerna commands directly, there are a few convenient npm s To build compass you can run `package-compass` script: ```sh -npm run package-compass +HADRON_DISTRIBUTION='compass' npm run package-compass ``` -You can change the type of distribution you are building with `HADRON_DISTRIBUTION` environmental variable: +It is required to provide `HADRON_DISTRIBUTION` env variable explicitly. You can change the type of distribution you are building by setting a different `HADRON_DISTRIBUTION` value: ```sh HADRON_DISTRIBUTION='compass-readonly' npm run package-compass diff --git a/README.md b/README.md index 217bfe090da..8c041e00191 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ Is there anything else you’d like to see in Compass? Let us know by submitting - [**@mongodb-js/databases-collections-list**](packages/databases-collections-list): List view for the databases and collections - [**@mongodb-js/explain-plan-helper**](packages/explain-plan-helper): Explain plan utility methods for MongoDB Compass - [**@mongodb-js/my-queries-storage**](packages/my-queries-storage): Saved aggregations and queries storage -- [**@mongodb-js/ssh-tunnel**](packages/ssh-tunnel): Yet another ssh tunnel based on ssh2 - [**bson-transpilers**](packages/bson-transpilers): Source to source compilers using ANTLR - [**compass-e2e-tests**](packages/compass-e2e-tests): E2E test suite for Compass app that follows smoke tests / feature testing matrix - [**compass-preferences-model**](packages/compass-preferences-model): Compass preferences model diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index c0a8de913d1..2965d2e03f0 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -1,5 +1,5 @@ The following third-party software is used by and included in **Mongodb Compass**. -This document was automatically generated on Fri Aug 16 2024. +This document was automatically generated on Wed Aug 21 2024. ## List of dependencies @@ -183,7 +183,7 @@ This document was automatically generated on Fri Aug 16 2024. | **[base64-js](#cf278cb8d073b3bd22b60816c2ba78b69043aec6bcd673437b4c1db3375153d6)** | 1.5.1 | MIT | | **[basic-ftp](#2893c6a2ae0507b9073fc65146e8902587fef3bfeb9e94a67ea34cb09124b902)** | 5.0.5 | MIT | | **[bcrypt-pbkdf](#b6b5900f1e48a933591abc1c918fbcc9c890b3d071f607c59d704bc1c13b3937)** | 1.0.2 | BSD-3-Clause | -| **[big-integer](#d95bbdb85fef6a0472cf8cdb5702b5f12c61716bebdace731d7894cf83f1382a)** | 1.6.51 | Unlicense | +| **[big-integer](#eb3eaa39f6d9126fdf0b39f43641e13908ba5ab8b70af073669d23cc768335b5)** | 1.6.52 | Unlicense | | **[bindings](#acdb65ce90d2786593049f690752613250632fd5aeaa2960152abc4f0e8f3a44)** | 1.5.0 | MIT | | **[bl](#0e8c95ceb67a28a94b8caec6fa59d55974c80aab5dcf21bf1b17b0867f694c3c)** | 4.1.0 | MIT | | **[body-parser](#6b44aee8dd5ecc9ca689f12bdce5cd72765171cc2d1b935f50040be51871621a)** | 1.20.2 | MIT | @@ -251,7 +251,7 @@ This document was automatically generated on Fri Aug 16 2024. | **[ee-first](#e2746902c758ae8a6f91ffb9618cd53717f936cb33c6323e65b6b7b24f7ebefe)** | 1.1.1 | MIT | | **[electron-dl](#e97e034c7b93c63e7a433d75f6f1de3e0668764225ebbd61dbde8d1b55d6f3b7)** | 3.5.0 | MIT | | **[electron-squirrel-startup](#09fb8168e8fda2e174f8d1a1c392ffd8f762c5637c788edd00d1e2486d060349)** | 1.0.1 | Apache-2.0 | -| **[electron](#2d57f0e02aec4ac92532b06902fbe13af26292bde2d718c49f75a8fe02c85fdd)** | 29.4.5 | MIT | +| **[electron](#a6c4259fce8f4193f5fead998cb489be8bcf7a128cc2af3e2291846a083855ce)** | 30.4.0 | MIT | | **[encodeurl](#b89152db475e86531e570f87b45d8a51aa5e5d87d4cc3b960cee7b8febf1d26a)** | 1.0.2 | MIT | | **[end-of-stream](#fadc10994f5fa767d06fb25cfff35fb17a895daf3bc3477c782907668ed16563)** | 1.4.4 | MIT | | **[ensure-error](#3b1eba5276d89414cef21a1007e85c4f1d6749bf57b300e082ab23975a41dbc9)** | 3.0.1 | MIT | @@ -270,7 +270,7 @@ This document was automatically generated on Fri Aug 16 2024. | **[eventemitter3](#344ac4a1404cf0768bccce4529868ee2081bb2d49637269457647deab073e298)** | 4.0.7 | MIT | | **[events-mixin](#5db5de476dd54dc255312e4dcdb45fb765e1b7d0622d9b9ba86b65527de02a1c)** | 1.3.0 | MIT | | **[execa](#4172423d3420d919e31613f23914ef325af8a3bf9ed3c6110a4053369b1cfddd)** | 5.1.1 | MIT | -| **[execa](#5447206133d68ca6e364d599f6111e0a75400f94dce6556d9c03ee6992ac0b08)** | 7.1.1 | MIT | +| **[execa](#099ba5f976333854bfd5aa2237fd12d883c4477af76007a7963109833edef012)** | 7.2.0 | MIT | | **[expand-template](#46d3e73ca0d4a8c14e99252386f0a5c1a4fd8b2747331373d7b4da97105c15bb)** | 2.0.3 | (MIT OR WTFPL) | | **[express](#2f6758d3407b167482b6e5939a39adfae4627f3ca3939e7ebd8f944423a1e069)** | 4.19.2 | MIT | | **[ext-list](#84470edae99e3ac5a9fdf9da513cd9a1ea7e479ca5fca13b6abecbb4c522f97c)** | 2.2.2 | MIT | @@ -464,7 +464,7 @@ This document was automatically generated on Fri Aug 16 2024. | **[modify-filename](#7153be07939379ccf0072006c519fba2bdf5ab79ca8bb59bc5273f87a7bacbf6)** | 1.1.0 | MIT | | **[moment](#94975b5423311209f3beed9c2c6bb6157f622312a3f8563d507b52e804bf6285)** | 2.29.4 | MIT | | **[mongodb-build-info](#f0a98c22ae0766702726f79e058ac6dc4e4bead8557b67b816f40bd13fb54170)** | 1.7.2 | Apache-2.0 | -| **[mongodb-client-encryption](#6e0ac1e457fd6c68a4a91ede36be521cd2cf275fc1748e1f3b540e2ccd3b2791)** | 6.0.0 | Apache-2.0 | +| **[mongodb-client-encryption](#42ba122fc0db7791e33e6160aff7213f42c4a34a8e891e78cd07c606555c2bb9)** | 6.0.1 | Apache-2.0 | | **[mongodb-cloud-info](#a784f3b401cf51746f49964e044db933529b3e3791e557702715730f5a3f1e46)** | 2.1.2 | Apache-2.0 | | **[mongodb-connection-string-url](#2e1146256a89ebd24e3398881e03807fe363d58444e6b7952ea50bd6108707bc)** | 3.0.1 | Apache-2.0 | | **[mongodb-log-writer](#c4945018f8490fc8e56e1414e262fcf1b802800e05cd15f2bd6b7a9d0b94af85)** | 1.4.2 | Apache-2.0 | @@ -486,7 +486,7 @@ This document was automatically generated on Fri Aug 16 2024. | **[node-fetch](#23d7d5a419e9a25e6384dee4aa24f7162544418f0cdc2d92e94e2cf924507b8c)** | 2.7.0 | MIT | | **[node-fetch](#22edb8ba3fe3457e8c1a02e497e6a8cb54e89775224f0c7680ea43772b5c1638)** | 3.3.2 | MIT | | **[npm-run-path](#b21248abecb88119fcf2885e5471cd9235f40d356cba189a348def4b8a6046bd)** | 4.0.1 | MIT | -| **[npm-run-path](#372e960fd5d56ee0c4a7a39def16250bd3e0f663ebda006c18afe5c9d2ef4bec)** | 5.1.0 | MIT | +| **[npm-run-path](#17c35569bae8ecfc9314730bb16dde145a190d68c6fe6a29c78873c9c5520ed1)** | 5.3.0 | MIT | | **[numeral](#b3c90be596160f7dccbd1ff771ddbffb9a1b19d0bb9456553d8822903386573e)** | 1.5.6 | MIT | | **[numeral](#d274a180ad09fc1ae9325f01bf5dc1296caf553888d952fab7ebf524dfdc56a1)** | 2.0.6 | MIT | | **[object-assign](#598e372231bb5bef26b7d61105282eb20e14ade430143052d064d2d406769b95)** | 4.1.1 | MIT | @@ -17175,9 +17175,9 @@ License files: ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - + -### [big-integer](https://www.npmjs.com/package/big-integer) (version 1.6.51) +### [big-integer](https://www.npmjs.com/package/big-integer) (version 1.6.52) License tags: Unlicense @@ -19655,9 +19655,9 @@ License files: See the License for the specific language governing permissions and limitations under the License. - + -### [electron](https://www.npmjs.com/package/electron) (version 29.4.5) +### [electron](https://www.npmjs.com/package/electron) (version 30.4.0) License tags: MIT @@ -20202,9 +20202,9 @@ License files: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + -### [execa](https://www.npmjs.com/package/execa) (version 7.1.1) +### [execa](https://www.npmjs.com/package/execa) (version 7.2.0) License tags: MIT @@ -27449,9 +27449,9 @@ License files: See the License for the specific language governing permissions and limitations under the License. - + -### [mongodb-client-encryption](https://www.npmjs.com/package/mongodb-client-encryption) (version 6.0.0) +### [mongodb-client-encryption](https://www.npmjs.com/package/mongodb-client-encryption) (version 6.0.1) License tags: Apache-2.0 @@ -29527,9 +29527,9 @@ License files: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + -### [npm-run-path](https://www.npmjs.com/package/npm-run-path) (version 5.1.0) +### [npm-run-path](https://www.npmjs.com/package/npm-run-path) (version 5.3.0) License tags: MIT diff --git a/configs/eslint-config-compass/package.json b/configs/eslint-config-compass/package.json index 469a7377fba..0d6459551dd 100644 --- a/configs/eslint-config-compass/package.json +++ b/configs/eslint-config-compass/package.json @@ -1,6 +1,6 @@ { "name": "@mongodb-js/eslint-config-compass", - "version": "1.1.4", + "version": "1.1.5", "description": "Shared Compass eslint configuration", "license": "SSPL", "main": "index.js", @@ -16,7 +16,7 @@ "@babel/core": "^7.21.4", "@babel/eslint-parser": "^7.14.3", "@mongodb-js/eslint-config-devtools": "^0.9.9", - "@mongodb-js/eslint-plugin-compass": "^1.0.18", + "@mongodb-js/eslint-plugin-compass": "^1.0.19", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "eslint-config-prettier": "^8.3.0", diff --git a/configs/eslint-plugin-compass/package.json b/configs/eslint-plugin-compass/package.json index f03c448bf9f..e9eab381563 100644 --- a/configs/eslint-plugin-compass/package.json +++ b/configs/eslint-plugin-compass/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.0.18", + "version": "1.0.19", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -37,7 +37,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", diff --git a/configs/mocha-config-compass/package.json b/configs/mocha-config-compass/package.json index 6c0e5179786..c82eabd527e 100644 --- a/configs/mocha-config-compass/package.json +++ b/configs/mocha-config-compass/package.json @@ -1,6 +1,6 @@ { "name": "@mongodb-js/mocha-config-compass", - "version": "1.3.10", + "version": "1.4.0", "description": "Shared mocha mocha configuration for Compass packages", "license": "SSPL", "main": "index.js", diff --git a/configs/webpack-config-compass/package.json b/configs/webpack-config-compass/package.json index 7564f87e464..684fcaebc5d 100644 --- a/configs/webpack-config-compass/package.json +++ b/configs/webpack-config-compass/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.3.15", + "version": "1.4.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -45,7 +45,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/cli-progress": "^3.9.2", @@ -74,7 +74,7 @@ "cli-progress": "^3.9.1", "core-js": "^3.17.3", "css-loader": "^4.3.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "html-webpack-plugin": "^5.3.2", "less": "^3.13.1", "less-loader": "^10.0.1", diff --git a/configs/webpack-config-compass/src/index.ts b/configs/webpack-config-compass/src/index.ts index 0050c5118ca..0992dcc88f2 100644 --- a/configs/webpack-config-compass/src/index.ts +++ b/configs/webpack-config-compass/src/index.ts @@ -1,7 +1,8 @@ -import type { - ResolveOptions, - WebpackPluginInstance, - Configuration, +import { + type ResolveOptions, + type WebpackPluginInstance, + type Configuration, + ProvidePlugin, } from 'webpack'; import { merge } from 'webpack-merge'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; @@ -110,6 +111,11 @@ const sharedResolveOptions = ( }; }; +const providePlugin = new ProvidePlugin({ + URL: ['whatwg-url', 'URL'], + URLSearchParams: ['whatwg-url', 'URLSearchParams'], +}); + export function createElectronMainConfig( args: Partial ): WebpackConfig { @@ -212,6 +218,7 @@ export function createElectronRendererConfig( plugins: [ ...entriesToHtml(entries), new WebpackPluginMulticompilerProgress(), + providePlugin, ], node: false as const, externals: toCommonJsExternal(sharedExternals), @@ -339,8 +346,9 @@ export function createWebConfig(args: Partial): WebpackConfig { ...sharedResolveOptions(opts.target), }, ignoreWarnings: sharedIgnoreWarnings, - plugins: - isServe(opts) && opts.hot + plugins: [ + providePlugin, + ...(isServe(opts) && opts.hot ? [ // Plugin types are not matching Webpack 5, but they work new ReactRefreshWebpackPlugin() as unknown as WebpackPluginInstance, @@ -355,7 +363,8 @@ export function createWebConfig(args: Partial): WebpackConfig { new DuplicatePackageCheckerPlugin(), ] - : [], + : []), + ], }; } diff --git a/package-lock.json b/package-lock.json index d69a6ee4828..4765cc9ac99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,13 +31,13 @@ }, "configs/eslint-config-compass": { "name": "@mongodb-js/eslint-config-compass", - "version": "1.1.4", + "version": "1.1.5", "license": "SSPL", "dependencies": { "@babel/core": "^7.21.4", "@babel/eslint-parser": "^7.14.3", "@mongodb-js/eslint-config-devtools": "^0.9.9", - "@mongodb-js/eslint-plugin-compass": "^1.0.18", + "@mongodb-js/eslint-plugin-compass": "^1.0.19", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "eslint-config-prettier": "^8.3.0", @@ -104,10 +104,10 @@ }, "configs/eslint-plugin-compass": { "name": "@mongodb-js/eslint-plugin-compass", - "version": "1.0.18", + "version": "1.0.19", "license": "SSPL", "devDependencies": { - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -118,7 +118,7 @@ }, "configs/mocha-config-compass": { "name": "@mongodb-js/mocha-config-compass", - "version": "1.3.10", + "version": "1.4.0", "license": "SSPL", "dependencies": { "@electron/remote": "^2.1.2", @@ -170,7 +170,7 @@ }, "configs/webpack-config-compass": { "name": "@mongodb-js/webpack-config-compass", - "version": "1.3.15", + "version": "1.4.0", "license": "SSPL", "dependencies": { "@babel/core": "^7.21.4", @@ -189,7 +189,7 @@ "cli-progress": "^3.9.1", "core-js": "^3.17.3", "css-loader": "^4.3.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "html-webpack-plugin": "^5.3.2", "less": "^3.13.1", "less-loader": "^10.0.1", @@ -211,7 +211,7 @@ "webpack-compass": "bin/webpack.js" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/cli-progress": "^3.9.2", @@ -7774,141 +7774,6 @@ "mongodb-log-writer": "^1.4.2" } }, - "node_modules/@mongodb-js/devtools-docker-test-envs": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.3.2.tgz", - "integrity": "sha512-CwRzxmQ5+t37CuXdCca9CaHTvnKqBOOViU6LkjvLWLB2Qq1emrltbZBtbY4IRoWclRbZemA+vXhHNByXZNEgIw==", - "dev": true, - "dependencies": { - "eslint-plugin-mocha": "^9.0.0", - "execa": "^5.1.1", - "hostile": "^1.3.3", - "mongodb-connection-string-url": "^2.0.0", - "uuid": "^8.3.2", - "wait-on": "^6.0.0" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/eslint-plugin-mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", - "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", - "dev": true, - "dependencies": { - "eslint-utils": "^3.0.0", - "ramda": "^0.27.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dev": true, - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/rxjs": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", - "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", - "dev": true, - "dependencies": { - "tslib": "~2.1.0" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/wait-on": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", - "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", - "dev": true, - "dependencies": { - "axios": "^0.21.1", - "joi": "^17.4.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^7.1.0" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@mongodb-js/devtools-github-repo": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-github-repo/-/devtools-github-repo-1.4.1.tgz", @@ -8166,13 +8031,14 @@ } }, "node_modules/@mongodb-js/mongodb-downloader": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.2.tgz", - "integrity": "sha512-bhMfxzaBy31RveAu7qqON3nVXRHYmxJXyC3lZI+mK+4DhagKZdGHJpMkLmHQRt+wAxMR6ldI9YlcWjHSqceIsQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.5.tgz", + "integrity": "sha512-0tNik3E/eQxx8EkJpsa9+IIK5LtMle3N7M/1wtKQKMixJjZ1vsgAjJGguYbLfCyJqfnrw9KMyD1cwgaS37tvRA==", + "license": "Apache-2.0", "dependencies": { "debug": "^4.3.4", "decompress": "^4.2.1", - "mongodb-download-url": "^1.3.0", + "mongodb-download-url": "^1.5.1", "node-fetch": "^2.6.11", "tar": "^6.1.15" } @@ -8181,6 +8047,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8196,6 +8063,25 @@ } } }, + "node_modules/@mongodb-js/mongodb-downloader/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@mongodb-js/mongodb-downloader/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@mongodb-js/mongodb-downloader/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@mongodb-js/monorepo-tools": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@mongodb-js/monorepo-tools/-/monorepo-tools-1.1.2.tgz", @@ -8429,9 +8315,10 @@ } }, "node_modules/@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", + "license": "Apache-2.0", "dependencies": { "express": "^4.18.2", "open": "^9.1.0", @@ -8445,6 +8332,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -8456,6 +8344,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "license": "MIT", "dependencies": { "default-browser": "^4.0.0", "define-lazy-prop": "^3.0.0", @@ -8765,10 +8654,6 @@ "node": ">=0.10.0" } }, - "node_modules/@mongodb-js/ssh-tunnel": { - "resolved": "packages/ssh-tunnel", - "link": true - }, "node_modules/@mongodb-js/tsconfig-compass": { "resolved": "configs/tsconfig-compass", "link": true @@ -13553,12 +13438,6 @@ "@types/ms": "*" } }, - "node_modules/@types/decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@types/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-zfremx+C6eM4p0Y+VRbjyYnieRWkezqnM+QUX97dulAl0+9RYptIiIOaHgUdHvXOvq3ykZC0yVA8YMXMj6v6ag==", - "dev": true - }, "node_modules/@types/enzyme": { "version": "3.10.14", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.14.tgz", @@ -13839,30 +13718,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/node-fetch/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -16766,12 +16621,12 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.7" } }, "node_modules/axobject-query": { @@ -17167,12 +17022,6 @@ "streamx": "^2.18.0" } }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", - "dev": true - }, "node_modules/base32-encode": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", @@ -17228,9 +17077,10 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", "engines": { "node": ">=0.6" } @@ -17408,6 +17258,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "license": "MIT", "dependencies": { "big-integer": "^1.6.44" }, @@ -17687,6 +17538,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "license": "MIT", "dependencies": { "run-applescript": "^5.0.0" }, @@ -17969,15 +17821,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -18172,18 +18015,6 @@ "node": ">=6" } }, - "node_modules/cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dependencies": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "engines": { - "node": ">=0.2.5" - } - }, "node_modules/cli-color": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.3.2.tgz", @@ -19231,15 +19062,6 @@ "resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz", "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=" }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -19660,14 +19482,6 @@ "node": ">=12" } }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, "node_modules/data-urls/node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -19804,19 +19618,6 @@ "node": ">=0.10" } }, - "node_modules/decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-h0TZ8t6Dp49duwyDHo3iw67mnh9/UpFiSSiOb5gDK1sqoXzrfX/SQxIUQd2R2QEiSnqib0KF2fnKnGfAhAs6lg==", - "dev": true, - "dependencies": { - "esprima": "4.0.1" - }, - "engines": { - "node": ">=6.4", - "npm": ">=2.15" - } - }, "node_modules/decompress": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", @@ -20084,6 +19885,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "license": "MIT", "dependencies": { "bundle-name": "^3.0.0", "default-browser-id": "^3.0.0", @@ -20101,6 +19903,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "license": "MIT", "dependencies": { "bplist-parser": "^0.2.0", "untildify": "^4.0.0" @@ -20113,9 +19916,10 @@ } }, "node_modules/default-browser/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", @@ -20138,6 +19942,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "license": "Apache-2.0", "engines": { "node": ">=14.18.0" } @@ -20146,6 +19951,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -20157,6 +19963,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -20165,9 +19972,10 @@ } }, "node_modules/default-browser/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -20182,6 +19990,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -20196,6 +20005,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -20207,6 +20017,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -20594,18 +20405,6 @@ "node": "*" } }, - "node_modules/digest-fetch": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-2.0.3.tgz", - "integrity": "sha512-HuTjHQE+wplAR+H8/YGwQjIGR1RQUCEsQcRyp3dZfuuxpSQH4OTm4BkHxyXuzxwmxUrNVzIPf9XkXi8QMJDNwQ==", - "dev": true, - "dependencies": { - "base-64": "^0.1.0", - "js-sha256": "^0.9.0", - "js-sha512": "^0.8.0", - "md5": "^2.3.0" - } - }, "node_modules/dir-compare": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", @@ -20744,14 +20543,6 @@ "node": ">=12" } }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, "node_modules/domhandler": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", @@ -21292,9 +21083,9 @@ } }, "node_modules/electron": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.5.tgz", - "integrity": "sha512-DlEuzGbWBYl1Qr0qUYgNZdoixJg4YGHy2HC6fkRjSXSlb01UrQ5ORi8hNLzelzyYx8rNQyyE3zDUuk9EnZwYuA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.4.0.tgz", + "integrity": "sha512-ric3KLPQ9anXYjtTDkj5NbEcXZqRUwqxrxTviIjLdMdHqd5O+hkSHEzXgbSJUOt+7uw+zZuybn9+IM9y7iEpqg==", "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", @@ -21734,6 +21525,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", @@ -21754,6 +21546,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -21765,6 +21558,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", "engines": { "node": ">=10.6.0" } @@ -21773,6 +21567,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -21787,6 +21582,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -21800,6 +21596,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -21824,6 +21621,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -21832,6 +21630,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -21843,6 +21642,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -21851,6 +21651,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", "engines": { "node": ">= 4.0.0" } @@ -23995,14 +23796,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -27417,12 +27210,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -27555,6 +27342,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", "dependencies": { "is-docker": "^3.0.0" }, @@ -27572,6 +27360,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -28250,18 +28039,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", - "dev": true - }, - "node_modules/js-sha512": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", - "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", - "dev": true - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -28471,14 +28248,6 @@ "node": ">= 4.0.0" } }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, "node_modules/jsdom/node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -31744,17 +31513,6 @@ "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==", "dev": true }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -32517,6 +32275,7 @@ "integrity": "sha512-GtqkqlSq19acX006/U1odA3l+gwhvABeoTUlvvgtvSs6qcN3qSHPnur3Z5N4oKOv6fZ7EtT8rIsWP2riI0+Eyg==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^4.3.0", @@ -32575,6 +32334,28 @@ } } }, + "node_modules/mongodb-cloud-info/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/mongodb-cloud-info/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/mongodb-cloud-info/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/mongodb-collection-model": { "resolved": "packages/collection-model", "link": true @@ -32600,37 +32381,6 @@ "@types/webidl-conversions": "*" } }, - "node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/mongodb-data-service": { "resolved": "packages/data-service", "link": true @@ -32640,9 +32390,10 @@ "link": true }, "node_modules/mongodb-download-url": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.3.0.tgz", - "integrity": "sha512-N7mRi3/LIAHCeTa+JtJVrVno4BNHVYF+6/WUamVFsbvCxtljDmQA1n9FSQxV4dfdiknR9zaoFcXAmd1gtg3Elg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.5.1.tgz", + "integrity": "sha512-AJH2lqb7mBo7tT7RyFWK3P/ZMh7RC1qWJgOaAVrBdKeuPuCWGCESrti+ZMt6FA6mJ4eU58Lm7iG2rTkl94pBdQ==", + "license": "Apache-2.0", "dependencies": { "debug": "^4.1.1", "minimist": "^1.2.3", @@ -33001,79 +32752,6 @@ "lodash": "^4.17.21" } }, - "node_modules/mongodb-runner": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.2.tgz", - "integrity": "sha512-6XF3iGXswbJy8TC4VgYPVxnrMiUTJ7iaehE+Hiox2sZL2y3b6aNKkrD3Rt2w6nO0JKnwlR/mukyXbMlz2Zmuvw==", - "dependencies": { - "@mongodb-js/mongodb-downloader": "^0.3.2", - "@mongodb-js/saslprep": "^1.1.7", - "debug": "^4.3.4", - "mongodb": "^6.3.0", - "mongodb-connection-string-url": "^3.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "mongodb-runner": "bin/runner.js" - } - }, - "node_modules/mongodb-runner/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/mongodb-runner/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongodb-runner/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mongodb-runner/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongodb-runner/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, "node_modules/mongodb-schema": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/mongodb-schema/-/mongodb-schema-12.2.0.tgz", @@ -33386,6 +33064,25 @@ } } }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -37713,6 +37410,28 @@ } } }, + "node_modules/puppeteer-core/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/puppeteer-core/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/puppeteer-core/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/puppeteer/node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -38979,40 +38698,6 @@ "resolve-mongodb-srv": "bin/resolve-mongodb-srv.js" } }, - "node_modules/resolve-mongodb-srv/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "devOptional": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-mongodb-srv/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "devOptional": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-mongodb-srv/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "devOptional": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/responselike": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", @@ -39139,6 +38824,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "license": "MIT", "dependencies": { "execa": "^5.0.0" }, @@ -40297,44 +39983,6 @@ "node": ">= 14" } }, - "node_modules/socksv5": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", - "integrity": "sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=", - "bundleDependencies": [ - "ipv6" - ], - "dependencies": { - "ipv6": "*" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/socksv5/node_modules/ipv6": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cli": "0.4.x", - "cliff": "0.1.x", - "sprintf": "0.1.x" - }, - "bin": { - "ipv6": "bin/ipv6.js", - "ipv6grep": "bin/ipv6grep.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/socksv5/node_modules/ipv6/node_modules/sprintf": { - "version": "0.1.3", - "inBundle": true, - "engines": { - "node": ">=0.2.4" - } - }, "node_modules/sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -41598,6 +41246,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -41722,9 +41371,15 @@ } }, "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } }, "node_modules/traverse": { "version": "0.6.6", @@ -42503,6 +42158,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", "engines": { "node": ">=8" } @@ -42808,6 +42464,40 @@ "node": ">=12" } }, + "node_modules/wait-on": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz", + "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==", + "dev": true, + "dependencies": { + "axios": "^0.25.0", + "joi": "^17.6.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.5.4" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/wait-on/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/wait-on/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/wait-port": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", @@ -43352,9 +43042,12 @@ } }, "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } }, "node_modules/webpack": { "version": "5.86.0", @@ -43606,12 +43299,15 @@ } }, "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" } }, "node_modules/which": { @@ -44310,31 +44006,30 @@ }, "packages/atlas-service": { "name": "@mongodb-js/atlas-service", - "version": "0.26.0", + "version": "0.27.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/oidc-plugin": "^1.0.0", - "compass-preferences-model": "^2.26.0", - "electron": "^29.4.5", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/devtools-connect": "^3.2.6", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/oidc-plugin": "^1.1.1", + "compass-preferences-model": "^2.27.0", + "electron": "^30.4.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "system-ca": "^2.0.0" + "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -44351,6 +44046,71 @@ "typescript": "^5.0.4" } }, + "packages/atlas-service/node_modules/@mongodb-js/devtools-connect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.6.tgz", + "integrity": "sha512-E5jvGDHZ13fnDkuIytnINIS2/2BR0aiC0rfXLKeOO6ongJfL8F5ACEz5dbCR+e6eJ4JCeh1Tb49CfjZK9iGhWQ==", + "dependencies": { + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/oidc-http-server-pages": "1.1.2", + "lodash.merge": "^4.6.2", + "mongodb-connection-string-url": "^3.0.0", + "socks": "^2.7.3" + }, + "optionalDependencies": { + "kerberos": "^2.1.0", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", + "os-dns-native": "^1.2.0", + "resolve-mongodb-srv": "^1.1.1" + }, + "peerDependencies": { + "@mongodb-js/oidc-plugin": "^1.1.0", + "mongodb": "^6.8.0", + "mongodb-log-writer": "^1.4.2" + } + }, + "packages/atlas-service/node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "dependencies": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "packages/atlas-service/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "packages/atlas-service/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "packages/atlas-service/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -44360,23 +44120,29 @@ "node": ">=0.3.1" } }, + "packages/atlas-service/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, "packages/atlas-service/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "packages/atlas-service/node_modules/sinon": { @@ -44398,7 +44164,7 @@ } }, "packages/bson-transpilers": { - "version": "3.0.6", + "version": "3.0.7", "license": "SSPL", "dependencies": { "antlr4": "4.7.2", @@ -44406,7 +44172,7 @@ "js-yaml": "^3.13.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "chai": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -44435,16 +44201,16 @@ }, "packages/collection-model": { "name": "mongodb-collection-model", - "version": "5.22.3", + "version": "5.23.0", "license": "SSPL", "dependencies": { "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", @@ -44463,75 +44229,74 @@ "clipboard": "^2.0.6", "kerberos": "^2.1.1", "keytar": "^7.9.0", - "mongodb-client-encryption": "6.0.0", + "mongodb-client-encryption": "~6.0.1", "os-dns-native": "^1.2.1", "system-ca": "^2.0.0" }, "devDependencies": { "@electron/rebuild": "^3.6.0", "@electron/remote": "^2.1.2", - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-aggregations": "^9.40.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connection-import-export": "^0.34.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-databases-collections": "^1.37.0", - "@mongodb-js/compass-explain-plan": "^6.38.0", - "@mongodb-js/compass-export-to-language": "^9.14.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-find-in-page": "^4.30.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-import-export": "^7.37.0", - "@mongodb-js/compass-indexes": "^5.37.0", - "@mongodb-js/compass-intercom": "^0.10.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-saved-aggregations-queries": "^1.38.0", - "@mongodb-js/compass-schema": "^6.39.0", - "@mongodb-js/compass-schema-validation": "^6.38.0", - "@mongodb-js/compass-serverstats": "^16.37.0", - "@mongodb-js/compass-settings": "^0.38.0", - "@mongodb-js/compass-shell": "^3.37.0", - "@mongodb-js/compass-sidebar": "^5.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-welcome": "^0.36.0", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/devtools-proxy-support": "^0.3.5", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-aggregations": "^9.41.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connection-import-export": "^0.35.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-databases-collections": "^1.38.0", + "@mongodb-js/compass-explain-plan": "^6.39.0", + "@mongodb-js/compass-export-to-language": "^9.15.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-find-in-page": "^4.30.1", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-import-export": "^7.38.0", + "@mongodb-js/compass-indexes": "^5.38.0", + "@mongodb-js/compass-intercom": "^0.11.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-saved-aggregations-queries": "^1.39.0", + "@mongodb-js/compass-schema": "^6.40.0", + "@mongodb-js/compass-schema-validation": "^6.39.0", + "@mongodb-js/compass-serverstats": "^16.38.0", + "@mongodb-js/compass-settings": "^0.39.0", + "@mongodb-js/compass-shell": "^3.38.0", + "@mongodb-js/compass-sidebar": "^5.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-welcome": "^0.37.0", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/get-os-info": "^0.3.24", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/mongodb-downloader": "^0.3.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/sbom-tools": "^0.7.0", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@mongodb-js/webpack-config-compass": "^1.3.15", + "@mongodb-js/webpack-config-compass": "^1.4.0", "@segment/analytics-node": "^1.1.4", - "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "ampersand-view": "^9.0.0", "chai": "^4.3.4", "chalk": "^4.1.2", "clean-stack": "^2.0.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-devtools-installer": "^3.2.0", "electron-dl": "^3.5.0", "electron-mocha": "^12.2.0", "electron-squirrel-startup": "^1.0.1", "ensure-error": "^3.0.1", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-build": "^25.5.7", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-build": "^25.5.8", + "hadron-ipc": "^3.2.21", "local-links": "^1.4.0", "make-fetch-happen": "^8.0.14", "marky": "^1.2.1", @@ -44539,11 +44304,10 @@ "mongodb-build-info": "^1.7.2", "mongodb-cloud-info": "^2.1.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-data-service": "^22.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-dom": "^17.0.2", "resolve-mongodb-srv": "^1.1.5", @@ -44561,7 +44325,7 @@ }, "packages/compass-aggregations": { "name": "@mongodb-js/compass-aggregations", - "version": "9.40.0", + "version": "9.41.0", "license": "SSPL", "dependencies": { "@babel/generator": "^7.19.5", @@ -44570,34 +44334,34 @@ "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/explain-plan-helper": "^1.1.15", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/explain-plan-helper": "^1.2.0", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/shell-bson-parser": "^1.1.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", @@ -44610,8 +44374,8 @@ "semver": "^7.6.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -44673,27 +44437,25 @@ }, "packages/compass-app-stores": { "name": "@mongodb-js/compass-app-stores", - "version": "7.24.0", + "version": "7.25.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/connection-info": "^0.5.3", - "hadron-app-registry": "^9.2.2", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/connection-info": "^0.6.0", + "hadron-app-registry": "^9.2.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/dom": "^8.20.1", - "@testing-library/react": "^12.1.5", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -44738,20 +44500,20 @@ }, "packages/compass-collection": { "name": "@mongodb-js/compass-collection", - "version": "4.37.0", + "version": "4.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", "@mongodb-js/mongodb-constants": "^0.10.2", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "mongodb-collection-model": "^5.22.3", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "mongodb-collection-model": "^5.23.0", "mongodb-ns": "^2.4.2", "numeral": "^2.0.6", "react": "^17.0.2", @@ -44760,8 +44522,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -44823,7 +44585,7 @@ }, "packages/compass-components": { "name": "@mongodb-js/compass-components", - "version": "1.29.0", + "version": "1.29.1", "license": "SSPL", "dependencies": { "@dnd-kit/core": "^6.0.7", @@ -44873,7 +44635,7 @@ "@react-aria/visually-hidden": "^3.3.1", "bson": "^6.7.0", "focus-trap-react": "^9.0.2", - "hadron-document": "^8.6.0", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", @@ -44887,8 +44649,8 @@ }, "devDependencies": { "@emotion/css": "^11.11.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/dom": "^8.20.1", @@ -44938,19 +44700,19 @@ }, "packages/compass-connection-import-export": { "name": "@mongodb-js/compass-connection-import-export", - "version": "0.34.0", + "version": "0.35.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/connection-storage": "^0.17.0", - "compass-preferences-model": "^2.26.0", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/connection-storage": "^0.18.0", + "compass-preferences-model": "^2.27.0", + "hadron-ipc": "^3.2.21", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45003,34 +44765,36 @@ }, "packages/compass-connections": { "name": "@mongodb-js/compass-connections", - "version": "1.38.0", + "version": "1.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-storage": "^0.18.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", - "react": "^17.0.2" + "mongodb-data-service": "^22.23.0", + "react": "^17.0.2", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/dom": "^8.20.1", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", @@ -45052,23 +44816,23 @@ }, "packages/compass-connections-navigation": { "name": "@mongodb-js/compass-connections-navigation", - "version": "1.37.0", + "version": "1.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "compass-preferences-model": "^2.27.0", "mongodb-build-info": "^1.7.2", "react": "^17.0.2", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45148,33 +44912,33 @@ }, "packages/compass-crud": { "name": "@mongodb-js/compass-crud", - "version": "13.38.0", + "version": "13.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "@mongodb-js/my-queries-storage": "^0.15.0", - "@mongodb-js/reflux-state-mixin": "^1.0.4", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "@mongodb-js/my-queries-storage": "^0.15.1", + "@mongodb-js/reflux-state-mixin": "^1.0.5", "@mongodb-js/shell-bson-parser": "^1.1.0", "ag-grid-community": "^20.2.0", "ag-grid-react": "^20.2.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "prop-types": "^15.7.2", @@ -45183,9 +44947,9 @@ "semver": "^7.6.2" }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45194,12 +44958,12 @@ "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "nyc": "^15.1.0", "react-dom": "^17.0.2", "sinon": "^8.1.1", @@ -45243,12 +45007,12 @@ } }, "packages/compass-e2e-tests": { - "version": "1.24.0", + "version": "1.25.0", "devDependencies": { "@electron/rebuild": "^3.6.0", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/oidc-mock-provider": "^0.9.3", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -45260,21 +45024,21 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "clipboardy": "^2.3.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", - "hadron-build": "^25.5.7", + "hadron-build": "^25.5.8", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", "mongodb-log-writer": "^1.4.2", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.3", "node-fetch": "^2.7.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -45288,6 +45052,26 @@ "xvfb-maybe": "^0.2.1" } }, + "packages/compass-e2e-tests/node_modules/@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "packages/compass-e2e-tests/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "packages/compass-e2e-tests/node_modules/clipboardy": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", @@ -45302,6 +45086,21 @@ "node": ">=8" } }, + "packages/compass-e2e-tests/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "packages/compass-e2e-tests/node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -45452,6 +45251,24 @@ "node": ">=16 || 14 >=14.17" } }, + "packages/compass-e2e-tests/node_modules/mongodb-runner": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.5.tgz", + "integrity": "sha512-joZJL5YKuwP7MNegz0CKt7rAPgAeUcWhOfJZPgifnKD+3tl4oMbRwP+HjcQjBieT1dr9lh+kI1MQuGInRtQzMw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/saslprep": "^1.1.8", + "debug": "^4.3.4", + "mongodb": "^6.8.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "mongodb-runner": "bin/runner.js" + } + }, "packages/compass-e2e-tests/node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -45523,9 +45340,73 @@ "node": ">=0.10.0" } }, + "packages/compass-e2e-tests/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "packages/compass-e2e-tests/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "packages/compass-e2e-tests/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "packages/compass-e2e-tests/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "packages/compass-e2e-tests/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "packages/compass-e2e-tests/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "packages/compass-editor": { "name": "@mongodb-js/compass-editor", - "version": "0.29.0", + "version": "0.29.1", "license": "SSPL", "dependencies": { "@codemirror/autocomplete": "^6.17.0", @@ -45537,7 +45418,7 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.2.0", - "@mongodb-js/compass-components": "^1.29.0", + "@mongodb-js/compass-components": "^1.29.1", "@mongodb-js/mongodb-constants": "^0.10.0", "mongodb-query-parser": "^4.2.0", "polished": "^4.2.2", @@ -45545,8 +45426,8 @@ "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -45591,21 +45472,21 @@ }, "packages/compass-explain-plan": { "name": "@mongodb-js/compass-explain-plan", - "version": "6.38.0", + "version": "6.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", "d3-flextree": "^2.1.2", "d3-hierarchy": "^3.1.2", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", "react": "^17.0.2", @@ -45614,8 +45495,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45624,7 +45505,7 @@ "@types/d3-hierarchy": "^3.1.2", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -45670,28 +45551,28 @@ }, "packages/compass-export-to-language": { "name": "@mongodb-js/compass-export-to-language", - "version": "9.14.0", + "version": "9.15.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", "@mongodb-js/shell-bson-parser": "^1.1.0", - "bson-transpilers": "^3.0.6", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "bson-transpilers": "^3.0.7", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1" }, "devDependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45735,24 +45616,24 @@ }, "packages/compass-field-store": { "name": "@mongodb-js/compass-field-store", - "version": "9.13.0", + "version": "9.14.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-connections": "^1.38.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-schema": "^12.2.0", "react": "^17.0.2", "react-redux": "^8.1.3", - "redux": "^4.2.1" + "redux": "^4.2.1", + "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -45797,20 +45678,20 @@ }, "packages/compass-find-in-page": { "name": "@mongodb-js/compass-find-in-page", - "version": "4.30.0", + "version": "4.30.1", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -45823,7 +45704,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -45864,51 +45745,43 @@ }, "packages/compass-generative-ai": { "name": "@mongodb-js/compass-generative-ai", - "version": "0.20.0", + "version": "0.21.0", "license": "SSPL", "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-intercom": "^0.10.0", - "@mongodb-js/compass-logging": "^1.4.3", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-intercom": "^0.11.0", + "@mongodb-js/compass-logging": "^1.4.4", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "mongodb": "^6.8.0", "mongodb-schema": "^12.2.0", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", - "@types/decomment": "^0.9.5", "@types/mocha": "^9.0.0", - "@types/node-fetch": "^2.6.11", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "decomment": "^0.9.5", "depcheck": "^1.4.1", - "digest-fetch": "^2.0.3", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-runner": "^5.6.2", - "node-fetch": "^2.7.0", "nyc": "^15.1.0", "p-queue": "^7.4.1", "prettier": "^2.7.1", "react-dom": "^17.0.2", "sinon": "^9.2.3", - "ts-node": "^10.9.1", "typescript": "^5.0.4", "xvfb-maybe": "^0.2.1" } @@ -45928,26 +45801,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, - "packages/compass-generative-ai/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "packages/compass-generative-ai/node_modules/p-queue": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.4.1.tgz", @@ -46066,27 +45919,27 @@ }, "packages/compass-import-export": { "name": "@mongodb-js/compass-import-export", - "version": "7.37.0", + "version": "7.38.0", "license": "SSPL", "dependencies": { "@electron/remote": "^2.1.2", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "debug": "^4.3.4", - "electron": "^29.4.5", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", - "hadron-ipc": "^3.2.20", + "electron": "^30.4.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", @@ -46099,9 +45952,9 @@ "strip-bom-stream": "^4.0.0" }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -46161,27 +46014,27 @@ }, "packages/compass-indexes": { "name": "@mongodb-js/compass-indexes", - "version": "5.37.0", + "version": "5.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-storage": "^0.18.0", "@mongodb-js/mongodb-constants": "^0.10.0", "@mongodb-js/shell-bson-parser": "^1.1.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-query-parser": "^4.2.0", "numeral": "^2.0.6", "react": "^17.0.2", @@ -46191,15 +46044,15 @@ "semver": "^7.6.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -46289,15 +46142,15 @@ }, "packages/compass-intercom": { "name": "@mongodb-js/compass-intercom", - "version": "0.10.0", + "version": "0.11.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "compass-preferences-model": "^2.26.0" + "@mongodb-js/compass-logging": "^1.4.4", + "compass-preferences-model": "^2.27.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -46396,19 +46249,19 @@ }, "packages/compass-logging": { "name": "@mongodb-js/compass-logging", - "version": "1.4.3", + "version": "1.4.4", "license": "SSPL", "dependencies": { "debug": "^4.3.4", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "is-electron-renderer": "^2.0.1", "mongodb-log-writer": "^1.4.2", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -46454,15 +46307,15 @@ }, "packages/compass-maybe-protect-connection-string": { "name": "@mongodb-js/compass-maybe-protect-connection-string", - "version": "0.24.0", + "version": "0.25.0", "license": "SSPL", "dependencies": { - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "mongodb-connection-string-url": "^3.0.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -46507,15 +46360,15 @@ } }, "packages/compass-preferences-model": { - "version": "2.26.0", + "version": "2.27.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/devtools-proxy-support": "^0.3.5", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "bson": "^6.7.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "react": "^17.0.2", @@ -46523,8 +46376,8 @@ "zod": "^3.22.3" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@testing-library/react": "^12.1.5", "@types/js-yaml": "^4.0.5", "@types/yargs-parser": "21.0.0", @@ -46535,6 +46388,73 @@ "sinon": "^9.2.3" } }, + "packages/compass-preferences-model/node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "dependencies": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "packages/compass-preferences-model/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "packages/compass-preferences-model/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/compass-preferences-model/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, + "packages/compass-preferences-model/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "packages/compass-preferences-model/node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -46572,30 +46492,30 @@ }, "packages/compass-query-bar": { "name": "@mongodb-js/compass-query-bar", - "version": "8.39.0", + "version": "8.40.0", "license": "SSPL", "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -46603,8 +46523,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -46612,7 +46532,7 @@ "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -46652,22 +46572,22 @@ }, "packages/compass-saved-aggregations-queries": { "name": "@mongodb-js/compass-saved-aggregations-queries", - "version": "1.38.0", + "version": "1.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "fuse.js": "^6.5.3", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -46675,9 +46595,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -46731,30 +46650,30 @@ }, "packages/compass-schema": { "name": "@mongodb-js/compass-schema", - "version": "6.39.0", + "version": "6.40.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/reflux-state-mixin": "^1.0.4", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/reflux-state-mixin": "^1.0.5", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "leaflet": "^1.5.1", "leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "moment": "^2.29.4", "mongodb": "^6.8.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "numeral": "^1.5.6", "prop-types": "^15.7.2", @@ -46764,9 +46683,9 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -46791,21 +46710,21 @@ }, "packages/compass-schema-validation": { "name": "@mongodb-js/compass-schema-validation", - "version": "6.38.0", + "version": "6.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "javascript-stringify": "^2.0.1", "lodash": "^4.17.21", "mongodb-ns": "^2.4.2", @@ -46818,19 +46737,19 @@ "semver": "^7.6.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-ipc": "^3.2.20", + "hadron-ipc": "^3.2.21", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "nyc": "^15.1.0", "react-dom": "^17.0.2", "sinon": "^8.1.1", @@ -46866,18 +46785,18 @@ }, "packages/compass-serverstats": { "name": "@mongodb-js/compass-serverstats", - "version": "16.37.0", + "version": "16.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", "d3": "^3.5.17", "d3-timer": "^1.0.3", "debug": "^4.3.4", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-ns": "^2.4.2", "prop-types": "^15.7.2", @@ -46885,8 +46804,8 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/d3": "^3.5.x", @@ -46915,24 +46834,24 @@ }, "packages/compass-settings": { "name": "@mongodb-js/compass-settings", - "version": "0.38.0", + "version": "0.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -46985,36 +46904,35 @@ }, "packages/compass-shell": { "name": "@mongodb-js/compass-shell", - "version": "3.37.0", + "version": "3.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", "@mongosh/browser-repl": "^2.3.0", "@mongosh/logging": "^2.3.0", "@mongosh/node-runtime-worker-thread": "^2.3.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", @@ -47054,25 +46972,25 @@ }, "packages/compass-sidebar": { "name": "@mongodb-js/compass-sidebar", - "version": "5.38.0", + "version": "5.39.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connection-import-export": "^0.34.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-connections-navigation": "^1.37.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connection-import-export": "^0.35.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-connections-navigation": "^1.38.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -47080,13 +46998,11 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", @@ -47099,7 +47015,7 @@ "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "react-dom": "^17.0.2", @@ -47108,36 +47024,6 @@ "xvfb-maybe": "^0.2.1" } }, - "packages/compass-sidebar/node_modules/@testing-library/react-hooks": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", - "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-test-renderer": "^16.9.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-test-renderer": { - "optional": true - } - } - }, "packages/compass-sidebar/node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -47167,17 +47053,17 @@ }, "packages/compass-telemetry": { "name": "@mongodb-js/compass-telemetry", - "version": "1.1.3", + "version": "1.1.4", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-logging": "^1.4.4", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -47277,14 +47163,14 @@ }, "packages/compass-test-server": { "name": "@mongodb-js/compass-test-server", - "version": "0.1.19", + "version": "0.1.20", "license": "SSPL", "dependencies": { - "mongodb-runner": "^5.6.2" + "mongodb-runner": "^5.6.3" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/mocha": "^9.0.0", @@ -47299,6 +47185,38 @@ "typescript": "^5.0.4" } }, + "packages/compass-test-server/node_modules/@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "packages/compass-test-server/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/compass-test-server/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "packages/compass-test-server/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -47308,6 +47226,23 @@ "node": ">=0.3.1" } }, + "packages/compass-test-server/node_modules/mongodb-runner": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.5.tgz", + "integrity": "sha512-joZJL5YKuwP7MNegz0CKt7rAPgAeUcWhOfJZPgifnKD+3tl4oMbRwP+HjcQjBieT1dr9lh+kI1MQuGInRtQzMw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/saslprep": "^1.1.8", + "debug": "^4.3.4", + "mongodb": "^6.8.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "mongodb-runner": "bin/runner.js" + } + }, "packages/compass-test-server/node_modules/sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -47326,19 +47261,58 @@ "url": "https://opencollective.com/sinon" } }, + "packages/compass-test-server/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "packages/compass-test-server/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "packages/compass-test-server/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "packages/compass-user-data": { "name": "@mongodb-js/compass-user-data", - "version": "0.3.3", + "version": "0.3.4", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-utils": "^0.6.9", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", "write-file-atomic": "^5.0.1", "zod": "^3.22.3" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -47408,15 +47382,15 @@ }, "packages/compass-utils": { "name": "@mongodb-js/compass-utils", - "version": "0.6.9", + "version": "0.6.10", "license": "SSPL", "dependencies": { "@electron/remote": "^2.1.2", - "electron": "^29.4.5" + "electron": "^30.4.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -47462,35 +47436,35 @@ }, "packages/compass-web": { "name": "@mongodb-js/compass-web", - "version": "0.5.7", + "version": "0.6.0", "license": "SSPL", "devDependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-aggregations": "^9.40.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-databases-collections": "^1.37.0", - "@mongodb-js/compass-explain-plan": "^6.38.0", - "@mongodb-js/compass-export-to-language": "^9.14.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-indexes": "^5.37.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-schema": "^6.39.0", - "@mongodb-js/compass-schema-validation": "^6.38.0", - "@mongodb-js/compass-sidebar": "^5.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-aggregations": "^9.41.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-databases-collections": "^1.38.0", + "@mongodb-js/compass-explain-plan": "^6.39.0", + "@mongodb-js/compass-export-to-language": "^9.15.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-indexes": "^5.38.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-schema": "^6.40.0", + "@mongodb-js/compass-schema-validation": "^6.39.0", + "@mongodb-js/compass-sidebar": "^5.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@mongodb-js/webpack-config-compass": "^1.3.15", + "@mongodb-js/webpack-config-compass": "^1.4.0", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", @@ -47505,23 +47479,23 @@ "bson": "^6.2.0", "buffer": "^6.0.3", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "crypto-browserify": "^3.12.0", "debug": "^4.3.4", "depcheck": "^1.4.1", "dns-query": "^0.11.2", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "events": "^3.3.0", "express": "^4.19.2", "express-http-proxy": "^2.0.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "is-ip": "^5.0.1", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", @@ -47715,18 +47689,6 @@ "url": "https://opencollective.com/sinon" } }, - "packages/compass-web/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dev": true, - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, "packages/compass-web/node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -47740,47 +47702,25 @@ "which-typed-array": "^1.1.2" } }, - "packages/compass-web/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "packages/compass-web/node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dev": true, - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "packages/compass-welcome": { "name": "@mongodb-js/compass-welcome", - "version": "0.36.0", + "version": "0.37.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "react": "^17.0.2", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -47833,19 +47773,19 @@ }, "packages/compass-workspaces": { "name": "@mongodb-js/compass-workspaces", - "version": "0.19.0", + "version": "0.20.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -47853,14 +47793,10 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", "@types/mocha": "^9.0.0", @@ -47918,36 +47854,6 @@ "type-detect": "4.0.8" } }, - "packages/compass-workspaces/node_modules/@testing-library/react-hooks": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", - "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-test-renderer": "^16.9.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-test-renderer": { - "optional": true - } - } - }, "packages/compass-workspaces/node_modules/nise": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", @@ -48006,47 +47912,114 @@ "url": "https://opencollective.com/sinon" } }, - "packages/compass/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "packages/compass/node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" - }, + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "packages/compass/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, "engines": { - "node": "4.x || >=6.0.0" + "node": ">= 12" + } + }, + "packages/compass/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" }, - "peerDependencies": { - "encoding": "^0.1.0" + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "encoding": { + "supports-color": { "optional": true } } }, + "packages/compass/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "packages/compass/node_modules/mongodb-client-encryption": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.1.tgz", + "integrity": "sha512-u6pKu9plR7hQH6VtsfYonC9dwWAM3HFEpi+Xy3EJIdUyoH6dlFgaxX8TnKx/Ycfi2I1cxTXq2IbhSpg157vVgg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "packages/compass/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "packages/connection-form": { "name": "@mongodb-js/connection-form", - "version": "1.36.0", + "version": "1.37.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/connection-info": "^0.5.3", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/connection-info": "^0.6.0", "@mongodb-js/shell-bson-parser": "^1.1.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "lodash": "^4.17.21", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-query-parser": "^4.2.0", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -48100,17 +48073,17 @@ }, "packages/connection-info": { "name": "@mongodb-js/connection-info", - "version": "0.5.3", + "version": "0.6.0", "license": "SSPL", "dependencies": { "lodash": "^4.17.21", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3" + "mongodb-data-service": "^22.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -48226,27 +48199,27 @@ }, "packages/connection-storage": { "name": "@mongodb-js/connection-storage", - "version": "0.17.0", + "version": "0.18.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-info": "^0.5.3", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-info": "^0.6.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "electron": "^29.4.5", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "compass-preferences-model": "^2.27.0", + "electron": "^30.4.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "keytar": "^7.9.0", "lodash": "^4.17.21", "mongodb-connection-string-url": "^3.0.1", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -48291,14 +48264,15 @@ }, "packages/data-service": { "name": "mongodb-data-service", - "version": "22.22.3", + "version": "22.23.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-utils": "^0.6.9", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/ssh-tunnel": "^2.3.3", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "bson": "^6.7.0", + "compass-preferences-model": "^2.27.0", "lodash": "^4.17.21", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", @@ -48306,11 +48280,11 @@ "mongodb-ns": "^2.4.2" }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/devtools-docker-test-envs": "^1.3.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/oidc-plugin": "^1.0.0", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/devtools-docker-test-envs": "^1.3.3", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/oidc-plugin": "^1.1.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/lodash": "^4.14.188", @@ -48321,6 +48295,7 @@ "eslint": "^7.25.0", "kerberos": "^2.1.1", "mocha": "^10.2.0", + "mongodb-log-writer": "^1.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^9.2.3", @@ -48328,7 +48303,148 @@ "typescript": "^5.0.4" }, "optionalDependencies": { - "mongodb-client-encryption": "6.0.0" + "mongodb-client-encryption": "~6.0.1" + } + }, + "packages/data-service/node_modules/@mongodb-js/devtools-docker-test-envs": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.3.3.tgz", + "integrity": "sha512-K7N7+dZXEn2/AXyNYo46rW4uWQ+HZKlU4cuz+o0eOjyiHjiMvZOkIpOP6zFsZ48ft/Jk9xZ1ReTJOuLKTH0rkQ==", + "dev": true, + "dependencies": { + "eslint-plugin-mocha": "^9.0.0", + "execa": "^5.1.1", + "hostile": "^1.3.3", + "mongodb-connection-string-url": "^2.0.0", + "uuid": "^8.3.2", + "wait-on": "^6.0.0" + } + }, + "packages/data-service/node_modules/@mongodb-js/devtools-docker-test-envs/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "packages/data-service/node_modules/@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "dependencies": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "packages/data-service/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "packages/data-service/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/data-service/node_modules/eslint-plugin-mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", + "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "ramda": "^0.27.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "packages/data-service/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "packages/data-service/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "engines": { + "node": "20 || >=22" + } + }, + "packages/data-service/node_modules/mongodb-client-encryption": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.1.tgz", + "integrity": "sha512-u6pKu9plR7hQH6VtsfYonC9dwWAM3HFEpi+Xy3EJIdUyoH6dlFgaxX8TnKx/Ycfi2I1cxTXq2IbhSpg157vVgg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=16.20.1" + } + }, + "packages/data-service/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, "packages/data-service/node_modules/sinon": { @@ -48358,18 +48474,52 @@ "node": ">=0.3.1" } }, + "packages/data-service/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "packages/data-service/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "packages/data-service/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "packages/database-model": { "name": "mongodb-database-model", - "version": "2.22.3", + "version": "2.23.0", "license": "SSPL", "dependencies": { "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -48427,24 +48577,24 @@ }, "packages/databases-collections": { "name": "@mongodb-js/compass-databases-collections", - "version": "1.37.0", + "version": "1.38.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/databases-collections-list": "^1.35.0", - "@mongodb-js/my-queries-storage": "^0.15.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/databases-collections-list": "^1.36.0", + "@mongodb-js/my-queries-storage": "^0.15.1", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "prop-types": "^15.7.2", @@ -48455,8 +48605,8 @@ "semver": "^7.6.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -48475,21 +48625,21 @@ }, "packages/databases-collections-list": { "name": "@mongodb-js/databases-collections-list", - "version": "1.35.0", + "version": "1.36.0", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "compass-preferences-model": "^2.27.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -48595,15 +48745,15 @@ }, "packages/explain-plan-helper": { "name": "@mongodb-js/explain-plan-helper", - "version": "1.1.15", + "version": "1.2.0", "license": "SSPL", "dependencies": { "@mongodb-js/shell-bson-parser": "^1.1.0", - "mongodb-explain-compat": "^3.0.4" + "mongodb-explain-compat": "^3.1.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -48674,7 +48824,7 @@ } }, "packages/hadron-app-registry": { - "version": "9.2.2", + "version": "9.2.3", "license": "SSPL", "dependencies": { "eventemitter3": "^4.0.0", @@ -48684,8 +48834,8 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -48730,7 +48880,7 @@ } }, "packages/hadron-build": { - "version": "25.5.7", + "version": "25.5.8", "hasInstallScript": true, "license": "SSPL", "dependencies": { @@ -48747,7 +48897,7 @@ "debug": "^4.3.4", "del": "^2.0.2", "download": "^8.0.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-packager": "^15.5.1", "electron-packager-plugin-non-proprietary-codecs-ffmpeg": "^1.0.2", "flatnest": "^1.0.0", @@ -48761,7 +48911,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "mongodb-js-cli": "^0.0.3", - "node-abi": "^3.65.0", + "node-abi": "^3.67.0", "normalize-package-data": "^2.3.5", "parse-github-repo-url": "^1.3.0", "semver": "^7.6.2", @@ -48775,7 +48925,7 @@ "hadron-build": "cli.js" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "chai": "^4.2.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -49188,6 +49338,28 @@ "mkdirp": "bin/cmd.js" } }, + "packages/hadron-build/node_modules/node-abi": { + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "packages/hadron-build/node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "packages/hadron-build/node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -49443,7 +49615,7 @@ } }, "packages/hadron-document": { - "version": "8.6.0", + "version": "8.6.1", "license": "SSPL", "dependencies": { "bson": "^6.7.0", @@ -49452,8 +49624,8 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", @@ -49563,16 +49735,16 @@ } }, "packages/hadron-ipc": { - "version": "3.2.20", + "version": "3.2.21", "license": "SSPL", "dependencies": { "debug": "^4.3.4", - "electron": "^29.4.5", + "electron": "^30.4.0", "is-electron-renderer": "^2.0.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -49634,16 +49806,16 @@ }, "packages/instance-model": { "name": "mongodb-instance-model", - "version": "12.23.3", + "version": "12.24.0", "license": "SSPL", "dependencies": { "ampersand-model": "^8.0.1", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "chai": "^4.3.4", "depcheck": "^1.4.1", @@ -49652,7 +49824,7 @@ } }, "packages/mongodb-explain-compat": { - "version": "3.0.4", + "version": "3.1.0", "license": "SSPL", "devDependencies": { "eslint": "^7.25.0", @@ -49668,15 +49840,15 @@ } }, "packages/mongodb-query-util": { - "version": "2.2.5", + "version": "2.2.6", "license": "SSPL", "dependencies": { "bson": "^6.7.0", "lodash": "^4.17.21" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -49911,18 +50083,18 @@ }, "packages/my-queries-storage": { "name": "@mongodb-js/my-queries-storage", - "version": "0.15.0", + "version": "0.15.1", "license": "SSPL", "dependencies": { - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-user-data": "^0.3.3", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-user-data": "^0.3.4", "bson": "^6.7.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -49989,14 +50161,14 @@ }, "packages/reflux-state-mixin": { "name": "@mongodb-js/reflux-state-mixin", - "version": "1.0.4", + "version": "1.0.5", "license": "SSPL", "dependencies": { "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/mocha": "^9.0.0", @@ -50498,6 +50670,7 @@ "packages/ssh-tunnel": { "name": "@mongodb-js/ssh-tunnel", "version": "2.3.3", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "@mongodb-js/compass-logging": "^1.4.3", @@ -50529,62 +50702,15 @@ "typescript": "^5.0.4" } }, - "packages/ssh-tunnel/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "packages/ssh-tunnel/node_modules/sinon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "packages/ssh-tunnel/node_modules/sinon/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "scripts": { "name": "@mongodb-js/compass-scripts", - "version": "0.16.17", + "version": "0.16.18", "license": "SSPL", "dependencies": { "@babel/core": "^7.24.3", "@mongodb-js/monorepo-tools": "^1.1.1", "commander": "^11.0.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "jsdom": "^21.1.0", "make-fetch-happen": "^8.0.14", "pacote": "^11.3.5", @@ -50597,7 +50723,7 @@ "compass-scripts": "cli.js" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -56154,15 +56280,16 @@ "@mongodb-js/atlas-service": { "version": "file:packages/atlas-service", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/oidc-plugin": "^1.0.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/devtools-connect": "^3.2.6", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/oidc-plugin": "^1.1.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -56170,15 +56297,14 @@ "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", "mocha": "^10.2.0", - "node-fetch": "^2.7.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "react": "^17.0.2", @@ -56186,22 +56312,75 @@ "redux": "^4.2.1", "redux-thunk": "^2.4.2", "sinon": "^9.2.3", - "system-ca": "^2.0.0", "typescript": "^5.0.4" }, "dependencies": { + "@mongodb-js/devtools-connect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.2.6.tgz", + "integrity": "sha512-E5jvGDHZ13fnDkuIytnINIS2/2BR0aiC0rfXLKeOO6ongJfL8F5ACEz5dbCR+e6eJ4JCeh1Tb49CfjZK9iGhWQ==", + "requires": { + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/oidc-http-server-pages": "1.1.2", + "kerberos": "^2.1.0", + "lodash.merge": "^4.6.2", + "mongodb-client-encryption": "^6.0.0 || ^6.1.0-alpha.0", + "mongodb-connection-string-url": "^3.0.0", + "os-dns-native": "^1.2.0", + "resolve-mongodb-srv": "^1.1.1", + "socks": "^2.7.3" + } + }, + "@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "requires": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==" + }, "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "requires": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" } }, "sinon": { @@ -56229,24 +56408,24 @@ "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -56257,21 +56436,21 @@ "@types/semver": "^7.3.9", "bson": "^6.7.0", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", @@ -56320,16 +56499,14 @@ "@mongodb-js/compass-app-stores": { "version": "file:packages/compass-app-stores", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/dom": "^8.20.1", - "@testing-library/react": "^12.1.5", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -56337,11 +56514,11 @@ "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -56376,15 +56553,15 @@ "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/mongodb-constants": "^0.10.2", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -56398,13 +56575,13 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", + "mongodb-collection-model": "^5.23.0", "mongodb-ns": "^2.4.2", "numeral": "^2.0.6", "nyc": "^15.1.0", @@ -56492,8 +56669,8 @@ "@leafygreen-ui/tokens": "^2.5.1", "@leafygreen-ui/tooltip": "^11.1.0", "@leafygreen-ui/typography": "^18.2.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@react-aria/interactions": "^3.9.1", @@ -56511,7 +56688,7 @@ "chai": "^4.3.4", "eslint": "^7.25.0", "focus-trap-react": "^9.0.2", - "hadron-document": "^8.6.0", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", @@ -56557,11 +56734,11 @@ "@mongodb-js/compass-connection-import-export": { "version": "file:packages/compass-connection-import-export", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -56573,11 +56750,11 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", - "hadron-ipc": "^3.2.20", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", "mocha": "^10.2.0", "nyc": "^15.1.0", @@ -56615,21 +56792,20 @@ "@mongodb-js/compass-connections": { "version": "file:packages/compass-connections", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/dom": "^8.20.1", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", @@ -56639,20 +56815,23 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", "sinon": "^9.2.3", "xvfb-maybe": "^0.2.1" }, @@ -56684,13 +56863,13 @@ "@mongodb-js/compass-connections-navigation": { "version": "file:packages/compass-connections-navigation", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -56704,7 +56883,7 @@ "@types/react-window": "^1.8.5", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -56746,23 +56925,23 @@ "@mongodb-js/compass-crud": { "version": "file:packages/compass-crud", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/reflux-state-mixin": "^1.0.4", + "@mongodb-js/reflux-state-mixin": "^1.0.5", "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -56773,20 +56952,20 @@ "bson": "^6.7.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", "mocha": "^10.2.0", - "mongodb-data-service": "^22.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-data-service": "^22.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "nyc": "^15.1.0", @@ -56802,33 +56981,33 @@ "@mongodb-js/compass-databases-collections": { "version": "file:packages/databases-collections", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/databases-collections-list": "^1.35.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/databases-collections-list": "^1.36.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "bson": "^6.7.0", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "nyc": "^15.1.0", @@ -56877,9 +57056,9 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.2.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/mongodb-constants": "^0.10.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -56927,15 +57106,15 @@ "@mongodb-js/compass-explain-plan": { "version": "file:packages/compass-explain-plan", "requires": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -56943,15 +57122,15 @@ "@types/d3-flextree": "^2.1.0", "@types/d3-hierarchy": "^3.1.2", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", "d3-flextree": "^2.1.2", "d3-hierarchy": "^3.1.2", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", @@ -56997,26 +57176,26 @@ "@mongodb-js/compass-export-to-language": { "version": "file:packages/compass-export-to-language", "requires": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", - "bson-transpilers": "^3.0.6", + "bson-transpilers": "^3.0.7", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", @@ -57055,13 +57234,12 @@ "@mongodb-js/compass-field-store": { "version": "file:packages/compass-field-store", "requires": { - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -57069,7 +57247,7 @@ "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb-schema": "^12.2.0", @@ -57078,6 +57256,7 @@ "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", + "redux-thunk": "^2.4.2", "sinon": "^9.2.3", "typescript": "^5.0.4", "xvfb-maybe": "^0.2.1" @@ -57108,9 +57287,9 @@ "@mongodb-js/compass-find-in-page": { "version": "file:packages/compass-find-in-page", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -57123,11 +57302,11 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -57168,46 +57347,38 @@ "@mongodb-js/compass-generative-ai": { "version": "file:packages/compass-generative-ai", "requires": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-intercom": "^0.10.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-intercom": "^0.11.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", - "@types/decomment": "^0.9.5", "@types/mocha": "^9.0.0", - "@types/node-fetch": "^2.6.11", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", - "decomment": "^0.9.5", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "digest-fetch": "^2.0.3", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-runner": "^5.6.2", "mongodb-schema": "^12.2.0", - "node-fetch": "^2.7.0", "nyc": "^15.1.0", "p-queue": "^7.4.1", "prettier": "^2.7.1", "react": "^17.0.2", "react-dom": "^17.0.2", "sinon": "^9.2.3", - "ts-node": "^10.9.1", "typescript": "^5.0.4", "xvfb-maybe": "^0.2.1" }, @@ -57224,15 +57395,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "p-queue": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.4.1.tgz", @@ -57269,16 +57431,16 @@ "version": "file:packages/compass-import-export", "requires": { "@electron/remote": "^2.1.2", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -57296,19 +57458,19 @@ "bson": "^6.7.0", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", @@ -57354,18 +57516,18 @@ "@mongodb-js/compass-indexes": { "version": "file:packages/compass-indexes", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/mongodb-constants": "^0.10.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/shell-bson-parser": "^1.1.0", @@ -57374,16 +57536,16 @@ "@testing-library/user-event": "^13.5.0", "bson": "^6.7.0", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-query-parser": "^4.2.0", "numeral": "^2.0.6", "nyc": "^15.1.0", @@ -57430,16 +57592,16 @@ "@mongodb-js/compass-intercom": { "version": "file:packages/compass-intercom", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", @@ -57533,8 +57695,8 @@ "@mongodb-js/compass-logging": { "version": "file:packages/compass-logging", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -57545,8 +57707,8 @@ "debug": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "is-electron-renderer": "^2.0.1", "mocha": "^10.2.0", "mongodb-log-writer": "^1.4.2", @@ -57584,15 +57746,15 @@ "@mongodb-js/compass-maybe-protect-connection-string": { "version": "file:packages/compass-maybe-protect-connection-string", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", @@ -57631,20 +57793,20 @@ "@mongodb-js/compass-query-bar": { "version": "file:packages/compass-query-bar", "requires": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -57652,19 +57814,19 @@ "@testing-library/user-event": "^13.5.0", "bson": "^6.7.0", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "nyc": "^15.1.0", "react": "^17.0.2", @@ -57702,18 +57864,17 @@ "@mongodb-js/compass-saved-aggregations-queries": { "version": "file:packages/compass-saved-aggregations-queries", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -57727,12 +57888,12 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "fuse.js": "^6.5.3", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", @@ -57774,19 +57935,19 @@ "@mongodb-js/compass-schema": { "version": "file:packages/compass-schema", "requires": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/reflux-state-mixin": "^1.0.4", + "@mongodb-js/reflux-state-mixin": "^1.0.5", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", @@ -57798,13 +57959,13 @@ "@types/react-dom": "^17.0.10", "bson": "^6.7.0", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "leaflet": "^1.5.1", "leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-draw": "^1.0.4", @@ -57812,7 +57973,7 @@ "mocha": "^10.2.0", "moment": "^2.29.4", "mongodb": "^6.8.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "numeral": "^1.5.6", "nyc": "^15.1.0", @@ -57852,33 +58013,33 @@ "@mongodb-js/compass-schema-validation": { "version": "file:packages/compass-schema-validation", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "bson": "^6.7.0", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "javascript-stringify": "^2.0.1", "lodash": "^4.17.21", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "nyc": "^15.1.0", @@ -57897,12 +58058,12 @@ "version": "file:scripts", "requires": { "@babel/core": "^7.24.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/monorepo-tools": "^1.1.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "commander": "^11.0.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "jsdom": "^21.1.0", "make-fetch-happen": "^8.0.14", @@ -58115,13 +58276,13 @@ "@mongodb-js/compass-serverstats": { "version": "file:packages/compass-serverstats", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/d3": "^3.5.x", @@ -58134,7 +58295,7 @@ "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb-ns": "^2.4.2", @@ -58162,12 +58323,12 @@ "@mongodb-js/compass-settings": { "version": "file:packages/compass-settings", "requires": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -58179,12 +58340,12 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -58225,16 +58386,15 @@ "@mongodb-js/compass-shell": { "version": "file:packages/compass-shell", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@mongosh/browser-repl": "^2.3.0", @@ -58242,13 +58402,13 @@ "@mongosh/node-runtime-worker-thread": "^2.3.0", "bson": "^6.7.0", "chai": "^4.2.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "nyc": "^15.1.0", "react": "^17.0.2", @@ -58285,24 +58445,22 @@ "@mongodb-js/compass-sidebar": { "version": "file:packages/compass-sidebar", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connection-import-export": "^0.34.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-connections-navigation": "^1.37.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connection-import-export": "^0.35.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-connections-navigation": "^1.38.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", @@ -58311,16 +58469,16 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-data-service": "^22.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -58334,15 +58492,6 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@testing-library/react-hooks": { - "version": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", - "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - } - }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -58370,9 +58519,9 @@ "@mongodb-js/compass-telemetry": { "version": "file:packages/compass-telemetry", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -58382,8 +58531,8 @@ "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -58476,8 +58625,8 @@ "@mongodb-js/compass-test-server": { "version": "file:packages/compass-test-server", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/mocha": "^9.0.0", @@ -58486,19 +58635,55 @@ "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^10.2.0", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.3", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^9.2.3", "typescript": "^5.0.4" }, "dependencies": { + "@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "mongodb-runner": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.5.tgz", + "integrity": "sha512-joZJL5YKuwP7MNegz0CKt7rAPgAeUcWhOfJZPgifnKD+3tl4oMbRwP+HjcQjBieT1dr9lh+kI1MQuGInRtQzMw==", + "requires": { + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/saslprep": "^1.1.8", + "debug": "^4.3.4", + "mongodb": "^6.8.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + } + }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -58512,16 +58697,43 @@ "nise": "^4.0.4", "supports-color": "^7.1.0" } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" } } }, "@mongodb-js/compass-user-data": { "version": "file:packages/compass-user-data", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -58581,8 +58793,8 @@ "version": "file:packages/compass-utils", "requires": { "@electron/remote": "^2.1.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -58590,7 +58802,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^10.2.0", @@ -58627,32 +58839,32 @@ "@mongodb-js/compass-web": { "version": "file:packages/compass-web", "requires": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-aggregations": "^9.40.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-databases-collections": "^1.37.0", - "@mongodb-js/compass-explain-plan": "^6.38.0", - "@mongodb-js/compass-export-to-language": "^9.14.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-indexes": "^5.37.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-schema": "^6.39.0", - "@mongodb-js/compass-schema-validation": "^6.38.0", - "@mongodb-js/compass-sidebar": "^5.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-aggregations": "^9.41.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-databases-collections": "^1.38.0", + "@mongodb-js/compass-explain-plan": "^6.39.0", + "@mongodb-js/compass-export-to-language": "^9.15.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-indexes": "^5.38.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-schema": "^6.40.0", + "@mongodb-js/compass-schema-validation": "^6.39.0", + "@mongodb-js/compass-sidebar": "^5.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@mongodb-js/webpack-config-compass": "^1.3.15", + "@mongodb-js/webpack-config-compass": "^1.4.0", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", @@ -58667,23 +58879,23 @@ "bson": "^6.2.0", "buffer": "^6.0.3", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "crypto-browserify": "^3.12.0", "debug": "^4.3.4", "depcheck": "^1.4.1", "dns-query": "^0.11.2", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "events": "^3.3.0", "express": "^4.19.2", "express-http-proxy": "^2.0.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "is-ip": "^5.0.1", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", @@ -58834,15 +59046,6 @@ "supports-color": "^7.2.0" } }, - "tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dev": true, - "requires": { - "punycode": "^2.3.0" - } - }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -58855,35 +59058,19 @@ "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true - }, - "whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dev": true, - "requires": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - } } } }, "@mongodb-js/compass-welcome": { "version": "file:packages/compass-welcome", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -58895,11 +59082,11 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -58939,18 +59126,14 @@ "@mongodb-js/compass-workspaces": { "version": "file:packages/compass-workspaces", "requires": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", - "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", "@types/mocha": "^9.0.0", @@ -58959,15 +59142,15 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -59021,15 +59204,6 @@ } } }, - "@testing-library/react-hooks": { - "version": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", - "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - } - }, "nise": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", @@ -59093,11 +59267,11 @@ "@mongodb-js/connection-form": { "version": "file:packages/connection-form", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -59112,7 +59286,7 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", @@ -59121,7 +59295,7 @@ "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-query-parser": "^4.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -59158,8 +59332,8 @@ "@mongodb-js/connection-info": { "version": "file:packages/connection-info", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -59174,7 +59348,7 @@ "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^17.0.1", @@ -59284,13 +59458,13 @@ "@mongodb-js/connection-storage": { "version": "file:packages/connection-storage", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -59298,12 +59472,12 @@ "@types/sinon-chai": "^3.2.5", "bson": "^6.7.0", "chai": "^4.3.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "keytar": "^7.9.0", "lodash": "^4.17.21", "mocha": "^10.2.0", @@ -59340,13 +59514,13 @@ "@mongodb-js/databases-collections-list": { "version": "file:packages/databases-collections-list", "requires": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -59358,7 +59532,7 @@ "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -59404,117 +59578,13 @@ "@mongodb-js/oidc-http-server-pages": "1.1.2", "kerberos": "^2.1.0", "lodash.merge": "^4.6.2", - "mongodb-client-encryption": "6.0.0", + "mongodb-client-encryption": "~6.0.1", "mongodb-connection-string-url": "^3.0.0", "os-dns-native": "^1.2.0", "resolve-mongodb-srv": "^1.1.1", "socks": "^2.7.3" } }, - "@mongodb-js/devtools-docker-test-envs": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.3.2.tgz", - "integrity": "sha512-CwRzxmQ5+t37CuXdCca9CaHTvnKqBOOViU6LkjvLWLB2Qq1emrltbZBtbY4IRoWclRbZemA+vXhHNByXZNEgIw==", - "dev": true, - "requires": { - "eslint-plugin-mocha": "^9.0.0", - "execa": "^5.1.1", - "hostile": "^1.3.3", - "mongodb-connection-string-url": "^2.0.0", - "uuid": "^8.3.2", - "wait-on": "^6.0.0" - }, - "dependencies": { - "eslint-plugin-mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", - "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", - "dev": true, - "requires": { - "eslint-utils": "^3.0.0", - "ramda": "^0.27.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dev": true, - "requires": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "rxjs": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz", - "integrity": "sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw==", - "dev": true, - "requires": { - "tslib": "~2.1.0" - } - }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, - "wait-on": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", - "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", - "dev": true, - "requires": { - "axios": "^0.21.1", - "joi": "^17.4.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^7.1.0" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - } - } - }, "@mongodb-js/devtools-github-repo": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-github-repo/-/devtools-github-repo-1.4.1.tgz", @@ -59629,7 +59699,7 @@ "@babel/core": "^7.21.4", "@babel/eslint-parser": "^7.14.3", "@mongodb-js/eslint-config-devtools": "^0.9.9", - "@mongodb-js/eslint-plugin-compass": "^1.0.18", + "@mongodb-js/eslint-plugin-compass": "^1.0.19", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "eslint-config-prettier": "^8.3.0", @@ -59726,7 +59796,7 @@ "@mongodb-js/eslint-plugin-compass": { "version": "file:configs/eslint-plugin-compass", "requires": { - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -59738,8 +59808,8 @@ "@mongodb-js/explain-plan-helper": { "version": "file:packages/explain-plan-helper", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/shell-bson-parser": "^1.1.0", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -59750,7 +59820,7 @@ "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-explain-compat": "^3.0.4", + "mongodb-explain-compat": "^3.1.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^9.2.3", @@ -59836,13 +59906,13 @@ } }, "@mongodb-js/mongodb-downloader": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.2.tgz", - "integrity": "sha512-bhMfxzaBy31RveAu7qqON3nVXRHYmxJXyC3lZI+mK+4DhagKZdGHJpMkLmHQRt+wAxMR6ldI9YlcWjHSqceIsQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-downloader/-/mongodb-downloader-0.3.5.tgz", + "integrity": "sha512-0tNik3E/eQxx8EkJpsa9+IIK5LtMle3N7M/1wtKQKMixJjZ1vsgAjJGguYbLfCyJqfnrw9KMyD1cwgaS37tvRA==", "requires": { "debug": "^4.3.4", "decompress": "^4.2.1", - "mongodb-download-url": "^1.3.0", + "mongodb-download-url": "^1.5.1", "node-fetch": "^2.6.11", "tar": "^6.1.15" }, @@ -59854,6 +59924,25 @@ "requires": { "whatwg-url": "^5.0.0" } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, @@ -59959,10 +60048,10 @@ "@mongodb-js/my-queries-storage": { "version": "file:packages/my-queries-storage", "requires": { - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -59973,7 +60062,7 @@ "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mocha": "^10.2.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -60068,9 +60157,9 @@ } }, "@mongodb-js/oidc-plugin": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.0.2.tgz", - "integrity": "sha512-hwTbkmJ31RPB5ksA6pLepnaQOBz6iurE+uH89B1IIJdxVuiO0Qz+OqpTN8vk8LZzcVDb/WbNoxqxogCWwMqFKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-1.1.1.tgz", + "integrity": "sha512-u2t3dvUpQJeTmMvXyZu730yJzqJ3aKraQ7ELlNwpKpl1AGxL6Dd9Z2AEu9ycExZjXhyjBW/lbaWuEhdNZHEgeg==", "requires": { "express": "^4.18.2", "open": "^9.1.0", @@ -60111,8 +60200,8 @@ "@mongodb-js/reflux-state-mixin": { "version": "file:packages/reflux-state-mixin", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/mocha": "^9.0.0", @@ -60339,69 +60428,6 @@ "ip-address": "^9.0.5" } }, - "@mongodb-js/ssh-tunnel": { - "version": "file:packages/ssh-tunnel", - "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/tsconfig-compass": "^1.0.4", - "@types/chai": "^4.2.21", - "@types/chai-as-promised": "^7.1.4", - "@types/mocha": "^9.0.0", - "@types/node-fetch": "^2.6.11", - "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "^1.11.8", - "chai": "^4.3.4", - "chai-as-promised": "^7.1.1", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "gen-esm-wrapper": "^1.1.0", - "mocha": "^10.2.0", - "node-fetch": "^2.7.0", - "nyc": "^15.1.0", - "prettier": "^2.7.1", - "sinon": "^9.2.3", - "socks": "^2.7.3", - "socksv5": "0.0.6", - "ssh2": "^1.12.0", - "typescript": "^5.0.4" - }, - "dependencies": { - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "sinon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", - "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.0.4", - "supports-color": "^7.1.0" - }, - "dependencies": { - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - } - } - } - } - }, "@mongodb-js/tsconfig-compass": { "version": "file:configs/tsconfig-compass", "requires": { @@ -60426,7 +60452,7 @@ "@babel/preset-typescript": "^7.21.4", "@babel/runtime": "^7.21.0", "@cerner/duplicate-package-checker-webpack-plugin": "^2.1.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", @@ -60442,7 +60468,7 @@ "core-js": "^3.17.3", "css-loader": "^4.3.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "html-webpack-plugin": "^5.3.2", "less": "^3.13.1", @@ -60959,7 +60985,7 @@ "bson": "^6.7.0", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", - "mongodb-client-encryption": "6.0.0", + "mongodb-client-encryption": "~6.0.1", "mongodb-connection-string-url": "^3.0.1" } }, @@ -64471,12 +64497,6 @@ "@types/ms": "*" } }, - "@types/decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@types/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-zfremx+C6eM4p0Y+VRbjyYnieRWkezqnM+QUX97dulAl0+9RYptIiIOaHgUdHvXOvq3ykZC0yVA8YMXMj6v6ag==", - "dev": true - }, "@types/enzyme": { "version": "3.10.14", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.14.tgz", @@ -64758,29 +64778,6 @@ "undici-types": "~5.26.4" } }, - "@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -67170,12 +67167,12 @@ "integrity": "sha512-3WVgVPs/7OnKU3s+lqMtkv3wQlg3WxK1YifmpJSDO0E1aPBrZWlrrTO6cxRqCXLuX2aYgCljqXIQd0VnRidV0g==" }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz", + "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==", "dev": true, "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.7" } }, "axobject-query": { @@ -67485,12 +67482,6 @@ "streamx": "^2.18.0" } }, - "base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", - "dev": true - }, "base32-encode": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", @@ -67529,9 +67520,9 @@ "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" }, "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" }, "big.js": { "version": "5.2.2", @@ -67854,7 +67845,7 @@ "bson-transpilers": { "version": "file:packages/bson-transpilers", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "antlr4": "4.7.2", "bson": "^6.2.0", "chai": "^4.3.4", @@ -68137,12 +68128,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", - "dev": true - }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -68282,14 +68267,6 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" }, - "cli": { - "version": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - } - }, "cli-color": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-0.3.2.tgz", @@ -68573,9 +68550,9 @@ "version": "file:packages/compass-e2e-tests", "requires": { "@electron/rebuild": "^3.6.0", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/oidc-mock-provider": "^0.9.3", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -68587,21 +68564,21 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "clipboardy": "^2.3.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", - "hadron-build": "^25.5.7", + "hadron-build": "^25.5.8", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", "mongodb-log-writer": "^1.4.2", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.3", "node-fetch": "^2.7.0", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -68615,6 +68592,21 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { + "@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "dev": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, "clipboardy": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", @@ -68626,6 +68618,17 @@ "is-wsl": "^2.1.1" } }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -68737,6 +68740,20 @@ "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", "dev": true }, + "mongodb-runner": { + "version": "5.6.5", + "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.5.tgz", + "integrity": "sha512-joZJL5YKuwP7MNegz0CKt7rAPgAeUcWhOfJZPgifnKD+3tl4oMbRwP+HjcQjBieT1dr9lh+kI1MQuGInRtQzMw==", + "dev": true, + "requires": { + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/saslprep": "^1.1.8", + "debug": "^4.3.4", + "mongodb": "^6.8.0", + "mongodb-connection-string-url": "^3.0.0", + "yargs": "^17.7.2" + } + }, "node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -68781,17 +68798,69 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } }, "compass-preferences-model": { "version": "file:packages/compass-preferences-model", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/devtools-proxy-support": "^0.3.5", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@testing-library/react": "^12.1.5", "@types/js-yaml": "^4.0.5", "@types/yargs-parser": "21.0.0", @@ -68799,8 +68868,8 @@ "chai": "^4.3.6", "depcheck": "^1.4.1", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "mocha": "^10.2.0", @@ -68810,6 +68879,52 @@ "zod": "^3.22.3" }, "dependencies": { + "@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "requires": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==" + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -69418,12 +69533,6 @@ "resolved": "https://registry.npmjs.org/cross-unzip/-/cross-unzip-0.0.2.tgz", "integrity": "sha1-UYO8R6CVWb78+YzEZXlkmZNZNy8=" }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", - "dev": true - }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -69736,11 +69845,6 @@ "punycode": "^2.1.1" } }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, "whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -69834,15 +69938,6 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, - "decomment": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/decomment/-/decomment-0.9.5.tgz", - "integrity": "sha512-h0TZ8t6Dp49duwyDHo3iw67mnh9/UpFiSSiOb5gDK1sqoXzrfX/SQxIUQd2R2QEiSnqib0KF2fnKnGfAhAs6lg==", - "dev": true, - "requires": { - "esprima": "4.0.1" - } - }, "decompress": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", @@ -70063,9 +70158,9 @@ }, "dependencies": { "execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", "requires": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", @@ -70094,9 +70189,9 @@ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==" }, "npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "requires": { "path-key": "^4.0.0" } @@ -70431,18 +70526,6 @@ "heap": ">= 0.2.0" } }, - "digest-fetch": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-2.0.3.tgz", - "integrity": "sha512-HuTjHQE+wplAR+H8/YGwQjIGR1RQUCEsQcRyp3dZfuuxpSQH4OTm4BkHxyXuzxwmxUrNVzIPf9XkXi8QMJDNwQ==", - "dev": true, - "requires": { - "base-64": "^0.1.0", - "js-sha256": "^0.9.0", - "js-sha512": "^0.8.0", - "md5": "^2.3.0" - } - }, "dir-compare": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-2.4.0.tgz", @@ -70551,13 +70634,6 @@ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", "requires": { "webidl-conversions": "^7.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - } } }, "domhandler": { @@ -70993,9 +71069,9 @@ } }, "electron": { - "version": "29.4.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.4.5.tgz", - "integrity": "sha512-DlEuzGbWBYl1Qr0qUYgNZdoixJg4YGHy2HC6fkRjSXSlb01UrQ5ORi8hNLzelzyYx8rNQyyE3zDUuk9EnZwYuA==", + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-30.4.0.tgz", + "integrity": "sha512-ric3KLPQ9anXYjtTDkj5NbEcXZqRUwqxrxTviIjLdMdHqd5O+hkSHEzXgbSJUOt+7uw+zZuybn9+IM9y7iEpqg==", "requires": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", @@ -73100,11 +73176,6 @@ "strip-final-newline": "^2.0.0" } }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -74759,8 +74830,8 @@ "hadron-app-registry": { "version": "file:packages/hadron-app-registry", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -74811,7 +74882,7 @@ "@mongodb-js/devtools-github-repo": "^1.4.1", "@mongodb-js/dl-center": "^1.0.1", "@mongodb-js/electron-wix-msi": "^3.0.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/signing-utils": "^0.3.1", "@npmcli/arborist": "^6.2.0", "@octokit/rest": "^18.6.2", @@ -74823,7 +74894,7 @@ "del": "^2.0.2", "depcheck": "^1.4.1", "download": "^8.0.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-installer-debian": "^3.2.0", "electron-installer-dmg": "^4.0.0", "electron-installer-redhat": "^2.0.0", @@ -74844,7 +74915,7 @@ "mocha": "^10.2.0", "moment": "^2.29.4", "mongodb-js-cli": "^0.0.3", - "node-abi": "^3.65.0", + "node-abi": "^3.67.0", "normalize-package-data": "^2.3.5", "parse-github-repo-url": "^1.3.0", "plist": "^3.0.1", @@ -75183,6 +75254,21 @@ "minimist": "^1.2.6" } }, + "node-abi": { + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + } + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -75390,8 +75476,8 @@ "hadron-document": { "version": "file:packages/hadron-document", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "bson": "^6.7.0", @@ -75511,8 +75597,8 @@ "hadron-ipc": { "version": "file:packages/hadron-ipc", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -75522,7 +75608,7 @@ "chai": "^4.3.6", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "is-electron-renderer": "^2.0.1", "mocha": "^10.2.0", @@ -76535,12 +76621,6 @@ "call-bind": "^1.0.2" } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -77118,18 +77198,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" }, - "js-sha256": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", - "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", - "dev": true - }, - "js-sha512": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", - "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -77285,11 +77353,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, "whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -80006,17 +80069,6 @@ "integrity": "sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==", "dev": true }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -80579,6 +80631,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.0.tgz", "integrity": "sha512-GtqkqlSq19acX006/U1odA3l+gwhvABeoTUlvvgtvSs6qcN3qSHPnur3Z5N4oKOv6fZ7EtT8rIsWP2riI0+Eyg==", + "optional": true, "requires": { "bindings": "^1.5.0", "node-addon-api": "^4.3.0", @@ -80619,13 +80672,35 @@ "requires": { "whatwg-url": "^5.0.0" } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, "mongodb-collection-model": { "version": "file:packages/collection-model", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", @@ -80633,7 +80708,7 @@ "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "xvfb-maybe": "^0.2.1" } @@ -80643,70 +80718,69 @@ "requires": { "@electron/rebuild": "^3.6.0", "@electron/remote": "^2.1.2", - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-aggregations": "^9.40.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connection-import-export": "^0.34.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-databases-collections": "^1.37.0", - "@mongodb-js/compass-explain-plan": "^6.38.0", - "@mongodb-js/compass-export-to-language": "^9.14.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-find-in-page": "^4.30.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-import-export": "^7.37.0", - "@mongodb-js/compass-indexes": "^5.37.0", - "@mongodb-js/compass-intercom": "^0.10.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-saved-aggregations-queries": "^1.38.0", - "@mongodb-js/compass-schema": "^6.39.0", - "@mongodb-js/compass-schema-validation": "^6.38.0", - "@mongodb-js/compass-serverstats": "^16.37.0", - "@mongodb-js/compass-settings": "^0.38.0", - "@mongodb-js/compass-shell": "^3.37.0", - "@mongodb-js/compass-sidebar": "^5.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-welcome": "^0.36.0", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/devtools-proxy-support": "^0.3.5", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-aggregations": "^9.41.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connection-import-export": "^0.35.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-databases-collections": "^1.38.0", + "@mongodb-js/compass-explain-plan": "^6.39.0", + "@mongodb-js/compass-export-to-language": "^9.15.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-find-in-page": "^4.30.1", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-import-export": "^7.38.0", + "@mongodb-js/compass-indexes": "^5.38.0", + "@mongodb-js/compass-intercom": "^0.11.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-saved-aggregations-queries": "^1.39.0", + "@mongodb-js/compass-schema": "^6.40.0", + "@mongodb-js/compass-schema-validation": "^6.39.0", + "@mongodb-js/compass-serverstats": "^16.38.0", + "@mongodb-js/compass-settings": "^0.39.0", + "@mongodb-js/compass-shell": "^3.38.0", + "@mongodb-js/compass-sidebar": "^5.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-welcome": "^0.37.0", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-storage": "^0.18.0", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/get-os-info": "^0.3.24", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/mongodb-downloader": "^0.3.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/mongodb-downloader": "^0.3.5", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/sbom-tools": "^0.7.0", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@mongodb-js/webpack-config-compass": "^1.3.15", + "@mongodb-js/webpack-config-compass": "^1.4.0", "@mongosh/node-runtime-worker-thread": "^2.3.0", "@segment/analytics-node": "^1.1.4", - "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "ampersand-view": "^9.0.0", "chai": "^4.3.4", "chalk": "^4.1.2", "clean-stack": "^2.0.0", "clipboard": "^2.0.6", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-devtools-installer": "^3.2.0", "electron-dl": "^3.5.0", "electron-mocha": "^12.2.0", "electron-squirrel-startup": "^1.0.1", "ensure-error": "^3.0.1", "eslint": "^7.25.0", - "hadron-app-registry": "^9.2.2", - "hadron-build": "^25.5.7", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-build": "^25.5.8", + "hadron-ipc": "^3.2.21", "kerberos": "^2.1.1", "keytar": "^7.9.0", "local-links": "^1.4.0", @@ -80715,14 +80789,13 @@ "marky": "^1.2.1", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", - "mongodb-client-encryption": "6.0.0", + "mongodb-client-encryption": "~6.0.1", "mongodb-cloud-info": "^2.1.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-data-service": "^22.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", - "node-fetch": "^2.7.0", "os-dns-native": "^1.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -80737,13 +80810,65 @@ "winreg-ts": "^1.0.4" }, "dependencies": { + "@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "dev": true, + "requires": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true + }, + "mongodb-client-encryption": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.1.tgz", + "integrity": "sha512-u6pKu9plR7hQH6VtsfYonC9dwWAM3HFEpi+Xy3EJIdUyoH6dlFgaxX8TnKx/Ycfi2I1cxTXq2IbhSpg157vVgg==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + } + }, "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, "requires": { - "whatwg-url": "^5.0.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" } } } @@ -80764,50 +80889,29 @@ "requires": { "@types/webidl-conversions": "*" } - }, - "tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "requires": { - "punycode": "^2.3.0" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" - }, - "whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "requires": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - } } } }, "mongodb-data-service": { "version": "file:packages/data-service", "requires": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/compass-utils": "^0.6.9", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/compass-utils": "^0.6.10", "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/devtools-docker-test-envs": "^1.3.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/oidc-plugin": "^1.0.0", + "@mongodb-js/devtools-docker-test-envs": "^1.3.3", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/oidc-plugin": "^1.1.1", "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/ssh-tunnel": "^2.3.3", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/lodash": "^4.14.188", "@types/whatwg-url": "^8.2.1", "bson": "^6.7.0", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", + "compass-preferences-model": "^2.27.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "kerberos": "^2.1.1", @@ -80815,8 +80919,9 @@ "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", - "mongodb-client-encryption": "6.0.0", + "mongodb-client-encryption": "~6.0.1", "mongodb-connection-string-url": "^3.0.1", + "mongodb-log-writer": "^1.4.2", "mongodb-ns": "^2.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", @@ -80825,6 +80930,108 @@ "typescript": "^5.0.4" }, "dependencies": { + "@mongodb-js/devtools-docker-test-envs": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.3.3.tgz", + "integrity": "sha512-K7N7+dZXEn2/AXyNYo46rW4uWQ+HZKlU4cuz+o0eOjyiHjiMvZOkIpOP6zFsZ48ft/Jk9xZ1ReTJOuLKTH0rkQ==", + "dev": true, + "requires": { + "eslint-plugin-mocha": "^9.0.0", + "execa": "^5.1.1", + "hostile": "^1.3.3", + "mongodb-connection-string-url": "^2.0.0", + "uuid": "^8.3.2", + "wait-on": "^6.0.0" + }, + "dependencies": { + "mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + } + } + }, + "@mongodb-js/devtools-proxy-support": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-proxy-support/-/devtools-proxy-support-0.3.6.tgz", + "integrity": "sha512-si41DJkT/SGhXm3C6BAdnts/5iKy1KJk/QnH0iWL+esMiiYkY929BIJ0v1sg6mm2cE9sX+nUb/a1jEByXWk8Dg==", + "requires": { + "@mongodb-js/socksv5": "^0.0.10", + "agent-base": "^7.1.1", + "debug": "^4.3.6", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "lru-cache": "^11.0.0", + "node-fetch": "^3.3.2", + "pac-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.4", + "ssh2": "^1.15.0", + "system-ca": "^2.0.0" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" + } + }, + "eslint-plugin-mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-9.0.0.tgz", + "integrity": "sha512-d7knAcQj1jPCzZf3caeBIn3BnW6ikcvfz0kSqQpwPYcVGLoJV5sz0l0OJB2LR8I7dvTDbqq1oV6ylhSgzA10zg==", + "dev": true, + "requires": { + "eslint-utils": "^3.0.0", + "ramda": "^0.27.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==" + }, + "mongodb-client-encryption": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.1.tgz", + "integrity": "sha512-u6pKu9plR7hQH6VtsfYonC9dwWAM3HFEpi+Xy3EJIdUyoH6dlFgaxX8TnKx/Ycfi2I1cxTXq2IbhSpg157vVgg==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.2" + } + }, + "node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "sinon": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", @@ -80846,27 +81053,52 @@ "dev": true } } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } } } }, "mongodb-database-model": { "version": "file:packages/database-model", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0" } }, "mongodb-download-url": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.3.0.tgz", - "integrity": "sha512-N7mRi3/LIAHCeTa+JtJVrVno4BNHVYF+6/WUamVFsbvCxtljDmQA1n9FSQxV4dfdiknR9zaoFcXAmd1gtg3Elg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mongodb-download-url/-/mongodb-download-url-1.5.1.tgz", + "integrity": "sha512-AJH2lqb7mBo7tT7RyFWK3P/ZMh7RC1qWJgOaAVrBdKeuPuCWGCESrti+ZMt6FA6mJ4eU58Lm7iG2rTkl94pBdQ==", "requires": { "debug": "^4.1.1", "minimist": "^1.2.3", @@ -80892,16 +81124,16 @@ "mongodb-instance-model": { "version": "file:packages/instance-model", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "ampersand-model": "^8.0.1", "chai": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0" } }, "mongodb-js-cli": { @@ -81182,8 +81414,8 @@ "mongodb-query-util": { "version": "file:packages/mongodb-query-util", "requires": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -81232,63 +81464,6 @@ "lodash": "^4.17.21" } }, - "mongodb-runner": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/mongodb-runner/-/mongodb-runner-5.6.2.tgz", - "integrity": "sha512-6XF3iGXswbJy8TC4VgYPVxnrMiUTJ7iaehE+Hiox2sZL2y3b6aNKkrD3Rt2w6nO0JKnwlR/mukyXbMlz2Zmuvw==", - "requires": { - "@mongodb-js/mongodb-downloader": "^0.3.2", - "@mongodb-js/saslprep": "^1.1.7", - "debug": "^4.3.4", - "mongodb": "^6.3.0", - "mongodb-connection-string-url": "^3.0.0", - "yargs": "^17.7.2" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - } - } - }, "mongodb-schema": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/mongodb-schema/-/mongodb-schema-12.2.0.tgz", @@ -81528,6 +81703,27 @@ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "requires": { "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } } }, "node-gyp": { @@ -84851,6 +85047,28 @@ "requires": { "whatwg-url": "^5.0.0" } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, @@ -85789,33 +86007,6 @@ "devOptional": true, "requires": { "whatwg-url": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0" - }, - "dependencies": { - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "devOptional": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "devOptional": true - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "devOptional": true, - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - } } }, "responselike": { @@ -86806,31 +86997,6 @@ "socks": "^2.8.3" } }, - "socksv5": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", - "integrity": "sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=", - "requires": { - "ipv6": "*" - }, - "dependencies": { - "ipv6": { - "version": "3.1.1", - "bundled": true, - "requires": { - "cli": "0.4.x", - "cliff": "0.1.x", - "sprintf": "0.1.x" - }, - "dependencies": { - "sprintf": { - "version": "0.1.3", - "bundled": true - } - } - } - } - }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -87932,9 +88098,12 @@ } }, "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "requires": { + "punycode": "^2.3.0" + } }, "traverse": { "version": "0.6.6", @@ -88756,6 +88925,36 @@ } } }, + "wait-on": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz", + "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==", + "dev": true, + "requires": { + "axios": "^0.25.0", + "joi": "^17.6.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.5.4" + }, + "dependencies": { + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + } + } + }, "wait-port": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", @@ -89147,9 +89346,9 @@ } }, "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "webpack": { "version": "5.86.0", @@ -89308,12 +89507,12 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" }, "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" } }, "which": { diff --git a/package.json b/package.json index ecb8aae5220..865ab3d3f97 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "release": "npm run release --workspace mongodb-compass --", "reformat": "lerna run reformat --stream --no-bail", "package-compass": "npm run package-compass --workspace=mongodb-compass --", + "package-compass-debug": "npm run package-compass-debug --workspace=mongodb-compass --", "package-compass-nocompile": "npm run package-compass-nocompile --workspace=mongodb-compass --", "prestart": "npm run compile --workspace=@mongodb-js/webpack-config-compass", "prestart-web": "npm run prestart", @@ -92,6 +93,6 @@ "scripts" ], "overrides": { - "mongodb-client-encryption": "6.0.0" + "mongodb-client-encryption": "~6.0.1" } } diff --git a/packages/atlas-service/package.json b/packages/atlas-service/package.json index b446bf422cc..a6bb4ef9636 100644 --- a/packages/atlas-service/package.json +++ b/packages/atlas-service/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.26.0", + "version": "0.27.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -55,8 +55,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -73,23 +73,22 @@ "typescript": "^5.0.4" }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/oidc-plugin": "^1.0.0", - "hadron-app-registry": "^9.2.2", - "compass-preferences-model": "^2.26.0", - "electron": "^29.4.5", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/devtools-connect": "^3.2.6", + "@mongodb-js/devtools-proxy-support": "^0.3.6", + "@mongodb-js/oidc-plugin": "^1.1.1", + "hadron-app-registry": "^9.2.3", + "compass-preferences-model": "^2.27.0", + "electron": "^30.4.0", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", - "node-fetch": "^2.7.0", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "system-ca": "^2.0.0" + "redux-thunk": "^2.4.2" } } diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index 8fa186d89d2..19e9a527fd2 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -85,11 +85,12 @@ describe('CompassAuthServiceMain', function () { createHandle: sandbox.stub(), }; CompassAuthService['fetch'] = mockFetch as any; + CompassAuthService['httpClient'] = { fetch: mockFetch } as any; CompassAuthService['createMongoDBOIDCPlugin'] = () => mockOidcPlugin; CompassAuthService['config'] = defaultConfig; - await CompassAuthService['setupPlugin'](); + CompassAuthService['setupPlugin'](); CompassAuthService['attachOidcPluginLoggerEvents'](); preferences = await createSandboxFromDefaultPreferences(); @@ -289,27 +290,9 @@ describe('CompassAuthServiceMain', function () { CompassAuthService as any, 'setupPlugin' ); - await CompassAuthService.init(preferences); + await CompassAuthService.init(preferences, {} as any); expect(setupPluginSpy).to.have.been.calledOnce; }); - - it('should pass the system ca to the plugin as a custom http option', async function () { - const createOIDCPluginSpy = sandbox.spy( - CompassAuthService as any, - 'createMongoDBOIDCPlugin' - ); - await CompassAuthService.init(preferences); - expect(createOIDCPluginSpy).to.have.been.calledOnce; - try { - expect( - createOIDCPluginSpy.firstCall.args[0].customHttpOptions.ca - ).to.include('-----BEGIN CERTIFICATE-----'); - } catch (e) { - throw new Error( - 'Expected ca to be included in the customHttpOptions, but it was not.' - ); - } - }); }); describe('with networkTraffic turned off', function () { @@ -346,9 +329,9 @@ describe('CompassAuthServiceMain', function () { CompassAuthService['currentUser'] = { sub: '1234', } as any; - await CompassAuthService.init(preferences); + await CompassAuthService.init(preferences, {} as any); CompassAuthService['config'] = defaultConfig; - expect(getListenerCount(logger)).to.eq(27); + expect(getListenerCount(logger)).to.eq(30); // We did all preparations, reset sinon history for easier assertions sandbox.resetHistory(); diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 61a993e9d08..028a04ece9b 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -11,11 +11,7 @@ import { hookLoggerToMongoLogWriter as oidcPluginHookLoggerToMongoLogWriter, } from '@mongodb-js/oidc-plugin'; import { oidcServerRequestHandler } from '@mongodb-js/devtools-connect'; -import { systemCertsAsync } from 'system-ca'; -import type { Options as SystemCAOptions } from 'system-ca'; -import type { RequestInfo, RequestInit, Response } from 'node-fetch'; -import https from 'https'; -import nodeFetch from 'node-fetch'; +import type { Agent } from 'https'; import type { IntrospectInfo, AtlasUserInfo, AtlasServiceConfig } from './util'; import { throwIfAborted } from '@mongodb-js/compass-utils'; import type { HadronIpcMain } from 'hadron-ipc'; @@ -27,6 +23,7 @@ import { OidcPluginLogger } from './oidc-plugin-logger'; import { spawn } from 'child_process'; import { getAtlasConfig } from './util'; import { createIpcTrack } from '@mongodb-js/compass-telemetry'; +import type { RequestInit, Response } from '@mongodb-js/devtools-proxy-support'; const { log } = createLogger('COMPASS-ATLAS-SERVICE'); const track = createIpcTrack(); @@ -36,20 +33,16 @@ const redirectRequestHandler = oidcServerRequestHandler.bind(null, { productDocsLink: 'https://www.mongodb.com/docs/compass', }); -async function getSystemCA() { - // It is possible for OIDC login flow to fail if system CA certs are different from - // the ones packaged with the application. To avoid this, we include the system CA - // certs in the OIDC plugin options. See COMPASS-7950 for more details. - const systemCAOpts: SystemCAOptions = { includeNodeCertificates: true }; - const ca = await systemCertsAsync(systemCAOpts); - return ca.join('\n'); -} - const TOKEN_TYPE_TO_HINT = { accessToken: 'access_token', refreshToken: 'refresh_token', } as const; +interface CompassAuthHTTPClient { + agent: Agent | undefined; + fetch: (url: string, init: RequestInit) => Promise; +} + export class CompassAuthService { private constructor() { // singleton @@ -57,6 +50,8 @@ export class CompassAuthService { private static initPromise: Promise | null = null; + private static httpClient: CompassAuthHTTPClient; + private static oidcPluginLogger = new OidcPluginLogger(); private static plugin: MongoDBOIDCPlugin | null = null; @@ -66,7 +61,7 @@ export class CompassAuthService { private static signInPromise: Promise | null = null; private static fetch = async ( - url: RequestInfo, + url: string, init: RequestInit = {} ): Promise => { await this.initPromise; @@ -79,15 +74,7 @@ export class CompassAuthService { { url } ); try { - const res = await nodeFetch(url, { - // Tests use 'http'. - ...(url.toString().includes('https') - ? { - agent: new https.Agent({ - ca: await getSystemCA(), - }), - } - : {}), + const res = await this.httpClient.fetch(url, { ...init, headers: { ...init.headers, @@ -145,7 +132,7 @@ export class CompassAuthService { private static createMongoDBOIDCPlugin = createMongoDBOIDCPlugin; - private static async setupPlugin(serializedState?: string) { + private static setupPlugin(serializedState?: string) { this.plugin = this.createMongoDBOIDCPlugin({ redirectServerRequestHandler: (data) => { if (data.result === 'redirecting') { @@ -167,7 +154,7 @@ export class CompassAuthService { logger: this.oidcPluginLogger, serializedState, customHttpOptions: { - ca: await getSystemCA(), + agent: this.httpClient.agent, }, }); oidcPluginHookLoggerToMongoLogWriter( @@ -177,7 +164,11 @@ export class CompassAuthService { ); } - static init(preferences: PreferencesAccess): Promise { + static init( + preferences: PreferencesAccess, + httpClient: CompassAuthHTTPClient + ): Promise { + this.httpClient = httpClient; this.preferences = preferences; this.config = getAtlasConfig(preferences); return (this.initPromise ??= (async () => { @@ -199,7 +190,7 @@ export class CompassAuthService { { config: this.config } ); const serializedState = await this.secretStore.getState(); - await this.setupPlugin(serializedState); + this.setupPlugin(serializedState); })()); } @@ -318,7 +309,7 @@ export class CompassAuthService { this.attachOidcPluginLoggerEvents(); // Destroy old plugin and setup new one await this.plugin?.destroy(); - await this.setupPlugin(); + this.setupPlugin(); // Revoke tokens. Revoking refresh token will also revoke associated access // tokens // https://developer.okta.com/docs/guides/revoke-tokens/main/#revoke-an-access-token-or-a-refresh-token diff --git a/packages/bson-transpilers/package.json b/packages/bson-transpilers/package.json index 23052f02edd..311a987c02e 100644 --- a/packages/bson-transpilers/package.json +++ b/packages/bson-transpilers/package.json @@ -1,6 +1,6 @@ { "name": "bson-transpilers", - "version": "3.0.6", + "version": "3.0.7", "apiVersion": "0.0.1", "description": "Source to source compilers using ANTLR", "contributors": [ @@ -32,7 +32,7 @@ }, "license": "SSPL", "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "chai": "^4.3.4", "depcheck": "^1.4.1", "eslint": "^7.25.0", diff --git a/packages/collection-model/package.json b/packages/collection-model/package.json index 0af261db1f3..8b3598524ac 100644 --- a/packages/collection-model/package.json +++ b/packages/collection-model/package.json @@ -2,7 +2,7 @@ "name": "mongodb-collection-model", "description": "MongoDB collection model", "author": "Lucas Hrabovsky ", - "version": "5.22.3", + "version": "5.23.0", "bugs": { "url": "https://jira.mongodb.org/projects/COMPASS/issues", "email": "compass@mongodb.com" @@ -31,11 +31,11 @@ "dependencies": { "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "electron-mocha": "^12.2.0", diff --git a/packages/compass-aggregations/package.json b/packages/compass-aggregations/package.json index 6a67eba7676..292e9e22e1b 100644 --- a/packages/compass-aggregations/package.json +++ b/packages/compass-aggregations/package.json @@ -2,7 +2,7 @@ "name": "@mongodb-js/compass-aggregations", "description": "Compass Aggregation Pipeline Builder", "private": true, - "version": "9.40.0", + "version": "9.41.0", "main": "dist/index.js", "compass:main": "src/index.ts", "types": "dist/index.d.ts", @@ -32,8 +32,8 @@ }, "license": "SSPL", "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -61,34 +61,34 @@ "@dnd-kit/core": "^6.0.7", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/explain-plan-helper": "^1.1.15", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/explain-plan-helper": "^1.2.0", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/shell-bson-parser": "^1.1.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", diff --git a/packages/compass-aggregations/src/components/aggregation-side-panel/index.spec.tsx b/packages/compass-aggregations/src/components/aggregation-side-panel/index.spec.tsx index 36797c39374..4370ddc1018 100644 --- a/packages/compass-aggregations/src/components/aggregation-side-panel/index.spec.tsx +++ b/packages/compass-aggregations/src/components/aggregation-side-panel/index.spec.tsx @@ -1,28 +1,25 @@ import React from 'react'; import type { ComponentProps } from 'react'; import { AggregationSidePanel } from './index'; -import { cleanup, render, screen } from '@testing-library/react'; +import { cleanup, screen } from '@testing-library/react'; import { DndContext } from '@dnd-kit/core'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; -import configureStore from '../../../test/configure-store'; -import { Provider } from 'react-redux'; +import { renderWithStore } from '../../../test/configure-store'; import sinon from 'sinon'; import { STAGE_WIZARD_USE_CASES } from './stage-wizard-use-cases'; const renderAggregationSidePanel = ( props: Partial> = {} ) => { - return render( - - - {}} - onCloseSidePanel={() => {}} - {...props} - /> - - + return renderWithStore( + + {}} + onCloseSidePanel={() => {}} + {...props} + /> + ); }; @@ -30,31 +27,31 @@ describe('aggregation side panel', function () { afterEach(cleanup); describe('header', function () { - it('renders title', function () { - renderAggregationSidePanel(); + it('renders title', async function () { + await renderAggregationSidePanel(); expect(screen.getByText('Stage Wizard')).to.exist; }); - it('renders close button', function () { - renderAggregationSidePanel(); + it('renders close button', async function () { + await renderAggregationSidePanel(); expect(screen.getByLabelText('Hide Stage Wizard')).to.exist; }); - it('calls onCloseSidePanel when close button is clicked', function () { + it('calls onCloseSidePanel when close button is clicked', async function () { const onCloseSidePanel = sinon.spy(); - renderAggregationSidePanel({ onCloseSidePanel }); + await renderAggregationSidePanel({ onCloseSidePanel }); screen.getByLabelText('Hide Stage Wizard').click(); expect(onCloseSidePanel).to.have.been.calledOnce; }); }); - it('renders a search input', function () { - renderAggregationSidePanel(); + it('renders a search input', async function () { + await renderAggregationSidePanel(); expect(screen.getByRole('search')).to.not.throw; }); - it('renders all the usecases', function () { - renderAggregationSidePanel(); + it('renders all the usecases', async function () { + await renderAggregationSidePanel(); expect( screen .getByTestId('side-panel-content') @@ -62,8 +59,8 @@ describe('aggregation side panel', function () { ).to.have.lengthOf(STAGE_WIZARD_USE_CASES.length); }); - it('renders usecases filtered by search text matching the title of the usecases', function () { - renderAggregationSidePanel(); + it('renders usecases filtered by search text matching the title of the usecases', async function () { + await renderAggregationSidePanel(); const searchBox = screen.getByPlaceholderText(/Search for a Stage/i); userEvent.type(searchBox, 'Sort'); expect( @@ -74,8 +71,8 @@ describe('aggregation side panel', function () { expect(screen.getByTestId('use-case-sort')).to.not.throw; }); - it('renders usecases filtered by search text matching the stage operator of the usecases', function () { - renderAggregationSidePanel(); + it('renders usecases filtered by search text matching the stage operator of the usecases', async function () { + await renderAggregationSidePanel(); const searchBox = screen.getByPlaceholderText(/Search for a Stage/i); userEvent.type(searchBox, 'lookup'); expect( @@ -95,9 +92,9 @@ describe('aggregation side panel', function () { expect(screen.getByTestId('use-case-lookup')).to.not.throw; }); - it('calls onSelectUseCase when a use case is clicked', function () { + it('calls onSelectUseCase when a use case is clicked', async function () { const onSelectUseCase = sinon.spy(); - renderAggregationSidePanel({ onSelectUseCase }); + await renderAggregationSidePanel({ onSelectUseCase }); screen.getByTestId('use-case-sort').click(); expect(onSelectUseCase).to.have.been.calledOnceWith('sort', '$sort'); }); diff --git a/packages/compass-aggregations/src/components/aggregations/aggregations.spec.tsx b/packages/compass-aggregations/src/components/aggregations/aggregations.spec.tsx index 892b463c674..11bb59f633c 100644 --- a/packages/compass-aggregations/src/components/aggregations/aggregations.spec.tsx +++ b/packages/compass-aggregations/src/components/aggregations/aggregations.spec.tsx @@ -1,32 +1,26 @@ import React from 'react'; -import { mount } from 'enzyme'; import { expect } from 'chai'; import Aggregations from '.'; -import configureStore from '../../../test/configure-store'; -import { Provider } from 'react-redux'; +import { renderWithStore } from '../../../test/configure-store'; +import { cleanup, screen } from '@testing-library/react'; describe('Aggregations [Component]', function () { - let component: ReturnType | null; - - beforeEach(function () { - component = mount( - - - + beforeEach(async function () { + await renderWithStore( + ); }); afterEach(function () { - component?.unmount(); - component = null; + cleanup(); }); it('renders the correct root classname', function () { - expect(component?.find(`[data-testid="compass-aggregations"]`)).exist; + expect(screen.getByTestId('compass-aggregations')).exist; }); }); diff --git a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-editor.spec.tsx b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-editor.spec.tsx index ea996661234..3616031c52c 100644 --- a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-editor.spec.tsx +++ b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-editor.spec.tsx @@ -1,29 +1,25 @@ import React from 'react'; import type { ComponentProps } from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { FocusModeStageEditor } from './focus-mode-stage-editor'; const renderFocusModeStageEditor = ( props: Partial> = {} ) => { - render( - - - + return renderWithStore( + , + { + pipeline: [{ $match: { _id: 1 } }, { $limit: 10 }, { $out: 'out' }], + } ); }; describe('FocusMode', function () { - it('does not render editor when stage index is -1', function () { - renderFocusModeStageEditor({ index: -1 }); + it('does not render editor when stage index is -1', async function () { + await renderFocusModeStageEditor({ index: -1 }); expect(() => { screen.getByTestId('stage-operator-combobox'); }).to.throw; @@ -33,8 +29,8 @@ describe('FocusMode', function () { }); context('when operator is not defined', function () { - beforeEach(function () { - renderFocusModeStageEditor({ + beforeEach(async function () { + await renderFocusModeStageEditor({ index: 0, operator: null, }); @@ -53,8 +49,8 @@ describe('FocusMode', function () { }); context('when operator is defined', function () { - beforeEach(function () { - renderFocusModeStageEditor({ + beforeEach(async function () { + await renderFocusModeStageEditor({ index: 0, operator: '$limit', }); diff --git a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx index f94d5523087..7fe2e8f8d8a 100644 --- a/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx +++ b/packages/compass-aggregations/src/components/focus-mode/focus-mode-stage-preview.spec.tsx @@ -1,9 +1,8 @@ import React, { type ComponentProps } from 'react'; import HadronDocument from 'hadron-document'; import type { Document } from 'mongodb'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; import { FocusModePreview, InputPreview, @@ -14,7 +13,7 @@ import { OUT_STAGE_PREVIEW_TEXT, } from '../../constants'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; const DEFAULT_PIPELINE: Document[] = [{ $match: { _id: 1 } }, { $limit: 10 }]; @@ -22,53 +21,52 @@ const renderFocusModePreview = ( props: Partial> = {}, pipeline = DEFAULT_PIPELINE ) => { - render( - - {}} - onCollapse={() => {}} - {...props} - /> - + return renderWithStore( + {}} + onCollapse={() => {}} + {...props} + />, + { pipeline } ); }; describe('FocusModeStagePreview', function () { - it('renders stage input', function () { - render( {}} onCollapse={() => {}} />); + it('renders stage input', async function () { + await renderWithStore( + {}} onCollapse={() => {}} /> + ); const preview = screen.getByTestId('focus-mode-stage-preview'); expect(preview).to.exist; expect(within(preview).getByText(/stage input/i)).to.exist; }); - it('renders stage output', function () { - render( {}} onCollapse={() => {}} />); + it('renders stage output', async function () { + await renderWithStore( + {}} onCollapse={() => {}} /> + ); const preview = screen.getByTestId('focus-mode-stage-preview'); expect(preview).to.exist; expect(within(preview).getByText(/stage output/i)).to.exist; }); context('FocusModePreview', function () { - it('renders loader', function () { - renderFocusModePreview({ + it('renders loader', async function () { + await renderFocusModePreview({ isLoading: true, }); const preview = screen.getByTestId('focus-mode-stage-preview'); expect(preview).to.exist; expect(within(preview).getByTitle(/loading/i)).to.exist; }); - it('renders list of documents', function () { - renderFocusModePreview({ + it('renders list of documents', async function () { + await renderFocusModePreview({ isLoading: false, documents: [ new HadronDocument({ _id: 12345 }), @@ -79,8 +77,8 @@ describe('FocusModeStagePreview', function () { expect(within(preview).getByText(/12345/i)).to.exist; expect(within(preview).getByText(/54321/i)).to.exist; }); - it('renders no preview documents when its not loading and documents are empty', function () { - renderFocusModePreview({ + it('renders no preview documents when its not loading and documents are empty', async function () { + await renderFocusModePreview({ documents: [], isLoading: false, }); @@ -88,8 +86,8 @@ describe('FocusModeStagePreview', function () { const preview = screen.getByTestId('focus-mode-stage-preview'); expect(within(preview).getByText(/no preview documents/i)).to.exist; }); - it('renders $out stage preview', function () { - renderFocusModePreview( + it('renders $out stage preview', async function () { + await renderFocusModePreview( { stageOperator: '$out', stageIndex: 1, @@ -99,8 +97,8 @@ describe('FocusModeStagePreview', function () { const preview = screen.getByTestId('focus-mode-stage-preview'); expect(within(preview).getByText(OUT_STAGE_PREVIEW_TEXT)).to.exist; }); - it('renders $merge stage preview', function () { - renderFocusModePreview( + it('renders $merge stage preview', async function () { + await renderFocusModePreview( { stageOperator: '$merge', stageIndex: 1, @@ -110,8 +108,8 @@ describe('FocusModeStagePreview', function () { const preview = screen.getByTestId('focus-mode-stage-preview'); expect(within(preview).getByText(MERGE_STAGE_PREVIEW_TEXT)).to.exist; }); - it('renders atlas stage preview', function () { - renderFocusModePreview({ + it('renders atlas stage preview', async function () { + await renderFocusModePreview({ stageOperator: '$search', stageIndex: 2, isMissingAtlasOnlyStageSupport: true, diff --git a/packages/compass-aggregations/src/components/focus-mode/focus-mode.spec.tsx b/packages/compass-aggregations/src/components/focus-mode/focus-mode.spec.tsx index 233da5f7375..bf372910a76 100644 --- a/packages/compass-aggregations/src/components/focus-mode/focus-mode.spec.tsx +++ b/packages/compass-aggregations/src/components/focus-mode/focus-mode.spec.tsx @@ -1,43 +1,46 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { cleanup, screen, waitFor } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import FocusMode from './focus-mode'; import { disableFocusMode, enableFocusMode } from '../../modules/focus-mode'; -const renderFocusMode = () => { - const store = configureStore({ +const renderFocusMode = async () => { + const result = await renderWithStore(, { pipeline: [{ $match: { _id: 1 } }, { $limit: 10 }, { $out: 'out' }], }); - render( - - - - ); - return store; + return result.plugin.store; }; describe('FocusMode', function () { - it('does not show modal when closed', function () { - const store = renderFocusMode(); + afterEach(cleanup); + + it('does not show modal when closed', async function () { + const store = await renderFocusMode(); store.dispatch(disableFocusMode() as any); - expect(() => { - screen.getByTestId('focus-mode-modal'); - }).to.throw; + await waitFor(() => { + expect(() => { + screen.getByTestId('focus-mode-modal'); + }).to.throw; + }); }); - it('shows modal when open', function () { - const store = renderFocusMode(); - store.dispatch(enableFocusMode(0) as any); - expect(screen.getByTestId('focus-mode-modal')).to.exist; + it('shows modal when open', async function () { + const store = await renderFocusMode(); + store.dispatch(enableFocusMode(0)); + await waitFor(() => { + expect(screen.getByTestId('focus-mode-modal')).to.exist; + }); }); - it('hides modal when close button is clicked', function () { - const store = renderFocusMode(); + it('hides modal when close button is clicked', async function () { + const store = await renderFocusMode(); store.dispatch(enableFocusMode(0) as any); - screen.getByLabelText(/close modal/i).click(); + + await waitFor(() => { + screen.getByLabelText(/close modal/i).click(); + }); expect(() => { screen.getByTestId('focus-mode-modal'); diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/index.spec.tsx index 0e15d165d6a..a6afa2675f0 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/index.spec.tsx @@ -1,43 +1,39 @@ import React from 'react'; -import { Provider } from 'react-redux'; import type { ComponentProps } from 'react'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, screen, within } from '@testing-library/react'; import { expect } from 'chai'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { PipelineBuilderWorkspace } from '.'; import { toggleSidePanel } from '../../modules/side-panel'; -const renderBuilderWorkspace = ( +const renderBuilderWorkspace = async ( props: Partial> = {} ) => { - const store = configureStore(); - render( - - - + const result = await renderWithStore( + ); - return store; + return result.plugin.store; }; describe('PipelineBuilderWorkspace', function () { afterEach(cleanup); - it('renders builder ui workspace', function () { - renderBuilderWorkspace({ pipelineMode: 'builder-ui' }); + it('renders builder ui workspace', async function () { + await renderBuilderWorkspace({ pipelineMode: 'builder-ui' }); const container = screen.getByTestId('pipeline-builder-workspace'); expect(within(container).getByTestId('pipeline-builder-ui-workspace')).to .exist; }); - it('renders as text workspace', function () { - renderBuilderWorkspace({ pipelineMode: 'as-text' }); + it('renders as text workspace', async function () { + await renderBuilderWorkspace({ pipelineMode: 'as-text' }); const container = screen.getByTestId('pipeline-builder-workspace'); expect(within(container).getByTestId('pipeline-as-text-workspace')).to .exist; }); - it('renders side panel when enabled in builder ui mode', function () { - const store = renderBuilderWorkspace({ pipelineMode: 'builder-ui' }); + it('renders side panel when enabled in builder ui mode', async function () { + const store = await renderBuilderWorkspace({ pipelineMode: 'builder-ui' }); store.dispatch(toggleSidePanel() as any); const container = screen.getByTestId('pipeline-builder-workspace'); expect(() => { @@ -45,8 +41,8 @@ describe('PipelineBuilderWorkspace', function () { }).to.not.throw; }); - it('does not render side panel when enabled in as text mode', function () { - const store = renderBuilderWorkspace({ pipelineMode: 'as-text' }); + it('does not render side panel when enabled in as text mode', async function () { + const store = await renderBuilderWorkspace({ pipelineMode: 'as-text' }); store.dispatch(toggleSidePanel() as any); const container = screen.getByTestId('pipeline-builder-workspace'); expect(() => { diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/index.spec.tsx index 1c3607c0c2a..5a3b53ccf8c 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/index.spec.tsx @@ -1,32 +1,29 @@ import React from 'react'; import type { ComponentProps } from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelineAsTextWorkspace } from '.'; const renderPipelineAsTextWorkspace = ( props: Partial> = {} ) => { - render( - - - + return renderWithStore( + ); }; describe('PipelineAsTextWorkspace', function () { - it('renders text workspace', function () { - renderPipelineAsTextWorkspace({}); + it('renders text workspace', async function () { + await renderPipelineAsTextWorkspace(); const container = screen.getByTestId('pipeline-as-text-workspace'); expect(container).to.exist; }); - it('does not render preview panel when disabled', function () { - renderPipelineAsTextWorkspace({ isAutoPreview: false }); + it('does not render preview panel when disabled', async function () { + await renderPipelineAsTextWorkspace({ isAutoPreview: false }); expect(() => { screen.getByTestId('pipeline-as-text-preview'); }).to.throw; diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-editor.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-editor.spec.tsx index d9bb32d7638..c15eb987e13 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-editor.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-editor.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; import type { ComponentProps } from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { MongoServerError } from 'mongodb'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelineEditor } from './pipeline-editor'; import { PipelineParserError } from '../../../modules/pipeline-builder/pipeline-parser/utils'; @@ -13,31 +12,29 @@ import { PipelineParserError } from '../../../modules/pipeline-builder/pipeline- const renderPipelineEditor = ( props: Partial> = {} ) => { - render( - - {}} - num_stages={1} - {...props} - /> - + return renderWithStore( + {}} + num_stages={1} + {...props} + /> ); }; describe('PipelineEditor', function () { - it('renders editor workspace', function () { - renderPipelineEditor({}); + it('renders editor workspace', async function () { + await renderPipelineEditor({}); const container = screen.getByTestId('pipeline-as-text-editor'); expect(container).to.exist; }); - it('renders server error', function () { - renderPipelineEditor({ + it('renders server error', async function () { + await renderPipelineEditor({ serverError: new MongoServerError({ message: 'Can not use out' }), }); const container = screen.getByTestId('pipeline-as-text-editor'); @@ -46,8 +43,8 @@ describe('PipelineEditor', function () { expect(within(container).findByText(/Can not use out/)).to.exist; }); - it('renders syntax error', function () { - renderPipelineEditor({ + it('renders syntax error', async function () { + await renderPipelineEditor({ syntaxErrors: [new PipelineParserError('invalid pipeline')], }); const container = screen.getByTestId('pipeline-as-text-editor'); diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx index 0903234872d..8b649503a09 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-preview.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; import type { ComponentProps } from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelinePreview } from './pipeline-preview'; import HadronDocument from 'hadron-document'; @@ -14,38 +13,37 @@ const renderPipelineEditor = ( props: Partial> = {}, storeOptions: any = {} ) => { - render( - - {}} - onCollapse={() => {}} - {...props} - /> - + return renderWithStore( + {}} + onCollapse={() => {}} + {...props} + />, + storeOptions ); }; describe('PipelinePreview', function () { - it('renders editor workspace', function () { - renderPipelineEditor({}); + it('renders editor workspace', async function () { + await renderPipelineEditor({}); const container = screen.getByTestId('pipeline-as-text-preview'); expect(container).to.exist; }); - it('renders header', function () { - renderPipelineEditor({}); + it('renders header', async function () { + await renderPipelineEditor({}); expect(screen.getByText(/Pipeline Output/)).to.exist; }); - it('renders text when pipeline is not run yet', function () { - renderPipelineEditor({ previewDocs: null }); + it('renders text when pipeline is not run yet', async function () { + await renderPipelineEditor({ previewDocs: null }); expect( screen.getByText( /Preview results to see a sample of the aggregated results from this pipeline./ @@ -53,13 +51,13 @@ describe('PipelinePreview', function () { ).to.exist; }); - it('renders text when preview docs are empty', function () { - renderPipelineEditor({ previewDocs: [] }); + it('renders text when preview docs are empty', async function () { + await renderPipelineEditor({ previewDocs: [] }); expect(screen.getByText(/No preview documents/)).to.exist; }); - it('renders document list', function () { - renderPipelineEditor({ + it('renders document list', async function () { + await renderPipelineEditor({ previewDocs: [{ _id: 1 }, { _id: 2 }].map( (doc) => new HadronDocument(doc) ), @@ -70,7 +68,7 @@ describe('PipelinePreview', function () { ).to.have.lengthOf(2); }); - it('renders pipeline output menu', function () { + it('renders pipeline output menu', async function () { const previewDocs = [ new HadronDocument({ _id: 1, @@ -88,7 +86,7 @@ describe('PipelinePreview', function () { ], }), ]; - renderPipelineEditor({ + await renderPipelineEditor({ previewDocs, onExpand: () => { previewDocs[0].expand(); @@ -150,8 +148,8 @@ describe('PipelinePreview', function () { expect(() => within(docList).getByText(/document/)).to.throw; }); - it('renders output stage preview', function () { - renderPipelineEditor( + it('renders output stage preview', async function () { + await renderPipelineEditor( { previewDocs: [{ _id: 1 }, { _id: 2 }, { _id: 3 }].map( (doc) => new HadronDocument(doc) @@ -170,8 +168,8 @@ describe('PipelinePreview', function () { expect(within(container).getByTestId('output-stage-preview')).to.exist; }); - it('renders atlas stage preview', function () { - renderPipelineEditor({ + it('renders atlas stage preview', async function () { + await renderPipelineEditor({ isMissingAtlasSupport: true, atlasOperator: '$search', }); @@ -181,18 +179,18 @@ describe('PipelinePreview', function () { describe('stale preview', function () { const staleMessage = /Output outdated and no longer in sync./; - it('does not render stale banner when preview docs is null', function () { - renderPipelineEditor({ isPreviewStale: true, previewDocs: null }); + it('does not render stale banner when preview docs is null', async function () { + await renderPipelineEditor({ isPreviewStale: true, previewDocs: null }); expect(screen.queryByText(staleMessage)).to.not.exist; }); - it('does not render stale banner when preview docs is empty', function () { - renderPipelineEditor({ isPreviewStale: true, previewDocs: [] }); + it('does not render stale banner when preview docs is empty', async function () { + await renderPipelineEditor({ isPreviewStale: true, previewDocs: [] }); expect(screen.queryByText(staleMessage)).to.not.exist; }); - it('renders stale banner when preview is stale', function () { - renderPipelineEditor({ + it('renders stale banner when preview is stale', async function () { + await renderPipelineEditor({ isPreviewStale: true, previewDocs: [new HadronDocument({ _id: 1 })], }); diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-stages-preview.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-stages-preview.spec.tsx index 85519632627..c3e91763928 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-stages-preview.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-as-text-workspace/pipeline-stages-preview.spec.tsx @@ -1,36 +1,33 @@ import React from 'react'; import type { ComponentProps } from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; import sinon from 'sinon'; -import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { OutputStagePreview } from './pipeline-stages-preview'; import type { PreferencesAccess } from 'compass-preferences-model'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; describe('OutputStagePreview', function () { let preferences: PreferencesAccess; const renderStageBanner = ( props: Partial> = {} ) => { - render( - - - {}} - onSaveCollection={() => {}} - {...props} - /> - - + return renderWithStore( + {}} + onSaveCollection={() => {}} + {...props} + />, + undefined, + undefined, + { preferences } ); }; @@ -40,8 +37,8 @@ describe('OutputStagePreview', function () { (['$out', '$merge'] as const).forEach((stageOperator) => { describe(`${stageOperator} with run aggregation enabled`, function () { - it('renders stage banner', function () { - renderStageBanner({ stageOperator }); + it('renders stage banner', async function () { + await renderStageBanner({ stageOperator }); expect(screen.getByTestId(`${stageOperator}-preview-banner`)).to.exist; expect(() => { screen.getByRole('button', { @@ -58,15 +55,15 @@ describe('OutputStagePreview', function () { }); }); - it(`renders stage banner`, function () { - renderStageBanner({ + it(`renders stage banner`, async function () { + await renderStageBanner({ stageOperator, }); expect(screen.getByTestId(`${stageOperator}-preview-banner`)).to.exist; }); - it(`renders stage action`, function () { - renderStageBanner({ + it(`renders stage action`, async function () { + await renderStageBanner({ stageOperator, }); expect( @@ -76,9 +73,9 @@ describe('OutputStagePreview', function () { ).to.exist; }); - it(`calls stage action on click`, function () { + it(`calls stage action on click`, async function () { const onSaveCollection = sinon.spy(); - renderStageBanner({ + await renderStageBanner({ stageOperator, onSaveCollection, }); @@ -90,8 +87,8 @@ describe('OutputStagePreview', function () { expect(onSaveCollection.calledOnce).to.be.true; }); - it('renders loading state', function () { - renderStageBanner({ + it('renders loading state', async function () { + await renderStageBanner({ stageOperator, isLoading: true, }); @@ -101,8 +98,8 @@ describe('OutputStagePreview', function () { expect(button.getAttribute('aria-disabled')).to.equal('true'); }); - it('renders complete state', function () { - renderStageBanner({ + it('renders complete state', async function () { + await renderStageBanner({ stageOperator, isComplete: true, }); @@ -110,8 +107,8 @@ describe('OutputStagePreview', function () { .exist; }); - it('renders complete state action button', function () { - renderStageBanner({ + it('renders complete state action button', async function () { + await renderStageBanner({ stageOperator, isComplete: true, }); @@ -121,9 +118,9 @@ describe('OutputStagePreview', function () { expect(button).to.exist; }); - it('calls complete state action button', function () { + it('calls complete state action button', async function () { const onOpenCollection = sinon.spy(); - renderStageBanner({ + await renderStageBanner({ stageOperator, isComplete: true, onOpenCollection, diff --git a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/index.spec.tsx index 7725995bdcf..3f427f262af 100644 --- a/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-builder-workspace/pipeline-builder-ui-workspace/index.spec.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { cleanup, render, screen } from '@testing-library/react'; +import { cleanup, screen } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import PipelineBuilderUIWorkspace from '.'; const SOURCE_PIPELINE = [ @@ -12,46 +11,36 @@ const SOURCE_PIPELINE = [ ]; const renderPipelineBuilderUIWorkspace = (props = {}, options = {}) => { - render( - - - - ); + return renderWithStore(, { + pipeline: SOURCE_PIPELINE, + ...options, + }); }; describe('PipelineBuilderUIWorkspace [Component]', function () { afterEach(cleanup); - it('renders', function () { - expect(() => renderPipelineBuilderUIWorkspace()).to.not.throw; - }); - context('when pipeline is not empty', function () { - it('renders the stages', function () { - renderPipelineBuilderUIWorkspace(); + it('renders the stages', async function () { + await renderPipelineBuilderUIWorkspace(); expect(screen.getAllByTestId('stage-card')).to.have.lengthOf(3); }); - it('renders add stage icon button between stages', function () { - renderPipelineBuilderUIWorkspace(); + it('renders add stage icon button between stages', async function () { + await renderPipelineBuilderUIWorkspace(); const buttons = screen.getAllByTestId('add-stage-icon-button'); expect(buttons.length).to.equal(3); }); - it('renders add stage button', function () { - renderPipelineBuilderUIWorkspace(); + it('renders add stage button', async function () { + await renderPipelineBuilderUIWorkspace(); const buttons = screen.getAllByTestId('add-stage'); expect(buttons.length).to.equal(1); expect(buttons[0]).to.have.text('Add Stage'); }); - it('adds a stage to the start of pipeline when first icon button is clicked', function () { - renderPipelineBuilderUIWorkspace(); + it('adds a stage to the start of pipeline when first icon button is clicked', async function () { + await renderPipelineBuilderUIWorkspace(); const buttons = screen.getAllByTestId('add-stage-icon-button'); buttons[0].click(); @@ -64,8 +53,8 @@ describe('PipelineBuilderUIWorkspace [Component]', function () { expect(stageNames).to.deep.equal(['', '$match', '$limit', '$out']); }); - it('adds a stage at the correct position of pipeline when last icon button is clicked', function () { - renderPipelineBuilderUIWorkspace(); + it('adds a stage at the correct position of pipeline when last icon button is clicked', async function () { + await renderPipelineBuilderUIWorkspace(); const buttons = screen.getAllByTestId('add-stage-icon-button'); buttons[2].click(); expect(screen.getAllByTestId('stage-card')).to.have.lengthOf(4); @@ -79,8 +68,8 @@ describe('PipelineBuilderUIWorkspace [Component]', function () { expect(stageNames).to.deep.equal(['$match', '$limit', '', '$out']); }); - it('adds a stage at the end when (text) add stage button is clicked', function () { - renderPipelineBuilderUIWorkspace(); + it('adds a stage at the end when (text) add stage button is clicked', async function () { + await renderPipelineBuilderUIWorkspace(); const button = screen.getByTestId('add-stage'); button.click(); @@ -92,25 +81,25 @@ describe('PipelineBuilderUIWorkspace [Component]', function () { }); context('when pipeline is empty', function () { - it('does not render any stage', function () { - renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); + it('does not render any stage', async function () { + await renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); expect(screen.queryByTestId('stage-card')).to.not.exist; }); - it('does not render icon buttons', function () { - renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); + it('does not render icon buttons', async function () { + await renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); expect(screen.queryByTestId('add-stage-icon-button')).to.not.exist; }); - it('renders (text) add stage button', function () { - renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); + it('renders (text) add stage button', async function () { + await renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); const button = screen.getByTestId('add-stage'); expect(button).to.exist; expect(button).to.have.text('Add Stage'); }); - it('adds a stage when (text) button is clicked', function () { - renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); + it('adds a stage when (text) button is clicked', async function () { + await renderPipelineBuilderUIWorkspace({}, { pipeline: [] }); const button = screen.getByTestId('add-stage'); button.click(); expect(screen.getAllByTestId('stage-card')).to.have.lengthOf(1); diff --git a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx index 02358ee5ff9..b8593d23c8c 100644 --- a/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-results-workspace/index.spec.tsx @@ -1,52 +1,49 @@ import HadronDocument from 'hadron-document'; import type { ComponentProps } from 'react'; import React from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { expect } from 'chai'; import { spy } from 'sinon'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { PipelineResultsWorkspace } from './index'; const renderPipelineResultsWorkspace = ( props: Partial> = {} ) => { - render( - - {}} - onCancel={() => {}} - resultsViewType={'document'} - {...props} - /> - + return renderWithStore( + {}} + onCancel={() => {}} + resultsViewType={'document'} + {...props} + /> ); }; describe('PipelineResultsWorkspace', function () { - it('renders loading state', function () { - renderPipelineResultsWorkspace({ isLoading: true }); + it('renders loading state', async function () { + await renderPipelineResultsWorkspace({ isLoading: true }); const container = screen.getByTestId('pipeline-results-workspace'); expect(container).to.exist; expect(within(container).getByTestId('pipeline-results-loader')).to.exist; }); - it('renders empty results state', function () { - renderPipelineResultsWorkspace({ isEmpty: true }); + it('renders empty results state', async function () { + await renderPipelineResultsWorkspace({ isEmpty: true }); const container = screen.getByTestId('pipeline-results-workspace'); expect(container).to.exist; expect(within(container).getByTestId('pipeline-empty-results')).to.exist; }); - it('renders documents', function () { - renderPipelineResultsWorkspace({ + it('renders documents', async function () { + await renderPipelineResultsWorkspace({ documents: [ new HadronDocument({ id: '1' }), new HadronDocument({ id: '2' }), @@ -58,9 +55,12 @@ describe('PipelineResultsWorkspace', function () { ).to.have.lengthOf(2); }); - it('calls cancel when user stop aggregation', function () { + it('calls cancel when user stop aggregation', async function () { const onCancelSpy = spy(); - renderPipelineResultsWorkspace({ isLoading: true, onCancel: onCancelSpy }); + await renderPipelineResultsWorkspace({ + isLoading: true, + onCancel: onCancelSpy, + }); const container = screen.getByTestId('pipeline-results-workspace'); expect(container).to.exist; userEvent.click(within(container).getByText('Stop'), undefined, { @@ -69,9 +69,9 @@ describe('PipelineResultsWorkspace', function () { expect(onCancelSpy.calledOnce).to.be.true; }); - it('should render error banner', function () { + it('should render error banner', async function () { const onRetry = spy(); - renderPipelineResultsWorkspace({ + await renderPipelineResultsWorkspace({ isError: true, error: 'Something bad happened', onRetry, @@ -83,9 +83,9 @@ describe('PipelineResultsWorkspace', function () { expect(onRetry).to.be.calledOnce; }); - it('should render $out / $merge result screen', function () { + it('should render $out / $merge result screen', async function () { const onOutClick = spy(); - renderPipelineResultsWorkspace({ + await renderPipelineResultsWorkspace({ isMergeOrOutPipeline: true, mergeOrOutDestination: 'foo.bar', onOutClick, diff --git a/packages/compass-aggregations/src/components/pipeline-results-workspace/pipeline-pagination.spec.tsx b/packages/compass-aggregations/src/components/pipeline-results-workspace/pipeline-pagination.spec.tsx index 0bb9824b21c..a7b6f87a8d1 100644 --- a/packages/compass-aggregations/src/components/pipeline-results-workspace/pipeline-pagination.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-results-workspace/pipeline-pagination.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { expect } from 'chai'; import { spy } from 'sinon'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { PipelinePagination, @@ -14,26 +13,24 @@ import { } from './pipeline-pagination'; const renderPipelinePagination = (props: Record = {}) => { - render( - - {}} - onNext={() => {}} - {...props} - /> - + return renderWithStore( + {}} + onNext={() => {}} + {...props} + /> ); }; describe('PipelinePagination', function () { describe('PipelinePagination Component', function () { - it('renders correctly', function () { - renderPipelinePagination(); + it('renders correctly', async function () { + await renderPipelinePagination(); const container = screen.getByTestId('pipeline-pagination'); expect( within(container).getByTestId('pipeline-pagination-desc').textContent @@ -45,15 +42,18 @@ describe('PipelinePagination', function () { expect(within(container).getByTestId('pipeline-pagination-next-action')) .to.exist; }); - it('does not render desc when disabled', function () { - renderPipelinePagination({ isCountDisabled: true }); + it('does not render desc when disabled', async function () { + await renderPipelinePagination({ isCountDisabled: true }); const container = screen.getByTestId('pipeline-pagination'); expect(() => { within(container).getByTestId('pipeline-pagination-desc'); }).to.throw; }); - it('renders paginate buttons as disabled when disabled', function () { - renderPipelinePagination({ isPrevDisabled: true, isNextDisabled: true }); + it('renders paginate buttons as disabled when disabled', async function () { + await renderPipelinePagination({ + isPrevDisabled: true, + isNextDisabled: true, + }); const container = screen.getByTestId('pipeline-pagination'); expect( within(container) @@ -66,18 +66,18 @@ describe('PipelinePagination', function () { .getAttribute('aria-disabled') ).to.equal('true'); }); - it('calls onPrev when clicked', function () { + it('calls onPrev when clicked', async function () { const onPrev = spy(); - renderPipelinePagination({ onPrev }); + await renderPipelinePagination({ onPrev }); const container = screen.getByTestId('pipeline-pagination'); userEvent.click( within(container).getByTestId('pipeline-pagination-prev-action') ); expect(onPrev.calledOnce).to.be.true; }); - it('calls onNext when clicked', function () { + it('calls onNext when clicked', async function () { const onNext = spy(); - renderPipelinePagination({ onNext }); + await renderPipelinePagination({ onNext }); const container = screen.getByTestId('pipeline-pagination'); userEvent.click( within(container).getByTestId('pipeline-pagination-next-action') diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/index.spec.tsx index d18f408ac82..579f9448318 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/index.spec.tsx @@ -1,31 +1,25 @@ import React from 'react'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Provider } from 'react-redux'; import { expect } from 'chai'; - -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { PipelineToolbar } from './index'; -import { PipelineStorageProvider } from '@mongodb-js/my-queries-storage/provider'; import { CompassPipelineStorage } from '@mongodb-js/my-queries-storage'; describe('PipelineToolbar', function () { describe('renders with setting row - visible', function () { let toolbar: HTMLElement; - beforeEach(function () { - render( - - - - - + beforeEach(async function () { + await renderWithStore( + , + { pipeline: [{ $match: { _id: 1 } }] }, + undefined, + { pipelineStorage: new CompassPipelineStorage() } ); toolbar = screen.getByTestId('pipeline-toolbar'); }); @@ -125,16 +119,14 @@ describe('PipelineToolbar', function () { }); describe('renders with setting row - hidden', function () { - it('does not render toolbar settings', function () { - render( - - - + it('does not render toolbar settings', async function () { + await renderWithStore( + ); const toolbar = screen.getByTestId('pipeline-toolbar'); expect(() => within(toolbar).getByTestId('pipeline-settings')).to.throw; diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.spec.tsx index 592ec9e72c2..df7f889394f 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-ai.spec.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { cleanup, screen, waitFor } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; import type { PreferencesAccess } from 'compass-preferences-model'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import userEvent from '@testing-library/user-event'; import PipelineAI from './pipeline-ai'; -import configureStore, { +import { MockAtlasAiService, + renderWithStore, } from '../../../test/configure-store'; import { AIPipelineActionTypes, @@ -15,68 +15,44 @@ import { showInput, } from '../../modules/pipeline-builder/pipeline-ai'; import { PreferencesProvider } from 'compass-preferences-model/provider'; -import { - LoggerProvider, - createNoopLogger, -} from '@mongodb-js/compass-logging/provider'; -import { TelemetryProvider } from '@mongodb-js/compass-telemetry/provider'; +import type { AggregationsStore } from '../../stores/store'; +import type Sinon from 'sinon'; const feedbackPopoverTextAreaId = 'feedback-popover-textarea'; const thumbsUpId = 'ai-feedback-thumbs-up'; describe('PipelineAI Component', function () { let preferences: PreferencesAccess; - let store: ReturnType; - let trackingEvents: any[] = []; - const track = (event: any, properties: any) => { - trackingEvents.push({ - event, - properties: typeof properties === 'function' ? properties() : properties, - }); - }; + let store: AggregationsStore; + let track: Sinon.SinonSpy; - const renderPipelineAI = () => { + const renderPipelineAI = async () => { const atlasAiService = new MockAtlasAiService(); - const store = configureStore({}, undefined, { - preferences, - atlasAiService: atlasAiService as any, - }); - render( + const result = await renderWithStore( // TODO(COMPASS-7415): use default values instead of updating values - - - - - - - - + + , + {}, + undefined, + { + preferences, + atlasAiService: atlasAiService as any, + track, + } ); + store = result.plugin.store; + track = result.track; return store; }; beforeEach(async function () { preferences = await createSandboxFromDefaultPreferences(); - store = renderPipelineAI(); + await renderPipelineAI(); await store.dispatch(showInput()); }); afterEach(function () { - trackingEvents = []; - (store as any) = null; cleanup(); }); @@ -131,7 +107,7 @@ describe('PipelineAI Component', function () { beforeEach(async function () { // Elements will render only if `trackUsageStatistics` is true await preferences.savePreferences({ trackUsageStatistics: true }); - store = renderPipelineAI(); + await renderPipelineAI(); await store.dispatch(showInput()); }); @@ -151,6 +127,10 @@ describe('PipelineAI Component', function () { expect(screen.queryByTestId(feedbackPopoverTextAreaId)).to.not.exist; + await waitFor(() => { + screen.getByRole('button', { name: 'Submit positive feedback' }); + }); + userEvent.click( screen.getByRole('button', { name: 'Submit positive feedback' }) ); @@ -162,25 +142,17 @@ describe('PipelineAI Component', function () { userEvent.click(screen.getByRole('button', { name: 'Submit' })); - await waitFor( - () => { - // No feedback popover is shown. - expect(screen.queryByTestId(feedbackPopoverTextAreaId)).to.not - .exist; - expect(trackingEvents).to.deep.equal([ - { - event: 'PipelineAI Feedback', - properties: { - connection_id: 'TEST', - feedback: 'positive', - request_id: 'pineapple', - text: 'this is the pipeline I was looking for', - }, - }, - ]); - }, - { interval: 10 } - ); + await waitFor(() => { + // No feedback popover is shown. + expect(screen.queryByTestId(feedbackPopoverTextAreaId)).to.not.exist; + + expect(track).to.have.been.calledWith('PipelineAI Feedback', { + connection_id: 'TEST', + feedback: 'positive', + request_id: 'pineapple', + text: 'this is the pipeline I was looking for', + }); + }); }); }); @@ -189,7 +161,7 @@ describe('PipelineAI Component', function () { await preferences.savePreferences({ trackUsageStatistics: false, }); - store = renderPipelineAI(); + await renderPipelineAI(); }); it('should not show the feedback items', function () { diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.spec.tsx index f9c72703884..21fd95a339c 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.spec.tsx @@ -1,34 +1,30 @@ import React from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; import { spy } from 'sinon'; import type { SinonSpy } from 'sinon'; -import { Provider } from 'react-redux'; - -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelineHeader } from '.'; -import { PipelineStorageProvider } from '@mongodb-js/my-queries-storage/provider'; import { CompassPipelineStorage } from '@mongodb-js/my-queries-storage'; describe('PipelineHeader', function () { let container: HTMLElement; let onToggleOptionsSpy: SinonSpy; - beforeEach(function () { + beforeEach(async function () { onToggleOptionsSpy = spy(); - render( - - - - - + await renderWithStore( + , + undefined, + undefined, + { pipelineStorage: new CompassPipelineStorage() } ); container = screen.getByTestId('pipeline-header'); }); diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.tsx index 6d02f6633de..59258071920 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/index.tsx @@ -110,6 +110,8 @@ export const PipelineHeader: React.FunctionComponent = ({ isOptionsVisible, isOpenPipelineVisible, }) => { + // TODO: remove direct check for storage existing, breaks single source of + // truth rule and exposes services to UI, this breaks the rules for locators const isSavingAggregationsEnabled = !!usePipelineStorage(); return (
diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx index d9dbde2a2f4..400101b3a8c 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-header/pipeline-actions.spec.tsx @@ -1,12 +1,17 @@ import React from 'react'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { + cleanup, + render, + screen, + waitFor, + within, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; import { spy } from 'sinon'; import type { SinonSpy } from 'sinon'; import ConnectedPipelineActions, { PipelineActions } from './pipeline-actions'; -import configureStore from '../../../../test/configure-store'; -import { Provider } from 'react-redux'; +import { renderWithStore } from '../../../../test/configure-store'; import { changeStageDisabled } from '../../../modules/pipeline-builder/stage-editor'; import { type PreferencesAccess, @@ -230,32 +235,24 @@ describe('PipelineActions', function () { }); describe('with store', function () { - function renderPipelineActions(options = {}) { - const store = configureStore(options); - - const component = ( - - {}} - > - + async function renderPipelineActions(options = {}) { + const result = await renderWithStore( + {}} + >, + options ); - - const result = render(component); return { ...result, - store, - rerender: () => { - result.rerender(component); - }, + store: result.plugin.store, }; } - it('should disable actions when pipeline contains errors', function () { - renderPipelineActions({ pipeline: [42] }); + it('should disable actions when pipeline contains errors', async function () { + await renderPipelineActions({ pipeline: [42] }); expect( screen @@ -276,8 +273,8 @@ describe('PipelineActions', function () { ).to.equal('true'); }); - it('should disable actions while ai is fetching', function () { - const { store, rerender } = renderPipelineActions({ + it('should disable actions while ai is fetching', async function () { + const { store } = await renderPipelineActions({ pipeline: [{ $match: { _id: 1 } }], }); @@ -285,29 +282,30 @@ describe('PipelineActions', function () { type: AIPipelineActionTypes.AIPipelineStarted, requestId: 'pineapples', }); - rerender(); - expect( - screen - .getByTestId('pipeline-toolbar-explain-aggregation-button') - .getAttribute('aria-disabled') - ).to.equal('true'); - - expect( - screen - .getByTestId('pipeline-toolbar-export-aggregation-button') - .getAttribute('aria-disabled') - ).to.equal('true'); - - expect( - screen - .getByTestId('pipeline-toolbar-run-button') - .getAttribute('aria-disabled') - ).to.equal('true'); + await waitFor(() => { + expect( + screen + .getByTestId('pipeline-toolbar-explain-aggregation-button') + .getAttribute('aria-disabled') + ).to.equal('true'); + + expect( + screen + .getByTestId('pipeline-toolbar-export-aggregation-button') + .getAttribute('aria-disabled') + ).to.equal('true'); + + expect( + screen + .getByTestId('pipeline-toolbar-run-button') + .getAttribute('aria-disabled') + ).to.equal('true'); + }); }); - it('should disable export button when pipeline is $out / $merge', function () { - renderPipelineActions({ + it('should disable export button when pipeline is $out / $merge', async function () { + await renderPipelineActions({ pipeline: [{ $out: 'foo' }], }); @@ -318,20 +316,20 @@ describe('PipelineActions', function () { ).to.equal('true'); }); - it('should disable export button when last enabled stage is $out / $merge', function () { - const { store, rerender } = renderPipelineActions({ + it('should disable export button when last enabled stage is $out / $merge', async function () { + const { store } = await renderPipelineActions({ pipeline: [{ $out: 'foo' }, { $match: { _id: 1 } }], }); - store.dispatch(changeStageDisabled(1, true) as any); - - rerender(); + store.dispatch(changeStageDisabled(1, true)); - expect( - screen - .getByTestId('pipeline-toolbar-export-aggregation-button') - .getAttribute('aria-disabled') - ).to.equal('true'); + await waitFor(() => { + expect( + screen + .getByTestId('pipeline-toolbar-export-aggregation-button') + .getAttribute('aria-disabled') + ).to.equal('true'); + }); }); }); }); diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx index 7ba0bacf743..823411c0254 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/index.spec.tsx @@ -1,19 +1,14 @@ import React from 'react'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelineOptions } from '.'; describe('PipelineOptions', function () { let container: HTMLElement; - beforeEach(function () { - render( - - - - ); + beforeEach(async function () { + await renderWithStore(); container = screen.getByTestId('pipeline-options'); }); diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx index 7de675f1799..b1f1ade7bdf 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-options/pipeline-collation.spec.tsx @@ -1,21 +1,17 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; import userEvent from '@testing-library/user-event'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import PipelineCollation from './pipeline-collation'; +import type { AggregationsStore } from '../../../stores/store'; describe('PipelineCollation', function () { - let store: ReturnType; - beforeEach(function () { - store = configureStore(); - render( - - - - ); + let store: AggregationsStore; + beforeEach(async function () { + const result = await renderWithStore(); + store = result.plugin.store; }); it('renders the collation toolbar', function () { diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx index 241b43e87eb..a00405948ae 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.spec.tsx @@ -1,29 +1,26 @@ import React from 'react'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; import { spy } from 'sinon'; import type { SinonSpy } from 'sinon'; -import { Provider } from 'react-redux'; -import configureStore from '../../../../test/configure-store'; +import { renderWithStore } from '../../../../test/configure-store'; import { PipelineSettings } from '.'; describe('PipelineSettings', function () { let container: HTMLElement; let onExportToLanguageSpy: SinonSpy; let onCreateNewPipelineSpy: SinonSpy; - beforeEach(function () { + beforeEach(async function () { onExportToLanguageSpy = spy(); onCreateNewPipelineSpy = spy(); - render( - - - + await renderWithStore( + ); container = screen.getByTestId('pipeline-settings'); }); diff --git a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx index 2a383811548..04458b965ff 100644 --- a/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx +++ b/packages/compass-aggregations/src/components/pipeline-toolbar/pipeline-settings/index.tsx @@ -47,6 +47,8 @@ export const PipelineSettings: React.FunctionComponent< onExportToLanguage, onCreateNewPipeline, }) => { + // TODO: remove direct check for storage existing, breaks single source of + // truth rule and exposes services to UI, this breaks the rules for locators const enableSavedAggregationsQueries = !!usePipelineStorage(); const isPipelineNameDisplayed = !editViewName && !!enableSavedAggregationsQueries; diff --git a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx index b80fe2f44bf..e05305a3a1f 100644 --- a/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx +++ b/packages/compass-aggregations/src/components/stage-preview/index.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; import type { ComponentProps } from 'react'; import type { Document } from 'mongodb'; -import { render, screen, cleanup } from '@testing-library/react'; +import { screen, cleanup } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import { StagePreview } from './'; import { @@ -19,50 +18,45 @@ const renderStagePreview = ( props: Partial> = {}, pipeline = DEFAULT_PIPELINE ) => { - render( - - - + return renderWithStore( + , + { pipeline } ); }; describe('StagePreview', function () { afterEach(cleanup); - it('renders empty content when stage is disabled', function () { - renderStagePreview({ + it('renders empty content when stage is disabled', async function () { + await renderStagePreview({ isDisabled: true, }); expect(screen.getByTestId('stage-preview-empty')).to.exist; }); - it('renders no preview documents when stage can not be previewed', function () { - renderStagePreview({ + it('renders no preview documents when stage can not be previewed', async function () { + await renderStagePreview({ shouldRenderStage: false, }); expect(screen.getByTestId('stage-preview-empty')).to.exist; }); - it('renders atlas preview when operator is $search', function () { - renderStagePreview({ + it('renders atlas preview when operator is $search', async function () { + await renderStagePreview({ shouldRenderStage: true, isMissingAtlasOnlyStageSupport: true, stageOperator: '$search', }); expect(screen.getByTestId('atlas-only-stage-preview')).to.exist; }); - it('renders out preivew when operator is $out', function () { - renderStagePreview( + it('renders out preivew when operator is $out', async function () { + await renderStagePreview( { shouldRenderStage: true, stageOperator: '$out', @@ -72,8 +66,8 @@ describe('StagePreview', function () { ); expect(screen.getByText(OUT_STAGE_PREVIEW_TEXT)).to.exist; }); - it('renders merge preview when operator is $merge', function () { - renderStagePreview( + it('renders merge preview when operator is $merge', async function () { + await renderStagePreview( { shouldRenderStage: true, stageOperator: '$merge', @@ -83,31 +77,31 @@ describe('StagePreview', function () { ); expect(screen.getByText(MERGE_STAGE_PREVIEW_TEXT)).to.exist; }); - it('renders loading preview docs', function () { - renderStagePreview({ + it('renders loading preview docs', async function () { + await renderStagePreview({ shouldRenderStage: true, isLoading: true, stageOperator: '$match', }); expect(screen.getByText(/Loading Preview Documents.../i)).to.exist; }); - it('renders no preview documents when there are no documents', function () { - renderStagePreview({ + it('renders no preview documents when there are no documents', async function () { + await renderStagePreview({ shouldRenderStage: true, documents: [], }); expect(screen.getByTestId('stage-preview-empty')).to.exist; }); - it('renders list of documents', function () { - renderStagePreview({ + it('renders list of documents', async function () { + await renderStagePreview({ shouldRenderStage: true, documents: [{ _id: 1 }, { _id: 2 }], }); const docs = screen.getAllByTestId('readonly-document'); expect(docs).to.have.length(2); }); - it('renders missing search index text for $search', function () { - renderStagePreview({ + it('renders missing search index text for $search', async function () { + await renderStagePreview({ shouldRenderStage: true, stageOperator: '$search', documents: [], @@ -119,8 +113,8 @@ describe('StagePreview', function () { ) ).to.exist; }); - it('renders $search preview docs', function () { - renderStagePreview({ + it('renders $search preview docs', async function () { + await renderStagePreview({ shouldRenderStage: true, stageOperator: '$search', documents: [{ _id: 1 }, { _id: 2 }], diff --git a/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx b/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx index 9f19ca12532..a374637faa9 100644 --- a/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx +++ b/packages/compass-aggregations/src/components/stage-toolbar/index.spec.tsx @@ -1,54 +1,48 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; -import { Provider } from 'react-redux'; -import configureStore from '../../../test/configure-store'; +import { renderWithStore } from '../../../test/configure-store'; import StageToolbar from './'; import { changeStageCollapsed, changeStageDisabled, } from '../../modules/pipeline-builder/stage-editor'; -const renderStageToolbar = () => { - const store = configureStore({ +const renderStageToolbar = async () => { + const result = await renderWithStore(, { pipeline: [{ $match: { _id: 1 } }, { $limit: 10 }, { $out: 'out' }], }); - render( - - - - ); - return store; + return result.plugin.store; }; describe('StageToolbar', function () { - it('renders collapse button', function () { - renderStageToolbar(); + it('renders collapse button', async function () { + await renderStageToolbar(); expect(screen.getByLabelText('Collapse')).to.exist; }); - it('renders stage number text', function () { - renderStageToolbar(); + it('renders stage number text', async function () { + await renderStageToolbar(); expect(screen.getByText('Stage 1')).to.exist; }); - it('render stage operator select', function () { - renderStageToolbar(); + it('render stage operator select', async function () { + await renderStageToolbar(); expect(screen.getByTestId('stage-operator-combobox')).to.exist; }); - it('renders stage enable/disable toggle', function () { - renderStageToolbar(); + it('renders stage enable/disable toggle', async function () { + await renderStageToolbar(); expect(screen.getByLabelText('Exclude stage from pipeline')).to.exist; }); context('renders stage text', function () { - it('when stage is disabled', function () { - const store = renderStageToolbar(); + it('when stage is disabled', async function () { + const store = await renderStageToolbar(); store.dispatch(changeStageDisabled(0, true)); expect( screen.getByText('Stage disabled. Results not passed in the pipeline.') ).to.exist; }); - it('when stage is collapsed', function () { - const store = renderStageToolbar(); + it('when stage is collapsed', async function () { + const store = await renderStageToolbar(); store.dispatch(changeStageCollapsed(0, true)); expect( screen.getByText( @@ -57,8 +51,8 @@ describe('StageToolbar', function () { ).to.exist; }); }); - it('renders option menu', function () { - renderStageToolbar(); + it('renders option menu', async function () { + await renderStageToolbar(); expect(screen.getByTestId('stage-option-menu-button')).to.exist; }); }); diff --git a/packages/compass-aggregations/src/modules/aggregation.spec.ts b/packages/compass-aggregations/src/modules/aggregation.spec.ts index 7414126a540..5f1bdf29bf3 100644 --- a/packages/compass-aggregations/src/modules/aggregation.spec.ts +++ b/packages/compass-aggregations/src/modules/aggregation.spec.ts @@ -23,6 +23,7 @@ import { EJSON } from 'bson'; import { defaultPreferencesInstance } from 'compass-preferences-model'; import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; +import type { AggregationsStore } from '../stores/store'; const getMockedStore = ( aggregation: AggregateState, @@ -68,14 +69,16 @@ describe('aggregation module', function () { it('runs an aggregation', async function () { const mockDocuments = [{ id: 1 }, { id: 2 }]; - const store: Store = configureStore( - { pipeline: [] }, - new (class { - aggregate() { - return Promise.resolve(mockDocuments); + const store: AggregationsStore = ( + await configureStore( + { pipeline: [] }, + { + aggregate() { + return Promise.resolve(mockDocuments); + }, } - })() as any - ); + ) + ).plugin.store; await store.dispatch(runAggregation() as any); const aggregation = store.getState().aggregation; @@ -111,14 +114,14 @@ describe('aggregation module', function () { page: 2, resultsViewType: 'document', }, - new (class { + { aggregate() { throw createCancelError(); - } + }, isCancelError() { return true; - } - })() as any + }, + } as any ); store.dispatch(fetchNextPage() as any); @@ -153,11 +156,11 @@ describe('aggregation module', function () { page: 2, resultsViewType: 'document', }, - new (class { + { aggregate() { return Promise.resolve(mockDocuments); - } - })() as any + }, + } as any ); await store.dispatch(fetchNextPage() as any); @@ -214,11 +217,11 @@ describe('aggregation module', function () { page: 2, resultsViewType: 'document', }, - new (class { + { aggregate() { return Promise.resolve(mockDocuments); - } - })() as any + }, + } as any ); await store.dispatch(fetchPrevPage() as any); diff --git a/packages/compass-aggregations/src/modules/auto-preview.spec.ts b/packages/compass-aggregations/src/modules/auto-preview.spec.ts index e3ae538821d..a8384833742 100644 --- a/packages/compass-aggregations/src/modules/auto-preview.spec.ts +++ b/packages/compass-aggregations/src/modules/auto-preview.spec.ts @@ -1,11 +1,12 @@ import { expect } from 'chai'; import { toggleAutoPreview } from './auto-preview'; import configureStore from '../../test/configure-store'; +import type { AggregationsStore } from '../stores/store'; describe('auto preview module', function () { - let store: ReturnType; - beforeEach(function () { - store = configureStore(); + let store: AggregationsStore; + beforeEach(async function () { + store = (await configureStore()).plugin.store; }); it('returns the default state', function () { diff --git a/packages/compass-aggregations/src/modules/collections-fields.spec.ts b/packages/compass-aggregations/src/modules/collections-fields.spec.ts index 545d95deca9..3826344302f 100644 --- a/packages/compass-aggregations/src/modules/collections-fields.spec.ts +++ b/packages/compass-aggregations/src/modules/collections-fields.spec.ts @@ -1,5 +1,3 @@ -import type { AnyAction, Store } from 'redux'; -import type { RootState } from '.'; import type { Document } from 'mongodb'; import { expect } from 'chai'; import reducer, { @@ -10,6 +8,7 @@ import reducer, { } from './collections-fields'; import configureStore from '../../test/configure-store'; import sinon from 'sinon'; +import type { AggregationsStore } from '../stores/store'; describe('collections-fields module', function () { describe('#reducer', function () { @@ -70,18 +69,20 @@ describe('collections-fields module', function () { }); }); describe('#actions', function () { - let store: Store; + let store: AggregationsStore; let sampleStub: sinon.SinonStub; let findStub: sinon.SinonStub; let sandbox: sinon.SinonSandbox; - beforeEach(function () { + beforeEach(async function () { sandbox = sinon.createSandbox(); sampleStub = sandbox.stub(); findStub = sandbox.stub(); - store = configureStore({ pipeline: [] }, { - sample: sampleStub, - find: findStub, - } as any); + store = ( + await configureStore({ pipeline: [] }, { + sample: sampleStub, + find: findStub, + } as any) + ).plugin.store; }); afterEach(function () { diff --git a/packages/compass-aggregations/src/modules/insights.spec.ts b/packages/compass-aggregations/src/modules/insights.spec.ts index fa5c9a72057..6bc7a56b088 100644 --- a/packages/compass-aggregations/src/modules/insights.spec.ts +++ b/packages/compass-aggregations/src/modules/insights.spec.ts @@ -44,14 +44,16 @@ describe('fetchExplainForPipeline', function () { explainAggregate: Sinon.stub().resolves(simpleExplain), }; - const store = configureStore( - { - namespace: 'test.test', - }, - dataService - ); + const store = ( + await configureStore( + { + namespace: 'test.test', + }, + dataService + ) + ).plugin.store; - await store.dispatch(fetchExplainForPipeline() as any); + await store.dispatch(fetchExplainForPipeline()); expect(store.getState()).to.have.nested.property( 'insights.isCollectionScan', @@ -64,14 +66,16 @@ describe('fetchExplainForPipeline', function () { explainAggregate: Sinon.stub().resolves(explainWithIndex), }; - const store = configureStore( - { - namespace: 'test.test', - }, - dataService - ); + const store = ( + await configureStore( + { + namespace: 'test.test', + }, + dataService + ) + ).plugin.store; - await store.dispatch(fetchExplainForPipeline() as any); + await store.dispatch(fetchExplainForPipeline()); expect(store.getState()).to.have.nested.property( 'insights.isCollectionScan', @@ -84,18 +88,21 @@ describe('fetchExplainForPipeline', function () { explainAggregate: Sinon.stub().resolves(explainWithIndex), }; - const store = configureStore( - { - namespace: 'test.test', - }, - dataService - ); + const store = ( + await configureStore( + { + namespace: 'test.test', + }, + dataService + ) + ).plugin.store; + + void store.dispatch(fetchExplainForPipeline()); + void store.dispatch(fetchExplainForPipeline()); + void store.dispatch(fetchExplainForPipeline()); + void store.dispatch(fetchExplainForPipeline()); - void store.dispatch(fetchExplainForPipeline() as any); - void store.dispatch(fetchExplainForPipeline() as any); - void store.dispatch(fetchExplainForPipeline() as any); - void store.dispatch(fetchExplainForPipeline() as any); - await store.dispatch(fetchExplainForPipeline() as any); + await store.dispatch(fetchExplainForPipeline()); expect(dataService.explainAggregate).to.be.calledOnce; }); @@ -106,15 +113,17 @@ describe('fetchExplainForPipeline', function () { isCancelError: Sinon.stub().returns(false), }; - const store = configureStore( - { - namespace: 'test.test', - pipeline: [{ $match: { foo: 1 } }, { $out: 'test' }], - }, - dataService - ); + const store = ( + await configureStore( + { + namespace: 'test.test', + pipeline: [{ $match: { foo: 1 } }, { $out: 'test' }], + }, + dataService + ) + ).plugin.store; - await store.dispatch(fetchExplainForPipeline() as any); + await store.dispatch(fetchExplainForPipeline()); expect(dataService.explainAggregate).to.be.calledWith('test.test', [ { $match: { foo: 1 } }, @@ -127,15 +136,17 @@ describe('fetchExplainForPipeline', function () { isCancelError: Sinon.stub().returns(false), }; - const store = configureStore( - { - namespace: 'test.test', - pipeline: [{ $merge: { into: 'test' } }, { $match: { bar: 2 } }], - }, - dataService - ); + const store = ( + await configureStore( + { + namespace: 'test.test', + pipeline: [{ $merge: { into: 'test' } }, { $match: { bar: 2 } }], + }, + dataService + ) + ).plugin.store; - await store.dispatch(fetchExplainForPipeline() as any); + await store.dispatch(fetchExplainForPipeline()); expect(dataService.explainAggregate).to.be.calledWith('test.test', [ { $match: { bar: 2 } }, diff --git a/packages/compass-aggregations/src/modules/max-time-ms.spec.ts b/packages/compass-aggregations/src/modules/max-time-ms.spec.ts index af48d5ef520..41892f98e7a 100644 --- a/packages/compass-aggregations/src/modules/max-time-ms.spec.ts +++ b/packages/compass-aggregations/src/modules/max-time-ms.spec.ts @@ -1,15 +1,18 @@ import { maxTimeMSChanged } from './max-time-ms'; import { expect } from 'chai'; import configureStore from '../../test/configure-store'; +import type { AggregationsStore } from '../stores/store'; describe('max-time-ms module', function () { - let store: ReturnType; - beforeEach(function () { - store = configureStore(undefined, undefined, { - preferences: { - getPreferences: () => ({ maxTimeMS: 1000 }), - }, - } as any); + let store: AggregationsStore; + beforeEach(async function () { + store = ( + await configureStore(undefined, undefined, { + preferences: { + getPreferences: () => ({ maxTimeMS: 1000 }), + } as any, + }) + ).plugin.store; }); it('initializes default max time to preferences value', function () { diff --git a/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts b/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts index c24a5be9dae..6d345e0d4e3 100644 --- a/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts +++ b/packages/compass-aggregations/src/modules/pipeline-builder/builder-helpers.spec.ts @@ -3,16 +3,20 @@ import { getPipelineStageOperatorsFromBuilderState } from './builder-helpers'; import { addStage } from './stage-editor'; import { changePipelineMode } from './pipeline-mode'; import configureStore from '../../../test/configure-store'; +import type { AggregationsStore } from '../../stores/store'; -function createStore(pipelineText = `[{$match: {_id: 1}}, {$limit: 10}]`) { - return configureStore({ pipelineText }); +async function createStore( + pipelineText = `[{$match: {_id: 1}}, {$limit: 10}]` +) { + const result = await configureStore({ pipelineText }); + return result.plugin.store; } describe('builder-helpers', function () { describe('getPipelineStageOperatorsFromBuilderState', function () { - let store: ReturnType; - beforeEach(function () { - store = createStore(); + let store: AggregationsStore; + beforeEach(async function () { + store = await createStore(); }); describe('in stage editor mode', function () { it('should return filtered stage names', function () { diff --git a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts index 372aecca22b..1d2fefdc0a2 100644 --- a/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts +++ b/packages/compass-aggregations/src/modules/pipeline-builder/pipeline-ai.spec.ts @@ -4,8 +4,9 @@ import type { PreferencesAccess } from 'compass-preferences-model'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider'; import type { DataService } from 'mongodb-data-service'; - -import configureReduxStore from '../../../test/configure-store'; +import configureReduxStore, { + MockAtlasAiService, +} from '../../../test/configure-store'; import { AIPipelineActionTypes, cancelAIPipelineGeneration, @@ -13,8 +14,6 @@ import { generateAggregationFromQuery, } from './pipeline-ai'; import { toggleAutoPreview } from '../auto-preview'; -import { MockAtlasAiService } from '../../../test/configure-store'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; describe('AIPipelineReducer', function () { const sandbox = Sinon.createSandbox(); @@ -34,26 +33,26 @@ describe('AIPipelineReducer', function () { sandbox.reset(); }); - function configureStore( + async function configureStore( aiService: Partial = {}, mockDataService?: Partial ) { const atlasAiService = Object.assign(new MockAtlasAiService(), aiService); - return configureReduxStore( + const result = await configureReduxStore( { namespace: 'database.collection', }, { sample: sandbox.stub().resolves([{ _id: 42 }]), getConnectionString: sandbox.stub().returns({ hosts: [] }), + ...mockDataService, } as any, { atlasAiService: atlasAiService as any, - dataService: mockDataService as any, preferences, - track: createNoopTrack(), } ); + return result.plugin.store; } describe('runAIPipelineGeneration', function () { @@ -62,7 +61,7 @@ describe('AIPipelineReducer', function () { const fetchJsonStub = sandbox.stub().resolves({ content: { aggregation: { pipeline: '[{ $match: { _id: 1 } }]' } }, }); - const store = configureStore({ + const store = await configureStore({ getAggregationFromUserInput: fetchJsonStub, }); @@ -98,7 +97,7 @@ describe('AIPipelineReducer', function () { describe('when there is an error', function () { it('sets the error on the store', async function () { - const store = configureStore({ + const store = await configureStore({ getAggregationFromUserInput: sandbox .stub() .rejects(new Error('500 Internal Server Error')), @@ -121,7 +120,7 @@ describe('AIPipelineReducer', function () { it('resets the store if errs was caused by user being unauthorized', async function () { const authError = new Error('Unauthorized'); (authError as any).statusCode = 401; - const store = configureStore({ + const store = await configureStore({ getAggregationFromUserInput: sandbox.stub().rejects(authError), }); await store.dispatch(runAIPipelineGeneration('testing prompt') as any); @@ -152,7 +151,7 @@ describe('AIPipelineReducer', function () { const mockDataService = { sample: sandbox.stub().resolves([{ pineapple: 'turtle' }]), }; - const store = configureStore( + const store = await configureStore( { getAggregationFromUserInput: fetchJsonStub, }, @@ -185,7 +184,7 @@ describe('AIPipelineReducer', function () { const fetchJsonStub = sandbox.stub().resolves({ content: { aggregation: { pipeline: '[{ $match: { _id: 1 } }]' } }, }); - const store = configureStore({ + const store = await configureStore({ getAggregationFromUserInput: fetchJsonStub, }); @@ -206,8 +205,8 @@ describe('AIPipelineReducer', function () { }); describe('cancelAIPipelineGeneration', function () { - it('should unset the fetching id and set the status on the store', function () { - const store = configureStore(); + it('should unset the fetching id and set the status on the store', async function () { + const store = await configureStore(); expect( store.getState().pipelineBuilder.aiPipeline.aiPipelineRequestId ).to.equal(null); @@ -236,8 +235,8 @@ describe('AIPipelineReducer', function () { }); describe('generateAggregationFromQuery', function () { - it('should create an aggregation pipeline', function () { - const store = configureStore({ + it('should create an aggregation pipeline', async function () { + const store = await configureStore({ getAggregationFromUserInput: sandbox.stub().resolves({ content: { aggregation: { pipeline: '[{ $group: { _id: "$price" } }]' }, diff --git a/packages/compass-aggregations/src/modules/search-indexes.spec.ts b/packages/compass-aggregations/src/modules/search-indexes.spec.ts index ab3dd5ac800..c9be316c4ca 100644 --- a/packages/compass-aggregations/src/modules/search-indexes.spec.ts +++ b/packages/compass-aggregations/src/modules/search-indexes.spec.ts @@ -3,6 +3,7 @@ import reducer, { fetchIndexes, ActionTypes } from './search-indexes'; import configureStore from '../../test/configure-store'; import sinon from 'sinon'; import type { AnyAction } from 'redux'; +import type { AggregationsStore } from '../stores/store'; describe('search-indexes module', function () { describe('#reducer', function () { @@ -55,20 +56,22 @@ describe('search-indexes module', function () { describe('#actions', function () { let getSearchIndexesStub: sinon.SinonStub; let sandbox: sinon.SinonSandbox; - let store: ReturnType; - beforeEach(function () { + let store: AggregationsStore; + beforeEach(async function () { sandbox = sinon.createSandbox(); getSearchIndexesStub = sandbox.stub(); - store = configureStore( - { - pipeline: [], - isSearchIndexesSupported: true, - namespace: 'test.listings', - }, - { - getSearchIndexes: getSearchIndexesStub, - } as any - ); + store = ( + await configureStore( + { + pipeline: [], + isSearchIndexesSupported: true, + namespace: 'test.listings', + }, + { + getSearchIndexes: getSearchIndexesStub, + } as any + ) + ).plugin.store; }); context('fetchIndexes', function () { it('fetches search indexes and sets status to READY', async function () { diff --git a/packages/compass-aggregations/src/modules/side-panel.spec.ts b/packages/compass-aggregations/src/modules/side-panel.spec.ts index f41e336577e..f149587e215 100644 --- a/packages/compass-aggregations/src/modules/side-panel.spec.ts +++ b/packages/compass-aggregations/src/modules/side-panel.spec.ts @@ -11,8 +11,8 @@ describe('side-panel module', function () { let store: Store; let fakeLocalStorage: SinonStub; - beforeEach(function () { - store = configureStore(); + beforeEach(async function () { + store = (await configureStore()).plugin.store; const localStorageValues: Record = {}; @@ -41,14 +41,14 @@ describe('side-panel module', function () { expect(store.getState().sidePanel.isPanelOpen).to.equal(false); }); - it('persists the last state', function () { - const store1 = configureStore(); + it('persists the last state', async function () { + const store1 = (await configureStore()).plugin.store; expect(store1.getState().sidePanel.isPanelOpen).to.equal(false); store1.dispatch(toggleSidePanel() as any); expect(store1.getState().sidePanel.isPanelOpen).to.equal(true); - const store2 = configureStore(); + const store2 = (await configureStore()).plugin.store; expect(store2.getState().sidePanel.isPanelOpen).to.equal(true); }); }); diff --git a/packages/compass-aggregations/src/plugin.spec.tsx b/packages/compass-aggregations/src/plugin.spec.tsx index a65701d6eac..89970482e78 100644 --- a/packages/compass-aggregations/src/plugin.spec.tsx +++ b/packages/compass-aggregations/src/plugin.spec.tsx @@ -1,23 +1,15 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { expect } from 'chai'; -import configureStore from '../test/configure-store'; +import { renderWithStore } from '../test/configure-store'; import { AggregationsPlugin } from './plugin'; -import { Provider } from 'react-redux'; - -const renderPlugin = () => { - const store = configureStore(); - const metadata = {} as any; - render( - - - - ); -}; describe('Aggregations [Plugin]', function () { - it('should render plugin with toolbar and export button', function () { - renderPlugin(); + it('should render plugin with toolbar and export button', async function () { + const metadata = {} as any; + await renderWithStore( + + ); expect(screen.getByTestId('pipeline-toolbar')).to.exist; expect(screen.getByTestId('pipeline-toolbar-export-aggregation-button')).to .exist; diff --git a/packages/compass-aggregations/src/stores/create-view.spec.ts b/packages/compass-aggregations/src/stores/create-view.spec.ts index 51957e95750..35e5f562768 100644 --- a/packages/compass-aggregations/src/stores/create-view.spec.ts +++ b/packages/compass-aggregations/src/stores/create-view.spec.ts @@ -1,14 +1,19 @@ -import AppRegistry, { createActivateHelpers } from 'hadron-app-registry'; -import { activateCreateViewPlugin } from './create-view'; +import type AppRegistry from 'hadron-app-registry'; import { expect } from 'chai'; -import { - ConnectionsManager, - type DataService, - type ConnectionRepository, -} from '@mongodb-js/compass-connections/provider'; import { changeViewName, createView } from '../modules/create-view'; import Sinon from 'sinon'; -import type { WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; +import { + activatePluginWithConnections, + cleanup, +} from '@mongodb-js/compass-connections/test'; +import { CreateViewPlugin } from '../index'; + +const TEST_CONNECTION = { + id: 'TEST', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, +}; describe('CreateViewStore [Store]', function () { if ( @@ -21,69 +26,60 @@ describe('CreateViewStore [Store]', function () { } let store: any; - let deactivate: any; - let globalAppRegistry: AppRegistry; - let appRegistryEmitSpy: Sinon.SinonSpy; - const logger = {} as any; - const track = () => {}; - const createViewStub = Sinon.stub(); - const dataService = { - createView: createViewStub, - } as unknown as DataService; - const connectionsManager = new ConnectionsManager({ logger }); - const openCollectionWorkspaceStub = Sinon.stub(); + let appRegistry: Sinon.SinonSpiedInstance; + const workspaces = { - openCollectionWorkspace: openCollectionWorkspaceStub, - } as unknown as WorkspacesService; - const connectionRepository = { - getConnectionInfoById: () => {}, - } as unknown as ConnectionRepository; + openCollectionWorkspace: Sinon.stub(), + } as any; + + const dataService = { + createView: Sinon.stub(), + }; + + beforeEach(async function () { + const { plugin, globalAppRegistry, connectionsStore } = + activatePluginWithConnections( + CreateViewPlugin.withMockServices({ workspaces }) as any, + {}, + { + connections: [TEST_CONNECTION], + connectFn() { + return dataService; + }, + } + ); + + await connectionsStore.actions.connect(TEST_CONNECTION); - beforeEach(function () { - globalAppRegistry = new AppRegistry(); - appRegistryEmitSpy = Sinon.spy(globalAppRegistry, 'emit'); - Sinon.stub(connectionsManager, 'getDataServiceForConnection').returns( - dataService - ); - ({ store, deactivate } = activateCreateViewPlugin( - {}, - { - globalAppRegistry, - connectionsManager, - connectionRepository, - logger, - track, - workspaces, - }, - createActivateHelpers() - )); + store = plugin.store; + appRegistry = Sinon.spy(globalAppRegistry); }); afterEach(function () { store = null; Sinon.restore(); - deactivate(); + cleanup(); }); describe('#configureStore', function () { describe('when open create view is emitted', function () { it('throws an error when the action is emitted without connection meta', function () { expect(() => { - globalAppRegistry.emit('open-create-view', { + appRegistry.emit('open-create-view', { source: 'dataService.test', pipeline: [{ $project: { a: 1 } }], }); }).to.throw; }); it('dispatches the open action and sets the correct state', function () { - globalAppRegistry.emit( + appRegistry.emit( 'open-create-view', { source: 'dataService.test', pipeline: [{ $project: { a: 1 } }], }, { - connectionId: 'TEST', + connectionId: TEST_CONNECTION.id, } ); expect(store.getState().isVisible).to.equal(true); @@ -91,39 +87,39 @@ describe('CreateViewStore [Store]', function () { { $project: { a: 1 } }, ]); expect(store.getState().source).to.equal('dataService.test'); - expect(store.getState().connectionId).to.equal('TEST'); + expect(store.getState().connectionId).to.equal(TEST_CONNECTION.id); }); }); it('handles createView action and notifies the rest of the app', async function () { - globalAppRegistry.emit( + appRegistry.emit( 'open-create-view', { source: 'dataService.test', pipeline: [{ $project: { a: 1 } }], }, { - connectionId: 'TEST', + connectionId: TEST_CONNECTION.id, } ); store.dispatch(changeViewName('TestView')); await store.dispatch(createView()); - expect(createViewStub).to.be.calledWithExactly( + expect(dataService.createView).to.be.calledWithExactly( 'TestView', 'dataService.test', [{ $project: { a: 1 } }], {} ); - expect(appRegistryEmitSpy.lastCall).to.be.calledWithExactly( + expect(appRegistry.emit.lastCall).to.be.calledWithExactly( 'view-created', 'dataService.TestView', - { connectionId: 'TEST' } + { connectionId: TEST_CONNECTION.id } ); - expect(openCollectionWorkspaceStub).to.be.calledWithExactly( - 'TEST', + expect(workspaces.openCollectionWorkspace).to.be.calledWithExactly( + TEST_CONNECTION.id, 'dataService.TestView', { newTab: true } ); diff --git a/packages/compass-aggregations/src/stores/store.spec.ts b/packages/compass-aggregations/src/stores/store.spec.ts index dd74ad895e1..b2724ab3d9b 100644 --- a/packages/compass-aggregations/src/stores/store.spec.ts +++ b/packages/compass-aggregations/src/stores/store.spec.ts @@ -1,20 +1,21 @@ -import AppRegistry from 'hadron-app-registry'; +import type AppRegistry from 'hadron-app-registry'; import rootReducer from '../modules'; import { expect } from 'chai'; import configureStore from '../../test/configure-store'; -import type { Store } from 'redux'; +import type { AggregationsStore } from '../stores/store'; const INITIAL_STATE = rootReducer(undefined, { type: '@@init' }); describe('Aggregation Store', function () { describe('#configureStore', function () { context('when providing a serverVersion', function () { - let store: Store; + let store: AggregationsStore; - beforeEach(function () { - store = configureStore({ + beforeEach(async function () { + const result = await configureStore({ serverVersion: '4.2.0', }); + store = result.plugin.store; }); it('sets the server version the state', function () { @@ -23,12 +24,13 @@ describe('Aggregation Store', function () { }); context('when providing an env', function () { - let store: Store; + let store: AggregationsStore; - beforeEach(function () { - store = configureStore({ + beforeEach(async function () { + const result = await configureStore({ env: 'atlas', }); + store = result.plugin.store; }); it('sets the env in the state', function () { @@ -37,17 +39,12 @@ describe('Aggregation Store', function () { }); context('when providing a namespace', function () { - context('when there is no collection', function () { - it('throws', function () { - expect(() => configureStore({ namespace: 'db' })).to.throw(); - }); - }); - context('when there is a collection', function () { - let store: Store; + let store: AggregationsStore; - beforeEach(function () { - store = configureStore({ namespace: 'db.coll' }); + beforeEach(async function () { + const result = await configureStore({ namespace: 'db.coll' }); + store = result.plugin.store; }); it('updates the namespace in the store', function () { @@ -55,15 +52,15 @@ describe('Aggregation Store', function () { }); it('resets the rest of the state to initial state', function () { + // Remove properties that we don't want to compare // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { aggregationWorkspaceId, dataService, ...state } = + const { aggregationWorkspaceId, dataService, sidePanel, ...state } = store.getState(); + state.pipelineBuilder.stageEditor = { stages: [], stagesIdAndType: [], }; - delete state.pipeline; - delete state.sidePanel; expect(state).to.deep.equal({ outResultsFn: INITIAL_STATE.outResultsFn, @@ -79,7 +76,6 @@ describe('Aggregation Store', function () { savedPipeline: INITIAL_STATE.savedPipeline, inputDocuments: { ...INITIAL_STATE.inputDocuments, - isLoading: true, }, serverVersion: INITIAL_STATE.serverVersion, isModified: INITIAL_STATE.isModified, @@ -109,15 +105,13 @@ describe('Aggregation Store', function () { }); describe('#onActivated', function () { - let store: Store; - const localAppRegistry = new AppRegistry(); - const globalAppRegistry = new AppRegistry(); - - beforeEach(function () { - store = configureStore(undefined, undefined, { - localAppRegistry: localAppRegistry, - globalAppRegistry: globalAppRegistry, - }); + let store: AggregationsStore; + let localAppRegistry: AppRegistry; + + beforeEach(async function () { + const result = await configureStore(); + localAppRegistry = result.localAppRegistry; + store = result.plugin.store; }); context('when an aggregation should be generated from query', function () { diff --git a/packages/compass-aggregations/src/stores/store.ts b/packages/compass-aggregations/src/stores/store.ts index 410f0330348..993a6f50dd0 100644 --- a/packages/compass-aggregations/src/stores/store.ts +++ b/packages/compass-aggregations/src/stores/store.ts @@ -316,3 +316,7 @@ const handleDatabaseCollections = ( onDatabaseCollectionStatusChange ); }; + +export type AggregationsStore = ReturnType< + typeof activateAggregationsPlugin +>['store']; diff --git a/packages/compass-aggregations/test/configure-store.ts b/packages/compass-aggregations/test/configure-store.ts index 18c7c1ddbf4..9c2fe89b090 100644 --- a/packages/compass-aggregations/test/configure-store.ts +++ b/packages/compass-aggregations/test/configure-store.ts @@ -1,19 +1,17 @@ -import AppRegistry, { createActivateHelpers } from 'hadron-app-registry'; import type { AggregationsPluginServices, ConfigureStoreOptions, } from '../src/stores/store'; -import { activateAggregationsPlugin } from '../src/stores/store'; import { mockDataService } from './mocks/data-service'; -import type { DataService } from '../src/modules/data-service'; -import { ReadOnlyPreferenceAccess } from 'compass-preferences-model/provider'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; import { AtlasAuthService } from '@mongodb-js/atlas-service/provider'; import { - ConnectionScopedAppRegistryImpl, - TEST_CONNECTION_INFO, -} from '@mongodb-js/compass-connections/provider'; + activatePluginWithActiveConnection, + renderPluginComponentWithActiveConnection, +} from '@mongodb-js/compass-connections/test'; +import { CompassAggregationsHadronPlugin } from '../src/index'; +import type { DataService } from '@mongodb-js/compass-connections/provider'; +import React from 'react'; +import { PipelineStorageProvider } from '@mongodb-js/my-queries-storage/provider'; export class MockAtlasAuthService extends AtlasAuthService { isAuthenticated() { @@ -42,30 +40,24 @@ export class MockAtlasAiService { } } -export default function configureStore( - options: Partial = {}, - dataService: DataService = mockDataService(), +function getMockedPluginArgs( + initialProps: Partial = {}, + dataService: Partial = mockDataService(), services: Partial = {} ) { - const preferences = new ReadOnlyPreferenceAccess(); - const logger = createNoopLogger(); - const track = createNoopTrack(); - const atlasAuthService = new MockAtlasAuthService(); const atlasAiService = new MockAtlasAiService(); - const globalAppRegistry = new AppRegistry(); - const connectionInfoAccess = { - getCurrentConnectionInfo() { - return TEST_CONNECTION_INFO; - }, - }; - const connectionScopedAppRegistry = - new ConnectionScopedAppRegistryImpl<'open-export'>( - globalAppRegistry.emit.bind(globalAppRegistry), - connectionInfoAccess - ); - - return activateAggregationsPlugin( + return [ + CompassAggregationsHadronPlugin.withMockServices({ + atlasAuthService, + atlasAiService, + collection: { + toJSON: () => ({}), + on: () => {}, + removeListener: () => {}, + } as any, + ...services, + } as any), { namespace: 'test.test', isReadonly: false, @@ -76,27 +68,47 @@ export default function configureStore( isDataLake: false, isAtlas: false, serverVersion: '4.0.0', - ...options, + ...initialProps, }, { - dataService, - instance: {} as any, - preferences, - globalAppRegistry, - localAppRegistry: new AppRegistry(), - workspaces: {} as any, - logger, - track, - atlasAiService: atlasAiService as any, - atlasAuthService, - connectionInfoAccess, - collection: { - toJSON: () => ({}), - on: () => {}, - } as any, - connectionScopedAppRegistry, - ...services, + id: 'TEST', + connectionOptions: { + connectionString: 'mongodb://localhost:27020', + }, }, - createActivateHelpers() - ).store; + { + connectFn() { + return dataService; + }, + preferences: services.preferences + ? services.preferences.getPreferences() + : undefined, + }, + ] as unknown as Parameters; +} + +/** + * @deprecated use renderWithStore and test store through UI instead + */ +export default function configureStore( + ...args: Parameters +) { + return activatePluginWithActiveConnection(...getMockedPluginArgs(...args)); +} + +export function renderWithStore( + ui: React.ReactElement, + ...args: Parameters +) { + ui = args[2]?.pipelineStorage + ? React.createElement(PipelineStorageProvider, { + value: args[2].pipelineStorage, + children: ui, + }) + : ui; + + return renderPluginComponentWithActiveConnection( + ui, + ...getMockedPluginArgs(...args) + ); } diff --git a/packages/compass-app-stores/package.json b/packages/compass-app-stores/package.json index 2b5ea66daa0..1823a458517 100644 --- a/packages/compass-app-stores/package.json +++ b/packages/compass-app-stores/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "7.24.0", + "version": "7.25.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -53,12 +53,10 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/dom": "^8.20.1", - "@testing-library/react": "^12.1.5", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -74,14 +72,14 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/connection-info": "^0.5.3", - "hadron-app-registry": "^9.2.2", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/connection-info": "^0.6.0", + "hadron-app-registry": "^9.2.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2" }, diff --git a/packages/compass-app-stores/src/plugin.tsx b/packages/compass-app-stores/src/plugin.tsx index 86f682c7895..8903e22be57 100644 --- a/packages/compass-app-stores/src/plugin.tsx +++ b/packages/compass-app-stores/src/plugin.tsx @@ -13,7 +13,7 @@ import { import { type MongoDBInstancesManager } from './instances-manager'; interface MongoDBInstancesProviderProps { - children: React.ReactNode; + children?: React.ReactNode; instancesManager: MongoDBInstancesManager; } @@ -28,13 +28,7 @@ function MongoDBInstancesManagerProvider({ ); } -export const CompassInstanceStorePlugin = registerHadronPlugin< - { children: React.ReactNode }, - { - logger: () => Logger; - connectionsManager: () => ConnectionsManager; - } ->( +export const CompassInstanceStorePlugin = registerHadronPlugin( { name: 'CompassInstanceStore', component: MongoDBInstancesManagerProvider as React.FunctionComponent< diff --git a/packages/compass-app-stores/src/provider.spec.tsx b/packages/compass-app-stores/src/provider.spec.tsx index 15070433863..65e21819ac9 100644 --- a/packages/compass-app-stores/src/provider.spec.tsx +++ b/packages/compass-app-stores/src/provider.spec.tsx @@ -4,10 +4,14 @@ import { MongoDBInstancesManagerProvider, TestMongoDBInstanceManager, } from './provider'; -import { render, screen, cleanup } from '@testing-library/react'; import { expect } from 'chai'; -import { waitFor } from '@testing-library/dom'; import Sinon from 'sinon'; +import { + renderWithActiveConnection, + screen, + cleanup, + waitFor, +} from '@mongodb-js/compass-connections/test'; describe('NamespaceProvider', function () { const sandbox = Sinon.createSandbox(); @@ -17,11 +21,11 @@ describe('NamespaceProvider', function () { sandbox.reset(); }); - it('should immediately render content if database exists', function () { + it('should immediately render content if database exists', async function () { const instanceManager = new TestMongoDBInstanceManager({ databases: [{ _id: 'foo' }] as any, }); - render( + await renderWithActiveConnection( hello @@ -29,11 +33,11 @@ describe('NamespaceProvider', function () { expect(screen.getByText('hello')).to.exist; }); - it('should immediately render content if collection exists', function () { + it('should immediately render content if collection exists', async function () { const instanceManager = new TestMongoDBInstanceManager({ databases: [{ _id: 'foo', collections: [{ _id: 'foo.bar' }] }] as any, }); - render( + await renderWithActiveConnection( hello @@ -41,9 +45,9 @@ describe('NamespaceProvider', function () { expect(screen.getByText('hello')).to.exist; }); - it("should not render content when namespace doesn't exist", function () { + it("should not render content when namespace doesn't exist", async function () { const instanceManager = new TestMongoDBInstanceManager(); - render( + await renderWithActiveConnection( hello @@ -59,7 +63,7 @@ describe('NamespaceProvider', function () { return Promise.resolve(); }); - render( + await renderWithActiveConnection( hello @@ -77,7 +81,7 @@ describe('NamespaceProvider', function () { const instanceManager = new TestMongoDBInstanceManager({ databases: [{ _id: 'foo' }] as any, }); - render( + await renderWithActiveConnection( ; let instancesManager: MongoDBInstancesManager; - let sandbox: sinon.SinonSandbox; + let getDataService: any; + let connectionsStore: any; function waitForInstanceRefresh(instance: MongoDBInstance): Promise { return new Promise((resolve) => { @@ -70,65 +54,47 @@ describe('InstanceStore [Store]', function () { } beforeEach(function () { - globalAppRegistry = new AppRegistry(); - sandbox = sinon.createSandbox(); - - dataService = createDataService(); - const logger = createNoopLogger(); - connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, - __TEST_CONNECT_FN: () => Promise.resolve(dataService), - }); - - store = createInstancesStore( + const result = activatePluginWithConnections( + CompassInstanceStorePlugin, + {}, { - connectionsManager, - globalAppRegistry, - logger, - }, - createActivateHelpers() + connectFn() { + return createDataService(); + }, + } ); - instancesManager = store.getState().instancesManager; + connectionsStore = result.connectionsStore; + getDataService = result.getDataServiceForConnection; + globalAppRegistry = result.globalAppRegistry; + sandbox = sinon.createSandbox(); + instancesManager = result.plugin.store.getState().instancesManager; }); afterEach(function () { sandbox.restore(); - store.deactivate(); + cleanup(); }); it('should not have any MongoDBInstance if no connection is established', function () { expect(instancesManager.listMongoDBInstances()).to.be.of.length(0); }); - it('should have a MongodbInstance for each of the connected connection', function () { - for (const connectedConnectionInfoId of ['1', '2', '3']) { - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - connectedConnectionInfoId, - dataService - ); + it('should have a MongodbInstance for each of the connected connection', async function () { + for (const connectionInfo of mockConnections) { + await connectionsStore.actions.connect(connectionInfo); + expect(() => { + instancesManager.getMongoDBInstanceForConnection(connectionInfo.id); + }).to.not.throw(); } - - expect(() => instancesManager.getMongoDBInstanceForConnection('1')).to.not - .throw; - expect(() => instancesManager.getMongoDBInstanceForConnection('2')).to.not - .throw; - expect(() => instancesManager.getMongoDBInstanceForConnection('3')).to.not - .throw; }); context('when connected', function () { let connectedInstance: MongoDBInstance; let initialInstanceRefreshedPromise: Promise; - beforeEach(function () { - sinon - .stub(connectionsManager, 'getDataServiceForConnection') - .returns(dataService); - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - connectedConnectionInfoId, - dataService - ); + const connectedConnectionInfoId = mockConnections[0].id; + + beforeEach(async function () { + await connectionsStore.actions.connect(mockConnections[0]); const instance = instancesManager.getMongoDBInstanceForConnection( connectedConnectionInfoId ); @@ -140,20 +106,22 @@ describe('InstanceStore [Store]', function () { context('on refresh data', function () { beforeEach(async function () { - sandbox - .stub(dataService, 'instance') - .returns({ build: { version: '3.2.1' } }); await initialInstanceRefreshedPromise; + sandbox + .stub(getDataService(connectedConnectionInfoId), 'instance') + .resolves({ build: { version: '3.2.1' } }); const instance = instancesManager.getMongoDBInstanceForConnection( connectedConnectionInfoId ); - expect(instance).to.have.nested.property('build.version', '1.2.3'); + expect(instance).to.have.nested.property('build.version', '0.0.0'); globalAppRegistry.emit('refresh-data'); await waitForInstanceRefresh(instance); }); it('calls instance model fetch', function () { - const instance = instancesManager.getMongoDBInstanceForConnection('1'); + const instance = instancesManager.getMongoDBInstanceForConnection( + connectedConnectionInfoId + ); expect(instance).to.have.nested.property('build.version', '3.2.1'); }); }); @@ -163,7 +131,9 @@ describe('InstanceStore [Store]', function () { await initialInstanceRefreshedPromise; await Promise.all( connectedInstance.databases.map((db) => { - return db.fetchCollections({ dataService }); + return db.fetchCollections({ + dataService: getDataService(connectedConnectionInfoId), + }); }) ); expect(connectedInstance.databases).to.have.lengthOf(1); @@ -208,7 +178,7 @@ describe('InstanceStore [Store]', function () { it('should remove collection from the database collections', function () { globalAppRegistry.emit('collection-dropped', 'foo.bar', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); expect( connectedInstance.databases.get('foo')?.collections.get('foo.bar') @@ -222,17 +192,17 @@ describe('InstanceStore [Store]', function () { coll?.on('change', () => {}); expect((coll as any)._events.change).to.have.lengthOf(1); globalAppRegistry.emit('collection-dropped', 'foo.bar', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); expect((coll as any)._events).to.not.exist; }); it('should remove database if last collection was removed', function () { globalAppRegistry.emit('collection-dropped', 'foo.bar', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); globalAppRegistry.emit('collection-dropped', 'foo.buz', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); expect(connectedInstance.databases).to.have.lengthOf(0); expect(connectedInstance.databases.get('foo')).not.to.exist; @@ -261,7 +231,7 @@ describe('InstanceStore [Store]', function () { it('should remove database from instance databases', function () { globalAppRegistry.emit('database-dropped', 'foo', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); expect(connectedInstance.databases).to.have.lengthOf(0); expect(connectedInstance.databases.get('foo')).not.to.exist; @@ -272,7 +242,7 @@ describe('InstanceStore [Store]', function () { db?.on('change', () => {}); expect((db as any)._events.change).to.have.lengthOf(1); globalAppRegistry.emit('database-dropped', 'foo', { - connectionId: '1', + connectionId: connectedConnectionInfoId, }); expect((db as any)._events).to.not.exist; }); @@ -474,43 +444,33 @@ describe('InstanceStore [Store]', function () { }); context('when disconnected', function () { + const connectionInfo = mockConnections[0]; + const connectedConnectionInfoId = connectionInfo.id; + it('should remove the instance from InstancesManager and should not perform any actions on the stale instance', async function () { // first connect - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - connectedConnectionInfoId, - dataService - ); + await connectionsStore.actions.connect(connectionInfo); // setup a spy on old instance const oldInstance = instancesManager.getMongoDBInstanceForConnection( connectedConnectionInfoId ); - const oldFetchDatabasesSpy = sinon.spy(oldInstance, 'fetchDatabases'); + await waitForInstanceRefresh(oldInstance); - // now disconnect - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionDisconnected, - connectedConnectionInfoId - ); + connectionsStore.actions.disconnect(connectedConnectionInfoId); + + // setup a spy on old instance + const oldFetchDatabasesSpy = sinon.spy(oldInstance, 'fetchDatabases'); // there is no instance in store InstancesManager now - expect(() => + expect(() => { instancesManager.getMongoDBInstanceForConnection( connectedConnectionInfoId - ) - ).to.throw; + ); + }).to.throw(); // lets connect again and ensure that old instance does not receive events anymore - const newDataService = createDataService(); - sinon - .stub(connectionsManager, 'getDataServiceForConnection') - .returns(dataService); - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - connectedConnectionInfoId, - newDataService - ); + await connectionsStore.actions.connect(connectionInfo); // setup a spy on new instance const newInstance = instancesManager.getMongoDBInstanceForConnection( diff --git a/packages/compass-app-stores/src/stores/instance-store.ts b/packages/compass-app-stores/src/stores/instance-store.ts index b5a801801c5..379eb2b9090 100644 --- a/packages/compass-app-stores/src/stores/instance-store.ts +++ b/packages/compass-app-stores/src/stores/instance-store.ts @@ -5,10 +5,7 @@ import type { DataService } from '@mongodb-js/compass-connections/provider'; import type { ActivateHelpers, AppRegistry } from 'hadron-app-registry'; import type { Logger } from '@mongodb-js/compass-logging/provider'; import { openToast } from '@mongodb-js/compass-components'; -import { - ConnectionsManagerEvents, - type ConnectionsManager, -} from '@mongodb-js/compass-connections/provider'; +import { type ConnectionsManager } from '@mongodb-js/compass-connections/provider'; import { MongoDBInstancesManager } from '../instances-manager'; function serversArray( @@ -256,79 +253,73 @@ export function createInstancesStore( } }; - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionDisconnected, - function (connectionInfoId: string) { - try { - const instance = - instancesManager.getMongoDBInstanceForConnection(connectionInfoId); - instance.removeAllListeners(); - } catch (error) { - log.warn( - mongoLogId(1_001_000_322), - 'Instance Store', - 'Failed to remove instance listeners upon disconnect', - { - message: (error as Error).message, - connectionId: connectionInfoId, - } - ); - } - instancesManager.removeMongoDBInstanceForConnection(connectionInfoId); - } - ); - - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - function (instanceConnectionId: string, dataService: DataService) { - const connectionString = dataService.getConnectionString(); - const firstHost = connectionString.hosts[0] || ''; - const [hostname, port] = firstHost.split(':'); - - const initialInstanceProps: Partial = { - _id: firstHost, - hostname: hostname, - port: port ? +port : undefined, - topologyDescription: getTopologyDescription( - dataService.getLastSeenTopology() - ), - }; - const instance = instancesManager.createMongoDBInstanceForConnection( - instanceConnectionId, - initialInstanceProps as MongoDBInstanceProps - ); - - addCleanup(() => { - instance.removeAllListeners(); - }); - - void refreshInstance( - { - fetchDatabases: true, - fetchDbStats: true, - }, + on(connectionsManager, 'disconnected', function (connectionInfoId: string) { + try { + const instance = + instancesManager.getMongoDBInstanceForConnection(connectionInfoId); + instance.removeAllListeners(); + } catch (error) { + log.warn( + mongoLogId(1_001_000_322), + 'Instance Store', + 'Failed to remove instance listeners upon disconnect', { - connectionId: instanceConnectionId, - } - ); - - on( - dataService, - 'topologyDescriptionChanged', - ({ - newDescription, - }: { - newDescription: ReturnType; - }) => { - instance.set({ - topologyDescription: getTopologyDescription(newDescription), - }); + message: (error as Error).message, + connectionId: connectionInfoId, } ); } - ); + instancesManager.removeMongoDBInstanceForConnection(connectionInfoId); + }); + + on(connectionsManager, 'connected', function (instanceConnectionId: string) { + const dataService = + connectionsManager.getDataServiceForConnection(instanceConnectionId); + const connectionString = dataService.getConnectionString(); + const firstHost = connectionString.hosts[0] || ''; + const [hostname, port] = firstHost.split(':'); + + const initialInstanceProps: Partial = { + _id: firstHost, + hostname: hostname, + port: port ? +port : undefined, + topologyDescription: getTopologyDescription( + dataService.getLastSeenTopology() + ), + }; + const instance = instancesManager.createMongoDBInstanceForConnection( + instanceConnectionId, + initialInstanceProps as MongoDBInstanceProps + ); + + addCleanup(() => { + instance.removeAllListeners(); + }); + + void refreshInstance( + { + fetchDatabases: true, + fetchDbStats: true, + }, + { + connectionId: instanceConnectionId, + } + ); + + on( + dataService, + 'topologyDescriptionChanged', + ({ + newDescription, + }: { + newDescription: ReturnType; + }) => { + instance.set({ + topologyDescription: getTopologyDescription(newDescription), + }); + } + ); + }); on( globalAppRegistry, diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index a970a2923c7..75ac8d25fb6 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "4.37.0", + "version": "4.38.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,17 +48,17 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", "@mongodb-js/mongodb-constants": "^0.10.2", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "mongodb-collection-model": "^5.22.3", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "mongodb-collection-model": "^5.23.0", "mongodb-ns": "^2.4.2", "numeral": "^2.0.6", "react": "^17.0.2", @@ -67,8 +67,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx index 2d5567b3f1a..ff013eaa3f4 100644 --- a/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx +++ b/packages/compass-collection/src/components/collection-header-actions/collection-header-actions.tsx @@ -58,17 +58,17 @@ const CollectionHeaderActions: React.FunctionComponent< const { readOnly: preferencesReadOnly, enableShell, - enableNewMultipleConnectionSystem, + enableMultipleConnectionSystem, } = usePreferences([ 'readOnly', 'enableShell', - 'enableNewMultipleConnectionSystem', + 'enableMultipleConnectionSystem', ]); const track = useTelemetry(); const { database, collection } = toNS(namespace); - const showOpenShellButton = enableShell && enableNewMultipleConnectionSystem; + const showOpenShellButton = enableShell && enableMultipleConnectionSystem; return (
; + component: HadronPluginComponent; } type CollectionTabComponentsProviderValue = { diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index 4097c7ab54d..65606d0bb3e 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -1,6 +1,6 @@ { "name": "@mongodb-js/compass-components", - "version": "1.29.0", + "version": "1.29.1", "description": "React Components used in Compass", "license": "SSPL", "main": "lib/index.js", @@ -78,7 +78,7 @@ "@react-aria/visually-hidden": "^3.3.1", "bson": "^6.7.0", "focus-trap-react": "^9.0.2", - "hadron-document": "^8.6.0", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", @@ -92,8 +92,8 @@ }, "devDependencies": { "@emotion/css": "^11.11.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/dom": "^8.20.1", diff --git a/packages/compass-components/src/components/compass-components-provider.tsx b/packages/compass-components/src/components/compass-components-provider.tsx index 58564cf58d8..18bac239d30 100644 --- a/packages/compass-components/src/components/compass-components-provider.tsx +++ b/packages/compass-components/src/components/compass-components-provider.tsx @@ -15,6 +15,7 @@ type CompassComponentsProviderProps = { * value will be derived from the system settings */ darkMode?: boolean; + popoverPortalContainer?: HTMLElement; /** * Either React children or a render callback that will get the darkMode * property passed as function properties @@ -97,6 +98,7 @@ export const CompassComponentsProvider = ({ utmSource, utmMedium, stackedElementsZIndex, + popoverPortalContainer: _popoverPortalContainer, ...signalHooksProviderProps }: CompassComponentsProviderProps) => { const darkMode = useDarkMode(_darkMode); @@ -107,7 +109,7 @@ export const CompassComponentsProvider = ({ // is literally no way around it with how leafygreen popover works and lucky // for us, this will usually cause a state update only once const [portalContainer, setPortalContainer] = useState( - null + _popoverPortalContainer ?? null ); const [scrollContainer, setScrollContainer] = useState( null diff --git a/packages/compass-components/src/components/error-boundary.tsx b/packages/compass-components/src/components/error-boundary.tsx index 527044790c2..1c5d79a2795 100644 --- a/packages/compass-components/src/components/error-boundary.tsx +++ b/packages/compass-components/src/components/error-boundary.tsx @@ -18,7 +18,6 @@ type Props = { className?: string; displayName?: string; onError?: (error: Error, errorInfo: ErrorInfo) => void; - children: React.ReactElement; }; class ErrorBoundary extends React.Component { diff --git a/packages/compass-components/src/components/workspace-tabs/tab.tsx b/packages/compass-components/src/components/workspace-tabs/tab.tsx index 4db6b63c3cf..7b5369e8f20 100644 --- a/packages/compass-components/src/components/workspace-tabs/tab.tsx +++ b/packages/compass-components/src/components/workspace-tabs/tab.tsx @@ -47,6 +47,7 @@ const tabStyles = css({ boxShadow: 'inset -1px -1px 0 0 var(--workspace-tab-border-color)', '&:hover': { + backgroundColor: 'inherit', cursor: 'pointer', zIndex: 1, }, @@ -132,6 +133,7 @@ const selectedTabStyles = css({ boxShadow: 'inset -1px 0 0 0 var(--workspace-tab-border-color)', '&:hover': { + backgroundColor: 'var(--workspace-tab-selected-background-color)', cursor: 'default', }, diff --git a/packages/compass-connection-import-export/package.json b/packages/compass-connection-import-export/package.json index 3533c714200..76dc9ea3af1 100644 --- a/packages/compass-connection-import-export/package.json +++ b/packages/compass-connection-import-export/package.json @@ -14,7 +14,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.34.0", + "version": "0.35.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -51,16 +51,16 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/connection-storage": "^0.17.0", - "compass-preferences-model": "^2.26.0", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/connection-storage": "^0.18.0", + "compass-preferences-model": "^2.27.0", + "hadron-ipc": "^3.2.21", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-connection-import-export/src/components/import-modal.tsx b/packages/compass-connection-import-export/src/components/import-modal.tsx index c096d12c5cd..68b89f9bc99 100644 --- a/packages/compass-connection-import-export/src/components/import-modal.tsx +++ b/packages/compass-connection-import-export/src/components/import-modal.tsx @@ -39,7 +39,7 @@ export function ImportConnectionsModal({ trackingProps?: Record; }): React.ReactElement { const multipleConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const { openToast } = useToast('compass-connection-import-export'); const finish = useCallback( diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts deleted file mode 100644 index 4dd614558ea..00000000000 --- a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.ts +++ /dev/null @@ -1,354 +0,0 @@ -import type React from 'react'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import type { - RenderResult, - RenderHookResult, -} from '@testing-library/react-hooks'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useExportConnections } from './use-export-connections'; -import type { ImportExportResult } from './common'; -import os from 'os'; -import path from 'path'; -import { promises as fs } from 'fs'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { createElement } from 'react'; -import { - ConnectionStorageProvider, - type ConnectionStorage, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { useConnectionRepository } from '@mongodb-js/compass-connections/provider'; - -type UseExportConnectionsProps = Parameters[0]; -type UseExportConnectionsResult = ReturnType; -type UseConnectionRepositoryResult = ReturnType; -type HookResults = { - connectionRepository: UseConnectionRepositoryResult; - exportConnections: UseExportConnectionsResult; -}; - -describe('useExportConnections', function () { - let sandbox: sinon.SinonSandbox; - let finish: sinon.SinonStub; - let finishedPromise: Promise; - let defaultProps: UseExportConnectionsProps; - let renderHookResult: RenderHookResult< - Partial, - HookResults - >; - let result: RenderResult; - let rerender: (props: Partial) => void; - let tmpdir: string; - let connectionStorage: ConnectionStorage; - - beforeEach(async function () { - sandbox = sinon.createSandbox(); - finishedPromise = new Promise((resolve) => { - finish = sinon.stub().callsFake(resolve); - }); - defaultProps = { - finish, - open: true, - trackingProps: { context: 'Tests' }, - }; - connectionStorage = new InMemoryConnectionStorage(); - const wrapper: React.FC = ({ children }) => - createElement(ConnectionStorageProvider, { - value: connectionStorage, - children, - }); - - renderHookResult = renderHook( - (props: Partial = {}) => { - return { - connectionRepository: useConnectionRepository(), - exportConnections: useExportConnections({ - ...defaultProps, - ...props, - }), - }; - }, - { - wrapper, - } - ); - ({ result, rerender } = renderHookResult); - tmpdir = path.join( - os.tmpdir(), - `compass-export-connections-ui-${Date.now()}-${Math.floor( - Math.random() * 1000 - )}` - ); - await fs.mkdir(tmpdir, { recursive: true }); - }); - - afterEach(async function () { - sandbox.restore(); - await fs.rm(tmpdir, { recursive: true }); - }); - - // Security-relevant test -- description is in the protect-connection-strings e2e test. - it('sets removeSecrets if protectConnectionStrings is set', async function () { - expect(result.current.exportConnections.state.removeSecrets).to.equal( - false - ); - act(() => { - result.current.exportConnections.onChangeRemoveSecrets({ - target: { checked: true }, - } as any); - }); - expect(result.current.exportConnections.state.removeSecrets).to.equal(true); - - const preferences = await createSandboxFromDefaultPreferences(); - await preferences.savePreferences({ protectConnectionStrings: true }); - const resultInProtectedMode = renderHook( - () => { - return useExportConnections(defaultProps); - }, - { - wrapper: ({ children }) => - createElement(PreferencesProvider, { children, value: preferences }), - } - ).result; - - expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true); - act(() => { - resultInProtectedMode.current.onChangeRemoveSecrets({ - target: { checked: false }, - } as any); - }); - expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true); - }); - - it('responds to changes in the connectionList', async function () { - expect(result.current.exportConnections.state.connectionList).to.deep.equal( - [] - ); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id1', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name1', - }, - savedConnectionType: 'favorite', - }); - }); - rerender({}); - expect(result.current.exportConnections.state.connectionList).to.deep.equal( - [{ id: 'id1', name: 'name1', selected: true }] - ); - - act(() => { - result.current.exportConnections.onChangeConnectionList([ - { id: 'id1', name: 'name1', selected: false }, - ]); - }); - expect(result.current.exportConnections.state.connectionList).to.deep.equal( - [{ id: 'id1', name: 'name1', selected: false }] - ); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id2', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name2', - }, - savedConnectionType: 'favorite', - }); - }); - - expect(result.current.exportConnections.state.connectionList).to.deep.equal( - [ - { id: 'id1', name: 'name1', selected: false }, - { id: 'id2', name: 'name2', selected: true }, - ] - ); - }); - - it('updates filename if changed', function () { - act(() => { - result.current.exportConnections.onChangeFilename('filename1234'); - }); - expect(result.current.exportConnections.state.filename).to.equal( - 'filename1234' - ); - }); - - it('handles actual export', async function () { - await act(async () => { - await connectionStorage.save?.({ - connectionInfo: { - id: 'id1', - connectionOptions: { - connectionString: 'mongodb://localhost:2020', - }, - savedConnectionType: 'favorite', - favorite: { - name: 'name1', - }, - }, - }); - await connectionStorage.save?.({ - connectionInfo: { - id: 'id2', - connectionOptions: { - connectionString: 'mongodb://localhost:2021', - }, - savedConnectionType: 'favorite', - favorite: { - name: 'name1', - }, - }, - }); - }); - rerender({}); - act(() => { - result.current.exportConnections.onChangeConnectionList([ - { id: 'id1', name: 'name1', selected: false }, - { id: 'id2', name: 'name2', selected: true }, - ]); - }); - - const filename = path.join(tmpdir, 'connections.json'); - const fileContents = '{"connections":[1,2,3]}'; - const exportConnectionStub = sandbox - .stub(connectionStorage, 'exportConnections') - .resolves(fileContents); - - act(() => { - result.current.exportConnections.onChangeFilename(filename); - result.current.exportConnections.onChangePassphrase('s3cr3t'); - }); - - act(() => { - result.current.exportConnections.onSubmit(); - }); - - expect(await finishedPromise).to.equal('succeeded'); - expect(await fs.readFile(filename, 'utf8')).to.equal(fileContents); - expect(exportConnectionStub).to.have.been.calledOnce; - const arg = exportConnectionStub.firstCall.args[0]; - expect(arg?.options?.passphrase).to.equal('s3cr3t'); - expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']); - expect(arg?.options?.trackingProps).to.deep.equal({ context: 'Tests' }); - expect(arg?.options?.removeSecrets).to.equal(false); - }); - - it('resets errors if filename changes', async function () { - const filename = path.join(tmpdir, 'nonexistent', 'connections.json'); - const exportConnectionsStub = sandbox - .stub(connectionStorage, 'exportConnections') - .resolves(''); - - act(() => { - result.current.exportConnections.onChangeFilename(filename); - }); - - expect(result.current.exportConnections.state.inProgress).to.equal(false); - act(() => { - result.current.exportConnections.onSubmit(); - }); - - expect(result.current.exportConnections.state.inProgress).to.equal(true); - expect(result.current.exportConnections.state.error).to.equal(''); - await renderHookResult.waitForValueToChange( - () => result.current.exportConnections.state.inProgress - ); - expect(result.current.exportConnections.state.inProgress).to.equal(false); - expect(result.current.exportConnections.state.error).to.include('ENOENT'); - - expect(exportConnectionsStub).to.have.been.calledOnce; - expect(finish).to.not.have.been.called; - - act(() => { - result.current.exportConnections.onChangeFilename(filename + '-changed'); - }); - - expect(result.current.exportConnections.state.error).to.equal(''); - }); - - context('when multiple connections is enabled', function () { - beforeEach(async function () { - const preferences = await createSandboxFromDefaultPreferences(); - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: true, - }); - const wrapper: React.FC = ({ children }) => - createElement(PreferencesProvider, { - value: preferences, - children: createElement(ConnectionStorageProvider, { - value: connectionStorage, - children, - }), - }); - renderHookResult = renderHook( - (props: Partial = {}) => { - return { - connectionRepository: useConnectionRepository(), - exportConnections: useExportConnections({ - ...defaultProps, - ...props, - }), - }; - }, - { wrapper } - ); - ({ result, rerender } = renderHookResult); - }); - - it('includes also the non-favorites connections in the export list', async function () { - expect( - result.current.exportConnections.state.connectionList - ).to.deep.equal([]); - - // expecting to include the non-favorite connections as well - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id1', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name1', - }, - savedConnectionType: 'recent', - }); - }); - - rerender({}); - expect( - result.current.exportConnections.state.connectionList - ).to.deep.equal([{ id: 'id1', name: 'name1', selected: true }]); - - act(() => { - result.current.exportConnections.onChangeConnectionList([ - { id: 'id1', name: 'name1', selected: false }, - ]); - }); - expect( - result.current.exportConnections.state.connectionList - ).to.deep.equal([{ id: 'id1', name: 'name1', selected: false }]); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id2', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name2', - }, - savedConnectionType: 'recent', - }); - }); - - expect( - result.current.exportConnections.state.connectionList - ).to.deep.equal([ - { id: 'id1', name: 'name1', selected: false }, - { id: 'id2', name: 'name2', selected: true }, - ]); - }); - }); -}); diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx new file mode 100644 index 00000000000..cfcff3677e6 --- /dev/null +++ b/packages/compass-connection-import-export/src/hooks/use-export-connections.spec.tsx @@ -0,0 +1,273 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { useExportConnections } from './use-export-connections'; +import type { ImportExportResult } from './common'; +import os from 'os'; +import path from 'path'; +import { promises as fs } from 'fs'; +import type { RenderConnectionsOptions } from '@mongodb-js/compass-connections/test'; +import { + renderHookWithConnections, + act, + cleanup, + createDefaultConnectionInfo, + waitFor, +} from '@mongodb-js/compass-connections/test'; + +describe('useExportConnections', function () { + let sandbox: sinon.SinonSandbox; + let finish: sinon.SinonStub; + let finishedPromise: Promise; + let tmpdir: string; + + function renderUseExportConnectionsHook( + props?: Partial[0]>, + options?: RenderConnectionsOptions + ) { + return renderHookWithConnections(() => { + return useExportConnections({ + finish, + open: true, + trackingProps: { context: 'Tests' }, + ...props, + }); + }, options); + } + + beforeEach(async function () { + sandbox = sinon.createSandbox(); + finishedPromise = new Promise((resolve) => { + finish = sandbox.stub().callsFake(resolve); + }); + tmpdir = path.join( + os.tmpdir(), + `compass-export-connections-ui-${Date.now()}-${Math.floor( + Math.random() * 1000 + )}` + ); + await fs.mkdir(tmpdir, { recursive: true }); + }); + + afterEach(async function () { + cleanup(); + sandbox.restore(); + await fs.rm(tmpdir, { recursive: true }); + }); + + // Security-relevant test -- description is in the protect-connection-strings e2e test. + it('sets removeSecrets if protectConnectionStrings is set', function () { + const { result } = renderUseExportConnectionsHook(); + + expect(result.current.state.removeSecrets).to.equal(false); + act(() => { + result.current.onChangeRemoveSecrets({ + target: { checked: true }, + } as any); + }); + expect(result.current.state.removeSecrets).to.equal(true); + cleanup(); + + const { result: resultInProtectedMode } = renderUseExportConnectionsHook( + {}, + { preferences: { protectConnectionStrings: true } } + ); + + expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true); + act(() => { + resultInProtectedMode.current.onChangeRemoveSecrets({ + target: { checked: false }, + } as any); + }); + expect(resultInProtectedMode.current.state.removeSecrets).to.equal(true); + }); + + it('responds to changes in the connectionList', async function () { + const connectionInfo1 = createDefaultConnectionInfo(); + const connectionInfo2 = createDefaultConnectionInfo(); + + const { result, connectionsStore, connectionStorage } = + renderUseExportConnectionsHook({}, { connections: [connectionInfo1] }); + + await act(async () => { + await connectionsStore.actions.saveEditedConnection({ + ...connectionInfo1, + favorite: { + name: 'name1', + }, + savedConnectionType: 'favorite', + }); + }); + + expect(result.current.state.connectionList).to.deep.equal( + [ + { + id: connectionInfo1.id, + name: 'name1', + selected: true, + }, + ], + 'expected name of connection 1 to get updated after save' + ); + + act(() => { + result.current.onChangeConnectionList([ + { id: connectionInfo1.id, name: 'name1', selected: false }, + ]); + }); + + expect(result.current.state.connectionList).to.deep.equal( + [{ id: connectionInfo1.id, name: 'name1', selected: false }], + 'expected selected status of connection 1 to change' + ); + + await act(async () => { + await connectionStorage.save?.({ + connectionInfo: { + ...connectionInfo2, + favorite: { + name: 'name2', + }, + savedConnectionType: 'favorite', + }, + }); + await connectionsStore.actions.refreshConnections(); + }); + + expect(result.current.state.connectionList).to.deep.equal([ + { id: connectionInfo1.id, name: 'name1', selected: false }, + { id: connectionInfo2.id, name: 'name2', selected: true }, + ]); + }); + + it('updates filename if changed', function () { + const { result } = renderUseExportConnectionsHook(); + + act(() => { + result.current.onChangeFilename('filename1234'); + }); + expect(result.current.state.filename).to.equal('filename1234'); + }); + + it('handles actual export', async function () { + const { result, connectionStorage } = renderUseExportConnectionsHook( + {}, + { + connections: [ + { + id: 'id1', + connectionOptions: { + connectionString: 'mongodb://localhost:2020', + }, + savedConnectionType: 'favorite', + favorite: { + name: 'name1', + }, + }, + { + id: 'id2', + connectionOptions: { + connectionString: 'mongodb://localhost:2021', + }, + savedConnectionType: 'favorite', + favorite: { + name: 'name1', + }, + }, + ], + } + ); + + act(() => { + result.current.onChangeConnectionList([ + { id: 'id1', name: 'name1', selected: false }, + { id: 'id2', name: 'name2', selected: true }, + ]); + }); + + const filename = path.join(tmpdir, 'connections.json'); + const fileContents = '{"connections":[1,2,3]}'; + const exportConnectionStub = sandbox + .stub(connectionStorage, 'exportConnections') + .resolves(fileContents); + + act(() => { + result.current.onChangeFilename(filename); + result.current.onChangePassphrase('s3cr3t'); + }); + + act(() => { + result.current.onSubmit(); + }); + + expect(await finishedPromise).to.equal('succeeded'); + expect(await fs.readFile(filename, 'utf8')).to.equal(fileContents); + expect(exportConnectionStub).to.have.been.calledOnce; + const arg = exportConnectionStub.firstCall.firstArg; + expect(arg?.options?.passphrase).to.equal('s3cr3t'); + expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']); + expect(arg?.options?.trackingProps).to.deep.equal({ context: 'Tests' }); + expect(arg?.options?.removeSecrets).to.equal(false); + }); + + it('resets errors if filename changes', async function () { + const { result, connectionStorage } = renderUseExportConnectionsHook(); + + const filename = path.join(tmpdir, 'nonexistent', 'connections.json'); + const exportConnectionsStub = sandbox + .stub(connectionStorage, 'exportConnections') + .resolves(''); + + act(() => { + result.current.onChangeFilename(filename); + }); + + expect(result.current.state.inProgress).to.equal(false); + act(() => { + result.current.onSubmit(); + }); + + expect(result.current.state.inProgress).to.equal(true); + expect(result.current.state.error).to.equal(''); + await waitFor(() => { + expect(result.current.state.inProgress).to.equal(false); + }); + expect(result.current.state.error).to.include('ENOENT'); + + expect(exportConnectionsStub).to.have.been.calledOnce; + expect(finish).to.not.have.been.called; + + act(() => { + result.current.onChangeFilename(filename + '-changed'); + }); + + expect(result.current.state.error).to.equal(''); + }); + + context('when multiple connections is enabled', function () { + it('includes also the non-favorites connections in the export list', function () { + const { result } = renderUseExportConnectionsHook( + {}, + { + preferences: { enableMultipleConnectionSystem: true }, + connections: [ + { + id: 'id1', + connectionOptions: { + connectionString: 'mongodb://localhost:2020', + }, + favorite: { + name: 'name1', + }, + // expecting to include the non-favorite connections as well + savedConnectionType: 'recent', + }, + ], + } + ); + + expect(result.current.state.connectionList).to.deep.equal([ + { id: 'id1', name: 'name1', selected: true }, + ]); + }); + }); +}); diff --git a/packages/compass-connection-import-export/src/hooks/use-export-connections.ts b/packages/compass-connection-import-export/src/hooks/use-export-connections.ts index ba2a057e58d..35a6a972660 100644 --- a/packages/compass-connection-import-export/src/hooks/use-export-connections.ts +++ b/packages/compass-connection-import-export/src/hooks/use-export-connections.ts @@ -56,7 +56,7 @@ export function useExportConnections({ state: ExportConnectionsState; } { const multipleConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const { favoriteConnections, nonFavoriteConnections } = useConnectionRepository(); @@ -80,25 +80,28 @@ export function useExportConnections({ } const [state, setState] = useState(INITIAL_STATE); - useEffect(() => setState(INITIAL_STATE), [open]); + useEffect(() => { + setState((prevState) => { + return { + // Reset the form state to initial when modal is open, but keep the list + ...INITIAL_STATE, + connectionList: prevState.connectionList, + }; + }); + }, [open]); const { passphrase, filename, connectionList, removeSecrets } = state; useEffect(() => { // If `connectionsToExport` changes, update the list of connections // that are displayed in our table. - if ( - connectionsToExport.map(({ id }) => id).join(',') !== - state.connectionList.map(({ id }) => id).join(',') - ) { - setState((prevState) => ({ - ...prevState, - connectionList: connectionInfosToConnectionShortInfos( - connectionsToExport, - state.connectionList - ), - })); - } - }, [connectionsToExport, state.connectionList]); + setState((prevState) => ({ + ...prevState, + connectionList: connectionInfosToConnectionShortInfos( + connectionsToExport, + prevState.connectionList + ), + })); + }, [connectionsToExport]); const protectConnectionStrings = !!usePreference('protectConnectionStrings'); useEffect(() => { diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts deleted file mode 100644 index 9f5d3105a91..00000000000 --- a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.ts +++ /dev/null @@ -1,462 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import type { - RenderResult, - RenderHookResult, -} from '@testing-library/react-hooks'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { useImportConnections } from './use-import-connections'; -import type { ImportExportResult } from './common'; -import os from 'os'; -import path from 'path'; -import { promises as fs } from 'fs'; -import { - type ConnectionInfo, - type ConnectionStorage, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { ConnectionStorageProvider } from '@mongodb-js/connection-storage/provider'; -import { useConnectionRepository } from '@mongodb-js/compass-connections/provider'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; - -type UseImportConnectionsProps = Parameters[0]; -type UseImportConnectionsResult = ReturnType; -type UseConnectionRepositoryResult = ReturnType; -type HookResults = { - connectionRepository: UseConnectionRepositoryResult; - importConnections: UseImportConnectionsResult; -}; -const exampleFileContents = '{"a":"b"}'; - -describe('useImportConnections', function () { - let sandbox: sinon.SinonSandbox; - let finish: sinon.SinonStub; - let finishedPromise: Promise; - let defaultProps: UseImportConnectionsProps; - let renderHookResult: RenderHookResult< - Partial, - HookResults - >; - let result: RenderResult; - let rerender: (props: Partial) => void; - let tmpdir: string; - let exampleFile: string; - let connectionStorage: ConnectionStorage; - - beforeEach(async function () { - sandbox = sinon.createSandbox(); - finishedPromise = new Promise((resolve) => { - finish = sinon.stub().callsFake(resolve); - }); - defaultProps = { - finish, - open: true, - trackingProps: { context: 'Tests' }, - }; - connectionStorage = new InMemoryConnectionStorage(); - const wrapper: React.FC = ({ children }) => - React.createElement(ConnectionStorageProvider, { - value: connectionStorage, - children, - }); - renderHookResult = renderHook( - (props: Partial = {}) => { - return { - connectionRepository: useConnectionRepository(), - importConnections: useImportConnections({ - ...defaultProps, - ...props, - }), - }; - }, - { wrapper } - ); - ({ result, rerender } = renderHookResult); - tmpdir = path.join( - os.tmpdir(), - `compass-export-connections-ui-${Date.now()}-${Math.floor( - Math.random() * 1000 - )}` - ); - await fs.mkdir(tmpdir, { recursive: true }); - exampleFile = path.join(tmpdir, 'connections.json'); - await fs.writeFile(exampleFile, exampleFileContents); - }); - - afterEach(async function () { - sandbox.restore(); - await fs.rm(tmpdir, { recursive: true }); - }); - - it('updates filename if changed', async function () { - const deserializeStub = sandbox - .stub(connectionStorage, 'deserializeConnections') - .callsFake(function ({ - content, - options, - }: { - content: string; - options: any; - }) { - expect(content).to.equal(exampleFileContents); - expect(options.passphrase).to.equal(''); - return Promise.resolve([ - { - id: 'id1', - favorite: { name: 'name1' }, - } as ConnectionInfo, - ]); - }); - - act(() => { - result.current.importConnections.onChangeFilename(exampleFile); - }); - expect(result.current.importConnections.state.filename).to.equal( - exampleFile - ); - expect(result.current.importConnections.state.error).to.equal(''); - expect(result.current.importConnections.state.connectionList).to.deep.equal( - [] - ); - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.connectionList.length - ); - - expect(deserializeStub).to.have.been.calledOnce; - expect(result.current.importConnections.state.connectionList).to.deep.equal( - [ - { - id: 'id1', - name: 'name1', - selected: true, - isExistingConnection: false, - }, - ] - ); - }); - - it('updates passphrase if changed', async function () { - sandbox - .stub(connectionStorage, 'deserializeConnections') - .onFirstCall() - .callsFake(function ({ - content, - options, - }: { - content: string; - options: any; - }) { - expect(content).to.equal(exampleFileContents); - expect(options.passphrase).to.equal('wrong'); - throw Object.assign(new Error('wrong password'), { - passphraseRequired: true, - }); - }) - .onSecondCall() - .callsFake(function ({ - content, - options, - }: { - content: string; - options: any; - }) { - expect(content).to.equal(exampleFileContents); - expect(options.passphrase).to.equal('s3cr3t'); - return Promise.resolve([ - { - id: 'id1', - favorite: { name: 'name1' }, - } as ConnectionInfo, - ]); - }); - - act(() => { - result.current.importConnections.onChangeFilename(exampleFile); - result.current.importConnections.onChangePassphrase('wrong'); - }); - expect(result.current.importConnections.state.passphrase).to.equal('wrong'); - - expect(result.current.importConnections.state.error).to.equal(''); - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.error - ); - expect(result.current.importConnections.state.error).to.equal( - 'wrong password' - ); - expect(result.current.importConnections.state.passphraseRequired).to.equal( - true - ); - - act(() => { - result.current.importConnections.onChangePassphrase('s3cr3t'); - }); - expect(result.current.importConnections.state.passphrase).to.equal( - 's3cr3t' - ); - - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.error - ); - - expect(result.current.importConnections.state.error).to.equal(''); - expect(result.current.importConnections.state.passphraseRequired).to.equal( - true - ); - expect( - result.current.importConnections.state.connectionList - ).to.have.lengthOf(1); - }); - - it('does not select existing favorites by default', async function () { - sandbox - .stub(connectionStorage, 'deserializeConnections') - .callsFake(({ content, options }: { content: string; options: any }) => { - expect(content).to.equal(exampleFileContents); - expect(options.passphrase).to.equal(''); - return Promise.resolve([ - { - id: 'id1', - favorite: { name: 'name1' }, - }, - { - id: 'id2', - favorite: { name: 'name2' }, - }, - ] as ConnectionInfo[]); - }); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id1', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name1', - }, - savedConnectionType: 'favorite', - }); - }); - - rerender({}); - act(() => { - result.current.importConnections.onChangeFilename(exampleFile); - }); - - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.connectionList.length - ); - expect(result.current.importConnections.state.connectionList).to.deep.equal( - [ - { - id: 'id1', - name: 'name1', - selected: false, - isExistingConnection: true, - }, - { - id: 'id2', - name: 'name2', - selected: true, - isExistingConnection: false, - }, - ] - ); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id2', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name2', - }, - savedConnectionType: 'favorite', - }); - }); - - rerender({}); - expect(result.current.importConnections.state.connectionList).to.deep.equal( - [ - { - id: 'id1', - name: 'name1', - selected: false, - isExistingConnection: true, - }, - { - id: 'id2', - name: 'name2', - selected: true, - isExistingConnection: true, - }, - ] - ); - }); - - it('handles actual import', async function () { - const connections = [ - { - id: 'id1', - favorite: { name: 'name1' }, - }, - { - id: 'id2', - favorite: { name: 'name2' }, - }, - ]; - sandbox - .stub(connectionStorage, 'deserializeConnections') - .resolves(connections as ConnectionInfo[]); - const importConnectionsStub = sandbox - .stub(connectionStorage, 'importConnections') - .callsFake(({ content }: { content: string }) => { - expect(content).to.equal(exampleFileContents); - return Promise.resolve(); - }); - act(() => { - result.current.importConnections.onChangeFilename(exampleFile); - }); - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.fileContents - ); - - act(() => { - result.current.importConnections.onChangeConnectionList([ - { id: 'id1', name: 'name1', selected: false }, - { id: 'id2', name: 'name2', selected: true }, - ]); - }); - - act(() => { - result.current.importConnections.onSubmit(); - }); - - expect(await finishedPromise).to.equal('succeeded'); - expect(importConnectionsStub).to.have.been.calledOnce; - const arg = importConnectionsStub.firstCall.args[0]; - expect(arg?.options?.trackingProps).to.deep.equal({ - context: 'Tests', - connection_ids: ['id2'], - }); - expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']); - }); - - context('when multiple connections is enabled', function () { - beforeEach(async function () { - const preferences = await createSandboxFromDefaultPreferences(); - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: true, - }); - const wrapper: React.FC = ({ children }) => - React.createElement(PreferencesProvider, { - value: preferences, - children: React.createElement(ConnectionStorageProvider, { - value: connectionStorage, - children, - }), - }); - renderHookResult = renderHook( - (props: Partial = {}) => { - return { - connectionRepository: useConnectionRepository(), - importConnections: useImportConnections({ - ...defaultProps, - ...props, - }), - }; - }, - { wrapper } - ); - ({ result, rerender } = renderHookResult); - }); - it('does not select existing connections (including non-favorites) by default', async function () { - sandbox - .stub(connectionStorage, 'deserializeConnections') - .callsFake( - ({ content, options }: { content: string; options: any }) => { - expect(content).to.equal(exampleFileContents); - expect(options.passphrase).to.equal(''); - // we're expecting both these non-favorite connections to be taken into - // account when performing the diff - return Promise.resolve([ - { - id: 'id1', - favorite: { name: 'name1' }, - savedConnectionType: 'recent', - }, - { - id: 'id2', - favorite: { name: 'name2' }, - savedConnectionType: 'recent', - }, - ] as ConnectionInfo[]); - } - ); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id1', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name1', - }, - savedConnectionType: 'recent', - }); - }); - - rerender({}); - act(() => { - result.current.importConnections.onChangeFilename(exampleFile); - }); - - await renderHookResult.waitForValueToChange( - () => result.current.importConnections.state.connectionList.length - ); - expect( - result.current.importConnections.state.connectionList - ).to.deep.equal([ - { - id: 'id1', - name: 'name1', - selected: false, - isExistingConnection: true, - }, - { - id: 'id2', - name: 'name2', - selected: true, - isExistingConnection: false, - }, - ]); - - await act(async () => { - await result.current.connectionRepository.saveConnection({ - id: 'id2', - connectionOptions: { connectionString: 'mongodb://localhost:2020' }, - favorite: { - name: 'name2', - }, - savedConnectionType: 'recent', - }); - }); - - rerender({}); - expect( - result.current.importConnections.state.connectionList - ).to.deep.equal([ - { - id: 'id1', - name: 'name1', - selected: false, - isExistingConnection: true, - }, - { - id: 'id2', - name: 'name2', - selected: true, - isExistingConnection: true, - }, - ]); - }); - }); -}); diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx new file mode 100644 index 00000000000..6b47e31c8bd --- /dev/null +++ b/packages/compass-connection-import-export/src/hooks/use-import-connections.spec.tsx @@ -0,0 +1,368 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { useImportConnections } from './use-import-connections'; +import type { ImportExportResult } from './common'; +import os from 'os'; +import path from 'path'; +import { promises as fs } from 'fs'; +import { type ConnectionInfo } from '@mongodb-js/connection-storage/provider'; +import type { RenderConnectionsOptions } from '@mongodb-js/compass-connections/test'; +import { + renderHookWithConnections, + waitFor, + act, +} from '@mongodb-js/compass-connections/test'; + +const exampleFileContents = '{"a":"b"}'; + +describe('useImportConnections', function () { + let sandbox: sinon.SinonSandbox; + let finish: sinon.SinonStub; + let finishedPromise: Promise; + let tmpdir: string; + let exampleFile: string; + + function renderUseImportConnectionsHook( + props?: Partial[0]>, + options?: RenderConnectionsOptions + ) { + return renderHookWithConnections(() => { + return useImportConnections({ + finish, + open: true, + trackingProps: { context: 'Tests' }, + ...props, + }); + }, options); + } + + beforeEach(async function () { + sandbox = sinon.createSandbox(); + finishedPromise = new Promise((resolve) => { + finish = sinon.stub().callsFake(resolve); + }); + tmpdir = path.join( + os.tmpdir(), + `compass-export-connections-ui-${Date.now()}-${Math.floor( + Math.random() * 1000 + )}` + ); + await fs.mkdir(tmpdir, { recursive: true }); + exampleFile = path.join(tmpdir, 'connections.json'); + await fs.writeFile(exampleFile, exampleFileContents); + }); + + afterEach(async function () { + sandbox.restore(); + await fs.rm(tmpdir, { recursive: true }); + }); + + it('updates filename if changed', async function () { + const { result, connectionStorage } = renderUseImportConnectionsHook(); + + const deserializeStub = sandbox + .stub(connectionStorage, 'deserializeConnections') + .callsFake(function ({ + content, + options, + }: { + content: string; + options: any; + }) { + expect(content).to.equal(exampleFileContents); + expect(options.passphrase).to.equal(''); + return Promise.resolve([ + { + id: 'id1', + favorite: { name: 'name1' }, + } as ConnectionInfo, + ]); + }); + + act(() => { + result.current.onChangeFilename(exampleFile); + }); + expect(result.current.state.filename).to.equal(exampleFile); + expect(result.current.state.error).to.equal(''); + expect(result.current.state.connectionList).to.deep.equal([]); + await waitFor(() => { + expect(result.current.state.connectionList).to.deep.equal([ + { + id: 'id1', + name: 'name1', + selected: true, + isExistingConnection: false, + }, + ]); + }); + expect(deserializeStub).to.have.been.calledOnce; + }); + + it('updates passphrase if changed', async function () { + const { result, connectionStorage } = renderUseImportConnectionsHook(); + + sandbox + .stub(connectionStorage, 'deserializeConnections') + .onFirstCall() + .callsFake(function ({ + content, + options, + }: { + content: string; + options: any; + }) { + expect(content).to.equal(exampleFileContents); + expect(options.passphrase).to.equal('wrong'); + throw Object.assign(new Error('wrong password'), { + passphraseRequired: true, + }); + }) + .onSecondCall() + .callsFake(function ({ + content, + options, + }: { + content: string; + options: any; + }) { + expect(content).to.equal(exampleFileContents); + expect(options.passphrase).to.equal('s3cr3t'); + return Promise.resolve([ + { + id: 'id1', + favorite: { name: 'name1' }, + } as ConnectionInfo, + ]); + }); + + act(() => { + result.current.onChangeFilename(exampleFile); + result.current.onChangePassphrase('wrong'); + }); + expect(result.current.state.passphrase).to.equal('wrong'); + + expect(result.current.state.error).to.equal(''); + await waitFor(() => { + expect(result.current.state.error).to.equal('wrong password'); + }); + expect(result.current.state.passphraseRequired).to.equal(true); + + act(() => { + result.current.onChangePassphrase('s3cr3t'); + }); + expect(result.current.state.passphrase).to.equal('s3cr3t'); + + await waitFor(() => { + expect(result.current.state.error).to.equal(''); + }); + + expect(result.current.state.passphraseRequired).to.equal(true); + expect(result.current.state.connectionList).to.have.lengthOf(1); + }); + + it('does not select existing favorites by default', async function () { + const { result, connectionStorage, connectionsStore } = + renderUseImportConnectionsHook( + {}, + { + connections: [ + { + id: 'id1', + connectionOptions: { + connectionString: 'mongodb://localhost:2020', + }, + favorite: { + name: 'name1', + }, + savedConnectionType: 'favorite', + }, + ], + } + ); + + sandbox + .stub(connectionStorage, 'deserializeConnections') + .callsFake(({ content, options }: { content: string; options: any }) => { + expect(content).to.equal(exampleFileContents); + expect(options.passphrase).to.equal(''); + return Promise.resolve([ + { + id: 'id1', + favorite: { name: 'name1' }, + }, + { + id: 'id2', + favorite: { name: 'name2' }, + }, + ] as ConnectionInfo[]); + }); + + act(() => { + result.current.onChangeFilename(exampleFile); + }); + + await waitFor(() => { + expect(result.current.state.connectionList).to.deep.equal([ + { + id: 'id1', + name: 'name1', + selected: false, + isExistingConnection: true, + }, + { + id: 'id2', + name: 'name2', + selected: true, + isExistingConnection: false, + }, + ]); + }); + + await connectionStorage.save?.({ + connectionInfo: { + id: 'id2', + connectionOptions: { connectionString: 'mongodb://localhost:2020' }, + favorite: { + name: 'name2', + }, + savedConnectionType: 'favorite', + }, + }); + + await connectionsStore.actions.refreshConnections(); + + await waitFor(() => { + expect(result.current.state.connectionList).to.deep.equal([ + { + id: 'id1', + name: 'name1', + selected: false, + isExistingConnection: true, + }, + { + id: 'id2', + name: 'name2', + selected: true, + isExistingConnection: true, + }, + ]); + }); + }); + + it('handles actual import', async function () { + const { result, connectionStorage } = renderUseImportConnectionsHook(); + + const connections = [ + { + id: 'id1', + favorite: { name: 'name1' }, + }, + { + id: 'id2', + favorite: { name: 'name2' }, + }, + ]; + sandbox + .stub(connectionStorage, 'deserializeConnections') + .resolves(connections as any); + const importConnectionsStub = sandbox + .stub(connectionStorage, 'importConnections') + .callsFake(({ content }: { content: string }) => { + expect(content).to.equal(exampleFileContents); + return Promise.resolve(); + }); + act(() => { + result.current.onChangeFilename(exampleFile); + }); + await waitFor(() => { + expect(result.current.state.fileContents).to.eq(exampleFileContents); + }); + + act(() => { + result.current.onChangeConnectionList([ + { id: 'id1', name: 'name1', selected: false }, + { id: 'id2', name: 'name2', selected: true }, + ]); + }); + + act(() => { + result.current.onSubmit(); + }); + + expect(await finishedPromise).to.equal('succeeded'); + expect(importConnectionsStub).to.have.been.calledOnce; + const arg = importConnectionsStub.firstCall.args[0]; + expect(arg?.options?.trackingProps).to.deep.equal({ + context: 'Tests', + connection_ids: ['id2'], + }); + expect(arg?.options?.filterConnectionIds).to.deep.equal(['id2']); + }); + + context('when multiple connections is enabled', function () { + it('does not select existing connections (including non-favorites) by default', async function () { + const { result, connectionStorage } = renderUseImportConnectionsHook( + {}, + { + preferences: { enableMultipleConnectionSystem: true }, + connections: [ + { + id: 'id1', + connectionOptions: { + connectionString: 'mongodb://localhost:2020', + }, + favorite: { + name: 'name1', + }, + savedConnectionType: 'recent', + }, + ], + } + ); + + sandbox + .stub(connectionStorage, 'deserializeConnections') + .callsFake( + ({ content, options }: { content: string; options: any }) => { + expect(content).to.equal(exampleFileContents); + expect(options.passphrase).to.equal(''); + // we're expecting both these non-favorite connections to be taken into + // account when performing the diff + return Promise.resolve([ + { + id: 'id1', + favorite: { name: 'name1' }, + savedConnectionType: 'recent', + }, + { + id: 'id2', + favorite: { name: 'name2' }, + savedConnectionType: 'recent', + }, + ] as ConnectionInfo[]); + } + ); + + act(() => { + result.current.onChangeFilename(exampleFile); + }); + + await waitFor(() => { + expect(result.current.state.connectionList).to.deep.equal([ + { + id: 'id1', + name: 'name1', + selected: false, + isExistingConnection: true, + }, + { + id: 'id2', + name: 'name2', + selected: true, + isExistingConnection: false, + }, + ]); + }); + }); + }); +}); diff --git a/packages/compass-connection-import-export/src/hooks/use-import-connections.ts b/packages/compass-connection-import-export/src/hooks/use-import-connections.ts index afaa46a5f6c..4685d422c84 100644 --- a/packages/compass-connection-import-export/src/hooks/use-import-connections.ts +++ b/packages/compass-connection-import-export/src/hooks/use-import-connections.ts @@ -13,7 +13,10 @@ import type { ConnectionShortInfo, CommonImportExportState, } from './common'; -import { useConnectionRepository } from '@mongodb-js/compass-connections/provider'; +import { + useConnectionActions, + useConnectionRepository, +} from '@mongodb-js/compass-connections/provider'; import { usePreference } from 'compass-preferences-model/provider'; type ConnectionImportInfo = ConnectionShortInfo & { @@ -101,10 +104,11 @@ export function useImportConnections({ state: ImportConnectionsState; } { const multipleConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const { favoriteConnections, nonFavoriteConnections } = useConnectionRepository(); + const { importConnections } = useConnectionActions(); const existingConnections = useMemo(() => { // in case of multiple connections all the connections are saved (that used // to be favorites in the single connection world) so we need to account for @@ -116,18 +120,24 @@ export function useImportConnections({ } }, [multipleConnectionsEnabled, favoriteConnections, nonFavoriteConnections]); const connectionStorage = useConnectionStorageContext(); - const importConnectionsImpl = - connectionStorage.importConnections?.bind(connectionStorage); const deserializeConnectionsImpl = connectionStorage.deserializeConnections?.bind(connectionStorage); - if (!importConnectionsImpl || !deserializeConnectionsImpl) { + if (!deserializeConnectionsImpl) { throw new Error( 'Import Connections feature requires the provided ConnectionStorage to implement importConnections and deserializeConnections' ); } const [state, setState] = useState(INITIAL_STATE); - useEffect(() => setState(INITIAL_STATE), [open]); + useEffect(() => { + // Reset the form state to initial when modal is open, but keep the list + setState((prevState) => { + return { + ...INITIAL_STATE, + connectionList: prevState.connectionList, + }; + }); + }, [open]); const { passphrase, filename, fileContents, connectionList } = state; const existingConnectionIds = existingConnections.map(({ id }) => id); @@ -153,7 +163,7 @@ export function useImportConnections({ .filter((x) => x.selected) .map((x) => x.id); try { - await importConnectionsImpl({ + await importConnections({ content: fileContents, options: { passphrase, diff --git a/packages/compass-connections-navigation/package.json b/packages/compass-connections-navigation/package.json index 2f52059b82a..a50ec07b35e 100644 --- a/packages/compass-connections-navigation/package.json +++ b/packages/compass-connections-navigation/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.37.0", + "version": "1.38.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -23,9 +23,11 @@ ], "license": "SSPL", "main": "dist/index.js", + "types": "dist/index.d.ts", "compass:main": "src/index.ts", "exports": { - "require": "./dist/index.js" + "require": "./dist/index.js", + "types": "./dist/index.d.ts" }, "compass:exports": { ".": "./src/index.ts" @@ -47,20 +49,20 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/compass-workspaces": "^0.19.0", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/compass-workspaces": "^0.20.0", + "compass-preferences-model": "^2.27.0", "mongodb-build-info": "^1.7.2", "react": "^17.0.2", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx index 7362bb4ea33..971d2620901 100644 --- a/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx +++ b/packages/compass-connections-navigation/src/connections-navigation-tree.spec.tsx @@ -140,7 +140,7 @@ describe('ConnectionsNavigationTree', function () { preferences = await createSandboxFromDefaultPreferences(); await preferences.savePreferences({ enableRenameCollectionModal: true, - enableNewMultipleConnectionSystem: true, + enableMultipleConnectionSystem: true, ...preferencesOverrides, }); return render( @@ -659,7 +659,7 @@ describe('ConnectionsNavigationTree', function () { preferences = await createSandboxFromDefaultPreferences(); await preferences.savePreferences({ enableRenameCollectionModal: true, - enableNewMultipleConnectionSystem: true, + enableMultipleConnectionSystem: true, }); }); diff --git a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx index 470d4a9fe66..98937558333 100644 --- a/packages/compass-connections-navigation/src/connections-navigation-tree.tsx +++ b/packages/compass-connections-navigation/src/connections-navigation-tree.tsx @@ -27,7 +27,6 @@ import { databaseItemActions, notConnectedConnectionItemActions, } from './item-actions'; -import { ConnectionStatus } from '@mongodb-js/compass-connections/provider'; const MCContainer = css({ display: 'flex', @@ -60,9 +59,7 @@ const ConnectionsNavigationTree: React.FunctionComponent< }) => { const preferencesShellEnabled = usePreference('enableShell'); const preferencesReadOnly = usePreference('readOnly'); - const isSingleConnection = !usePreference( - 'enableNewMultipleConnectionSystem' - ); + const isSingleConnection = !usePreference('enableMultipleConnectionSystem'); const isRenameCollectionEnabled = usePreference( 'enableRenameCollectionModal' ); @@ -88,12 +85,9 @@ const ConnectionsNavigationTree: React.FunctionComponent< const onDefaultAction: OnDefaultAction = useCallback( (item, evt) => { if (item.type === 'connection') { - if (item.connectionStatus === ConnectionStatus.Connected) { + if (item.connectionStatus === 'connected') { onItemAction(item, 'select-connection'); - } else if ( - item.connectionStatus === ConnectionStatus.Disconnected || - item.connectionStatus === ConnectionStatus.Failed - ) { + } else { onItemAction(item, 'connection-connect'); } } else if (item.type === 'database') { @@ -173,7 +167,7 @@ const ConnectionsNavigationTree: React.FunctionComponent< actions: [], }; case 'connection': { - if (item.connectionStatus === ConnectionStatus.Connected) { + if (item.connectionStatus === 'connected') { const actions = connectedConnectionItemActions({ hasWriteActionsDisabled: item.hasWriteActionsDisabled, isShellEnabled: item.isShellEnabled, diff --git a/packages/compass-connections-navigation/src/navigation-item.tsx b/packages/compass-connections-navigation/src/navigation-item.tsx index e10469f84e8..e655bbe04f9 100644 --- a/packages/compass-connections-navigation/src/navigation-item.tsx +++ b/packages/compass-connections-navigation/src/navigation-item.tsx @@ -257,8 +257,7 @@ export function NavigationItem({ dataAttributes={itemDataProps} isExpandVisible={item.isExpandable} isExpandDisabled={ - item.type === 'connection' && - item.connectionStatus === 'disconnected' + item.type === 'connection' && item.connectionStatus !== 'connected' } onExpand={(isExpanded: boolean) => { onItemExpand(item, isExpanded); diff --git a/packages/compass-connections-navigation/src/placeholder.tsx b/packages/compass-connections-navigation/src/placeholder.tsx index 6d75bbc9665..2b6523a29ad 100644 --- a/packages/compass-connections-navigation/src/placeholder.tsx +++ b/packages/compass-connections-navigation/src/placeholder.tsx @@ -23,9 +23,7 @@ export const PlaceholderItem: React.FunctionComponent<{ level: number; style?: CSSProperties; }> = ({ level, style }) => { - const isSingleConnection = !usePreference( - 'enableNewMultipleConnectionSystem' - ); + const isSingleConnection = !usePreference('enableMultipleConnectionSystem'); const itemPaddingStyles = useMemo( () => getTreeItemStyles({ level, isExpandable: false }), [level] diff --git a/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx b/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx index c6755e046df..663fb9a5640 100644 --- a/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx +++ b/packages/compass-connections-navigation/src/sc-connections-navigation-tree.spec.tsx @@ -67,7 +67,7 @@ const activeWorkspace = { const dummyPreferences = { getPreferences() { return { - enableNewMultipleConnectionSystem: false, + enableMultipleConnectionSystem: false, }; }, onPreferenceValueChanged() {}, @@ -93,7 +93,8 @@ function renderComponent( ); } -describe('ConnectionsNavigationTree -- Single connection usage', function () { +// TODO(COMPASS-7906): remove +describe.skip('ConnectionsNavigationTree -- Single connection usage', function () { let preferences: PreferencesAccess; afterEach(cleanup); @@ -106,7 +107,7 @@ describe('ConnectionsNavigationTree -- Single connection usage', function () { beforeEach(async function () { await preferences.savePreferences({ enableRenameCollectionModal: true, - enableNewMultipleConnectionSystem: false, + enableMultipleConnectionSystem: false, }); renderComponent( diff --git a/packages/compass-connections-navigation/src/styled-navigation-item.tsx b/packages/compass-connections-navigation/src/styled-navigation-item.tsx index 0693dde0619..a31c978bcca 100644 --- a/packages/compass-connections-navigation/src/styled-navigation-item.tsx +++ b/packages/compass-connections-navigation/src/styled-navigation-item.tsx @@ -25,9 +25,7 @@ export default function StyledNavigationItem({ const isDarkMode = useDarkMode(); const { connectionColorToHex, connectionColorToHexActive } = useConnectionColor(); - const isSingleConnection = !usePreference( - 'enableNewMultipleConnectionSystem' - ); + const isSingleConnection = !usePreference('enableMultipleConnectionSystem'); const { colorCode } = item; const isDisconnectedConnection = item.type === 'connection' && diff --git a/packages/compass-connections-navigation/src/tree-data.ts b/packages/compass-connections-navigation/src/tree-data.ts index a485db6c0d5..320214beea7 100644 --- a/packages/compass-connections-navigation/src/tree-data.ts +++ b/packages/compass-connections-navigation/src/tree-data.ts @@ -18,9 +18,11 @@ type DatabaseOrCollectionStatus = | 'error'; export type NotConnectedConnectionStatus = - | ConnectionStatus.Connecting - | ConnectionStatus.Disconnected - | ConnectionStatus.Failed; + | 'initial' + | 'connecting' + | 'disconnected' + | 'canceled' + | 'failed'; export type NotConnectedConnection = { name: string; @@ -31,7 +33,7 @@ export type NotConnectedConnection = { export type ConnectedConnection = { name: string; connectionInfo: ConnectionInfo; - connectionStatus: ConnectionStatus.Connected; + connectionStatus: 'connected'; isReady: boolean; isDataLake: boolean; isWritable: boolean; @@ -80,7 +82,7 @@ export type ConnectedConnectionTreeItem = VirtualTreeItem & { colorCode?: string; isExpanded: boolean; connectionInfo: ConnectionInfo; - connectionStatus: ConnectionStatus.Connected; + connectionStatus: 'connected'; isPerformanceTabSupported: boolean; hasWriteActionsDisabled: boolean; isShellEnabled: boolean; diff --git a/packages/compass-connections-navigation/src/with-status-marker.tsx b/packages/compass-connections-navigation/src/with-status-marker.tsx index 977344c0174..b10daf1c389 100644 --- a/packages/compass-connections-navigation/src/with-status-marker.tsx +++ b/packages/compass-connections-navigation/src/with-status-marker.tsx @@ -7,9 +7,11 @@ import { import React from 'react'; export type StatusMarker = + | 'initial' | 'connected' | 'disconnected' | 'connecting' + | 'canceled' | 'failed'; export type StatusMarkerProps = { status: StatusMarker; @@ -92,6 +94,8 @@ const MARKER_COMPONENTS: Record = { connecting: ConnectingStatusMarker, failed: FailedStatusMarker, disconnected: NoMarker, + initial: NoMarker, + canceled: NoMarker, } as const; const withStatusMarkerStyles = css({ diff --git a/packages/compass-connections/package.json b/packages/compass-connections/package.json index 17f292a1f25..bf0b2bb1011 100644 --- a/packages/compass-connections/package.json +++ b/packages/compass-connections/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.38.0", + "version": "1.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -25,10 +25,11 @@ ], "license": "SSPL", "main": "dist/index.js", - "compass:main": "src/index.ts", + "compass:main": "src/index.tsx", "compass:exports": { - ".": "./src/index.ts", - "./provider": "./src/provider.ts" + ".": "./src/index.tsx", + "./provider": "./src/provider.ts", + "./test": "./src/test.tsx" }, "types": "./dist/index.d.ts", "scripts": { @@ -51,31 +52,33 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/connection-storage": "^0.17.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/connection-storage": "^0.18.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-build-info": "^1.7.2", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3", - "react": "^17.0.2" + "mongodb-data-service": "^22.23.0", + "react": "^17.0.2", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/dom": "^8.20.1", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", diff --git a/packages/compass-connections/src/components/connection-status-notifications.tsx b/packages/compass-connections/src/components/connection-status-notifications.tsx index 9abeb860519..32d6cddea7a 100644 --- a/packages/compass-connections/src/components/connection-status-notifications.tsx +++ b/packages/compass-connections/src/components/connection-status-notifications.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { Body, Code, @@ -6,7 +6,8 @@ import { Link, showConfirmation, spacing, - useToast, + openToast, + closeToast, } from '@mongodb-js/compass-components'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; import { getConnectionTitle } from '@mongodb-js/connection-info'; @@ -90,158 +91,135 @@ const deviceAuthModalContentStyles = css({ }, }); -/** - * Returns triggers for various notifications (toasts and modals) that are - * supposed to be displayed every time connection flow is happening in the - * application. - * - * All toasts and modals are only applicable in multiple connections mode. Right - * now it's gated by the feature flag, the flag check can be removed when this - * is the default behavior - */ -export function useConnectionStatusNotifications() { - const enableNewMultipleConnectionSystem = usePreference( - 'enableNewMultipleConnectionSystem' - ); - const { openToast, closeToast } = useToast('connection-status'); - - const openConnectionStartedToast = useCallback( - (connectionInfo: ConnectionInfo, onCancelClick: () => void) => { - const { title, description } = getConnectingStatusText(connectionInfo); - openToast(connectionInfo.id, { - title, - description, - dismissible: true, - variant: 'progress', - actionElement: ( - { - closeToast(connectionInfo.id); - onCancelClick(); - }} - data-testid="cancel-connection-button" - > - CANCEL - - ), - }); - }, - [closeToast, openToast] - ); +const openConnectionStartedToast = ( + connectionInfo: ConnectionInfo, + onCancelClick: () => void +) => { + const { title, description } = getConnectingStatusText(connectionInfo); + openToast(`connection-status--${connectionInfo.id}`, { + title, + description, + dismissible: true, + variant: 'progress', + actionElement: ( + { + closeToast(`connection-status--${connectionInfo.id}`); + onCancelClick(); + }} + data-testid="cancel-connection-button" + > + CANCEL + + ), + }); +}; - const openConnectionSucceededToast = useCallback( - (connectionInfo: ConnectionInfo) => { - openToast(connectionInfo.id, { - title: `Connected to ${getConnectionTitle(connectionInfo)}`, - variant: 'success', - timeout: 3_000, - }); - }, - [openToast] - ); +const openConnectionSucceededToast = (connectionInfo: ConnectionInfo) => { + openToast(`connection-status--${connectionInfo.id}`, { + title: `Connected to ${getConnectionTitle(connectionInfo)}`, + variant: 'success', + timeout: 3_000, + }); +}; - const openConnectionFailedToast = useCallback( - ( - // Connection info might be missing if we failed connecting before we - // could even resolve connection info. Currently the only case where this - // can happen is autoconnect flow - connectionInfo: ConnectionInfo | null | undefined, - error: Error, - onReviewClick: () => void - ) => { - const failedToastId = connectionInfo?.id ?? 'failed'; - - openToast(failedToastId, { - title: error.message, - description: ( - { - closeToast(failedToastId); - onReviewClick(); - }} - /> - ), - variant: 'warning', - }); - }, - [closeToast, openToast] - ); +const openConnectionFailedToast = ( + // Connection info might be missing if we failed connecting before we + // could even resolve connection info. Currently the only case where this + // can happen is autoconnect flow + connectionInfo: ConnectionInfo | null | undefined, + error: Error, + onReviewClick: () => void +) => { + const failedToastId = connectionInfo?.id ?? 'failed'; + + openToast(`connection-status--${failedToastId}`, { + title: error.message, + description: ( + { + closeToast(`connection-status--${failedToastId}`); + onReviewClick(); + }} + /> + ), + variant: 'warning', + }); +}; - const openMaximumConnectionsReachedToast = useCallback( - (maxConcurrentConnections: number) => { - const message = `Only ${maxConcurrentConnections} connection${ - maxConcurrentConnections > 1 ? 's' : '' - } can be connected to at the same time. First disconnect from another connection.`; - - openToast('max-connections-reached', { - title: 'Maximum concurrent connections limit reached', - description: message, - variant: 'warning', - timeout: 5_000, - }); - }, - [openToast] - ); +const openMaximumConnectionsReachedToast = ( + maxConcurrentConnections: number +) => { + const message = `Only ${maxConcurrentConnections} connection${ + maxConcurrentConnections > 1 ? 's' : '' + } can be connected to at the same time. First disconnect from another connection.`; + + openToast('max-connections-reached', { + title: 'Maximum concurrent connections limit reached', + description: message, + variant: 'warning', + timeout: 5_000, + }); +}; - const openNotifyDeviceAuthModal = useCallback( - ( - connectionInfo: ConnectionInfo, - verificationUrl: string, - userCode: string, - onCancel: () => void, - signal: AbortSignal - ) => { - void showConfirmation({ - title: `Complete authentication in the browser`, - description: ( -
- - Visit the following URL to complete authentication for{' '} - {getConnectionTitle(connectionInfo)}: - - - - {verificationUrl} - - -

- Enter the following code on that page: - - - {userCode} - - -
- ), - hideConfirmButton: true, - signal, - }).then( - (result) => { - if (result === false) { - onCancel?.(); - } - }, - () => { - // Abort signal was triggered - } - ); +const openNotifyDeviceAuthModal = ( + connectionInfo: ConnectionInfo, + verificationUrl: string, + userCode: string, + onCancel: () => void, + signal: AbortSignal +) => { + void showConfirmation({ + title: `Complete authentication in the browser`, + description: ( +
+ + Visit the following URL to complete authentication for{' '} + {getConnectionTitle(connectionInfo)}: + + + + {verificationUrl} + + +

+ Enter the following code on that page: + + + {userCode} + + +
+ ), + hideConfirmButton: true, + signal, + }).then( + (result) => { + if (result === false) { + onCancel?.(); + } }, - [] + () => { + // Abort signal was triggered + } ); +}; - // Gated by the feature flag: if flag is on, we return trigger functions, if - // flag is off, we return noop functions so that we can call them - // unconditionally in the actual flow - return enableNewMultipleConnectionSystem +export function getNotificationTriggers( + enableMultipleConnectionSystem: boolean +) { + return enableMultipleConnectionSystem ? { openNotifyDeviceAuthModal, openConnectionStartedToast, openConnectionSucceededToast, openConnectionFailedToast, openMaximumConnectionsReachedToast, - closeConnectionStatusToast: closeToast, + closeConnectionStatusToast: (connectionId: string) => { + return closeToast(`connection-status--${connectionId}`); + }, } : { openNotifyDeviceAuthModal: noop, @@ -252,3 +230,23 @@ export function useConnectionStatusNotifications() { closeConnectionStatusToast: noop, }; } + +/** + * Returns triggers for various notifications (toasts and modals) that are + * supposed to be displayed every time connection flow is happening in the + * application. + * + * All toasts and modals are only applicable in multiple connections mode. Right + * now it's gated by the feature flag, the flag check can be removed when this + * is the default behavior + */ +export function useConnectionStatusNotifications() { + const enableMultipleConnectionSystem = usePreference( + 'enableMultipleConnectionSystem' + ); + + // Gated by the feature flag: if flag is on, we return trigger functions, if + // flag is off, we return noop functions so that we can call them + // unconditionally in the actual flow + return getNotificationTriggers(enableMultipleConnectionSystem); +} diff --git a/packages/compass-connections/src/components/connections-provider.tsx b/packages/compass-connections/src/components/connections-provider.tsx deleted file mode 100644 index ffd3392d297..00000000000 --- a/packages/compass-connections/src/components/connections-provider.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useContext, useEffect, useRef } from 'react'; -import { type ConnectionInfo, useConnectionsManagerContext } from '../provider'; -import { useConnections as useConnectionsStore } from '../stores/connections-store'; -import { useConnectionRepository as useConnectionsRepositoryState } from '../hooks/use-connection-repository'; -import { createServiceLocator } from 'hadron-app-registry'; - -const ConnectionsStoreContext = React.createContext | null>(null); - -const ConnectionsRepositoryStateContext = React.createContext | null>(null); - -type UseConnectionsParams = Parameters[0]; - -const ConnectionsStoreProvider: React.FunctionComponent< - UseConnectionsParams -> = ({ children, ...useConnectionsParams }) => { - const connectionsStore = useConnectionsStore(useConnectionsParams); - return ( - - {children} - - ); -}; - -export const ConnectionsProvider: React.FunctionComponent< - UseConnectionsParams -> = ({ children, ...useConnectionsParams }) => { - const connectionsManagerRef = useRef(useConnectionsManagerContext()); - const connectionsRepositoryState = useConnectionsRepositoryState(); - useEffect(() => { - const cm = connectionsManagerRef.current; - return () => { - void cm.closeAllConnections(); - }; - }, []); - return ( - - - {children} - - - ); -}; - -export function useConnections() { - const store = useContext(ConnectionsStoreContext); - if (!store) { - // TODO(COMPASS-7879): implement a default provider in test methods - if (process.env.NODE_ENV === 'test') { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useConnectionsStore(); - } - throw new Error( - 'Can not use useConnections outside of ConnectionsProvider component' - ); - } - return store; -} - -export function useConnectionRepository() { - const repository = useContext(ConnectionsRepositoryStateContext); - if (!repository) { - // TODO(COMPASS-7879): implement a default provider in test methods - if (process.env.NODE_ENV === 'test') { - // eslint-disable-next-line react-hooks/rules-of-hooks - return useConnectionsRepositoryState(); - } - throw new Error( - 'Can not use useConnectionRepository outside of ConnectionsProvider component' - ); - } - return repository; -} - -type FirstArgument = F extends (...args: [infer A, ...any]) => any - ? A - : F extends { new (...args: [infer A, ...any]): any } - ? A - : never; - -function withConnectionRepository< - T extends ((...args: any[]) => any) | { new (...args: any[]): any } ->( - ReactComponent: T -): React.FunctionComponent, 'connectionRepository'>> { - const WithConnectionRepository = ( - props: Omit, 'connectionRepository'> & React.Attributes - ) => { - const connectionRepository = useConnectionRepository(); - return React.createElement(ReactComponent, { - ...props, - connectionRepository, - }); - }; - return WithConnectionRepository; -} - -export { withConnectionRepository }; - -export type ConnectionRepositoryAccess = Pick< - ConnectionRepository, - 'getConnectionInfoById' ->; - -export const useConnectionRepositoryAccess = (): ConnectionRepositoryAccess => { - const repository = useConnectionRepository(); - const repositoryRef = useRef(repository); - repositoryRef.current = repository; - return { - getConnectionInfoById(id: ConnectionInfo['id']) { - return repositoryRef.current.getConnectionInfoById(id); - }, - }; -}; -export const connectionRepositoryAccessLocator = createServiceLocator( - useConnectionRepositoryAccess, - 'connectionRepositoryAccessLocator' -); - -export type ConnectionRepository = ReturnType; -export { areConnectionsEqual } from '../hooks/use-connection-repository'; diff --git a/packages/compass-connections/src/components/legacy-connections.spec.tsx b/packages/compass-connections/src/components/legacy-connections.spec.tsx index 5a4f5cd553c..ba498d552ca 100644 --- a/packages/compass-connections/src/components/legacy-connections.spec.tsx +++ b/packages/compass-connections/src/components/legacy-connections.spec.tsx @@ -1,92 +1,47 @@ import React from 'react'; -import { - cleanup, - render, - screen, - waitFor, - fireEvent, -} from '@testing-library/react'; import { expect } from 'chai'; -import type { ConnectionOptions, connect } from 'mongodb-data-service'; import { UUID } from 'bson'; import sinon from 'sinon'; import Connections from './legacy-connections'; -import { ToastArea } from '@mongodb-js/compass-components'; -import type { PreferencesAccess } from 'compass-preferences-model'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; +import type { ConnectionInfo } from '../connection-info-provider'; import { - InMemoryConnectionStorage, - ConnectionStorageProvider, - type ConnectionStorage, - type ConnectionInfo, -} from '@mongodb-js/connection-storage/provider'; -import { ConnectionsManager, ConnectionsManagerProvider } from '../provider'; -import type { DataService } from 'mongodb-data-service'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { ConnectionsProvider } from './connections-provider'; - -function getConnectionsManager(mockTestConnectFn?: typeof connect) { - const { log } = createNoopLogger(); - return new ConnectionsManager({ - logger: log.unbound, - __TEST_CONNECT_FN: mockTestConnectFn, - }); -} + renderWithConnections, + screen, + userEvent, + waitFor, + cleanup, +} from '../test'; async function loadSavedConnectionAndConnect(connectionInfo: ConnectionInfo) { const savedConnectionButton = screen.getByTestId( `saved-connection-button-${connectionInfo.id}` ); - fireEvent.click(savedConnectionButton); + userEvent.click(savedConnectionButton); // Wait for the connection to load in the form. await waitFor(() => - expect(screen.queryByRole('textbox')?.textContent).to.equal( + expect(screen.queryByTestId('connectionString')?.textContent).to.equal( connectionInfo.connectionOptions.connectionString ) ); - const connectButton = screen.getByText('Connect'); - fireEvent.click(connectButton); + const connectButton = screen.getByRole('button', { name: 'Connect' }); + userEvent.click(connectButton); // Wait for the connecting... modal to hide. await waitFor(() => expect(screen.queryByText('Cancel')).to.not.exist); } -describe('Connections Component', function () { - let preferences: PreferencesAccess; - - before(async function () { - preferences = await createSandboxFromDefaultPreferences(); - await preferences.savePreferences({ persistOIDCTokens: false }); - }); - +// TODO(COMPASS-7906): remove +describe.skip('Connections Component', function () { afterEach(function () { sinon.restore(); cleanup(); }); context('when rendered', function () { - let loadConnectionsSpy: sinon.SinonSpy; beforeEach(function () { - const mockStorage = new InMemoryConnectionStorage([]); - loadConnectionsSpy = sinon.spy(mockStorage, 'loadAll'); - render( - - - - - - - - - - ); - }); - - it('calls once to load the connections', function () { - expect(loadConnectionsSpy.callCount).to.equal(1); + renderWithConnections(); }); it('renders the connect button from the connect-form', function () { @@ -128,17 +83,16 @@ describe('Connections Component', function () { }); context('when rendered with saved connections in storage', function () { - let connectSpyFn: sinon.SinonSpy; - let mockStorage: ConnectionStorage; let savedConnectionId: string; let savedConnectionWithAppNameId: string; - let saveConnectionSpy: sinon.SinonSpy; let connections: ConnectionInfo[]; + let connectSpyFn: sinon.SinonSpy; + let saveConnectionSpy: sinon.SinonSpy; + let getState; beforeEach(async function () { savedConnectionId = new UUID().toString(); savedConnectionWithAppNameId = new UUID().toString(); - saveConnectionSpy = sinon.spy(); connections = [ { @@ -156,30 +110,23 @@ describe('Connections Component', function () { }, }, ]; - mockStorage = new InMemoryConnectionStorage(connections); - sinon.replace(mockStorage, 'save', saveConnectionSpy); - - const connectionsManager = getConnectionsManager(() => { - return Promise.resolve({ - mockDataService: 'yes', - addReauthenticationHandler() {}, - } as unknown as DataService); - }); - connectSpyFn = sinon.spy(connectionsManager, 'connect'); - - render( - - - - - - - - - + + connectSpyFn = sinon.stub().returns({}); + + const { connectionsStore, connectionStorage } = renderWithConnections( + , + { + connections, + connectFn: connectSpyFn, + } ); - await waitFor(() => expect(screen.queryAllByRole('listitem')).to.exist); + saveConnectionSpy = sinon.spy(connectionStorage, 'save'); + getState = connectionsStore.getState; + + await waitFor(() => { + expect(screen.queryAllByRole('listitem')).to.exist; + }); }); it('should render the saved connections', function () { @@ -200,54 +147,28 @@ describe('Connections Component', function () { context( 'when a saved connection is clicked on and connected to', function () { - const _Date = globalThis.Date; beforeEach(async function () { - globalThis.Date = class { - constructor() { - return new _Date(0); - } - static now() { - return 0; - } - } as DateConstructor; await loadSavedConnectionAndConnect( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion connections.find(({ id }) => id === savedConnectionId)! ); }); - afterEach(function () { - globalThis.Date = _Date; - }); - - it('should call the connect function on ConnectionsManager with the connection options to connect', function () { + it('should call the connect function with the connection options to connect', function () { expect(connectSpyFn.callCount).to.equal(1); - expect( - connectSpyFn.firstCall.args[0].connectionOptions - ).to.deep.equal({ - connectionString: - 'mongodb://localhost:27018/?readPreference=primary&ssl=false', - }); + expect(connectSpyFn.firstCall.args[0]).to.have.property( + 'connectionString', + 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=TEST' + ); }); - it('should call to save the connection with the connection config', function () { + it('should call to save the connection', function () { expect(saveConnectionSpy.callCount).to.equal(1); - expect( - saveConnectionSpy.firstCall.args[0].connectionInfo.id - ).to.equal(savedConnectionId); - expect( - saveConnectionSpy.firstCall.args[0].connectionInfo.connectionOptions - ).to.deep.equal({ - connectionString: - 'mongodb://localhost:27018/?readPreference=primary&ssl=false', - }); }); - it('should call to save the connection with a new lastUsed time', function () { - expect(saveConnectionSpy.callCount).to.equal(1); + it('should update the connection with a new lastUsed time', function () { expect( - saveConnectionSpy.firstCall.args[0].connectionInfo.lastUsed.getTime() - ).to.equal(0); + getState().connections.byId[savedConnectionId].info + ).to.have.property('lastUsed'); }); } ); @@ -264,12 +185,10 @@ describe('Connections Component', function () { it('should call the connect function without replacing appName', function () { expect(connectSpyFn.callCount).to.equal(1); - expect( - connectSpyFn.firstCall.args[0].connectionOptions - ).to.deep.equal({ - connectionString: - 'mongodb://localhost:27019/?appName=Some+App+Name', - }); + expect(connectSpyFn.firstCall.args[0]).to.have.property( + 'connectionString', + 'mongodb://localhost:27019/?appName=Some+App+Name' + ); }); } ); @@ -278,48 +197,15 @@ describe('Connections Component', function () { context( 'when connecting to a connection that is not succeeding', function () { - let mockConnectFn: sinon.SinonSpy; - let saveConnectionSpy: sinon.SinonSpy; let savedConnectableId: string; let savedUnconnectableId: string; let connections: ConnectionInfo[]; let connectSpyFn: sinon.SinonSpy; + let saveConnectionSpy: sinon.SinonSpy; beforeEach(async function () { - saveConnectionSpy = sinon.spy(); savedConnectableId = new UUID().toString(); savedUnconnectableId = new UUID().toString(); - - mockConnectFn = sinon.fake( - async ({ - connectionOptions, - }: { - connectionOptions: ConnectionOptions; - }) => { - if ( - connectionOptions.connectionString === - 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000' - ) { - return new Promise((resolve) => { - // On first call we want this attempt to be cancelled before - // this promise resolves. - setTimeout(() => { - resolve({ - mockDataService: 'yes', - addReauthenticationHandler() {}, - }); - }, 500); - }); - } - return Promise.resolve({ - mockDataService: 'yes', - addReauthenticationHandler() {}, - }); - } - ); - - const connectionsManager = getConnectionsManager(mockConnectFn); - connectSpyFn = sinon.spy(connectionsManager, 'connect'); connections = [ { id: savedConnectableId, @@ -336,21 +222,28 @@ describe('Connections Component', function () { }, }, ]; - const mockStorage = new InMemoryConnectionStorage(connections); - sinon.replace(mockStorage, 'save', saveConnectionSpy); - - render( - - - - - - - - - + + connectSpyFn = sinon + .stub() + // On first call we cancel it, so just never resolve to give UI time + // to render the connecting... state + .onFirstCall() + .callsFake(() => { + return new Promise(() => {}); + }) + // On second call connect successfully without blocking + .onSecondCall() + .callsFake(() => { + return {}; + }); + + const { connectionStorage } = renderWithConnections( + , + { connections, connectFn: connectSpyFn } ); + saveConnectionSpy = sinon.spy(connectionStorage, 'save'); + await waitFor( () => expect( @@ -363,31 +256,35 @@ describe('Connections Component', function () { const savedConnectionButton = screen.getByTestId( `saved-connection-button-${savedUnconnectableId}` ); - fireEvent.click(savedConnectionButton); + userEvent.click(savedConnectionButton); // Wait for the connection to load in the form. await waitFor(() => - expect(screen.queryByRole('textbox')?.textContent).to.equal( + expect( + screen.queryByTestId('connectionString')?.textContent + ).to.equal( 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000' ) ); - const connectButton = screen.getByText('Connect'); - fireEvent.click(connectButton); + const connectButton = screen.getByRole('button', { name: 'Connect' }); + userEvent.click(connectButton); // Wait for the connecting... modal to be shown. - await waitFor(() => expect(screen.queryByText('Cancel')).to.be.visible); + await waitFor(() => { + expect(screen.queryByText('Cancel')).to.be.visible; + }); }); context('when the connection attempt is cancelled', function () { beforeEach(async function () { - const cancelButton = screen.getByText('Cancel'); - fireEvent.click(cancelButton); + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + userEvent.click(cancelButton); // Wait for the connecting... modal to hide. - await waitFor( - () => expect(screen.queryByText('Cancel')).to.not.exist - ); + await waitFor(() => { + expect(screen.queryByText('Cancel')).to.not.exist; + }); }); it('should enable the connect button', function () { @@ -405,12 +302,10 @@ describe('Connections Component', function () { it('should call the connect function with the connection options to connect', function () { expect(connectSpyFn.callCount).to.equal(1); - expect( - connectSpyFn.firstCall.args[0].connectionOptions - ).to.deep.equal({ - connectionString: - 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000', - }); + expect(connectSpyFn.firstCall.args[0]).to.have.property( + 'connectionString', + 'mongodb://localhost:27099/?connectTimeoutMS=5000&serverSelectionTimeoutMS=5000&appName=TEST' + ); }); context( @@ -429,12 +324,10 @@ describe('Connections Component', function () { it('should call the connect function with the connection options to connect', function () { expect(connectSpyFn.callCount).to.equal(2); - expect( - connectSpyFn.secondCall.args[0].connectionOptions - ).to.deep.equal({ - connectionString: - 'mongodb://localhost:27018/?readPreference=primary&ssl=false', - }); + expect(connectSpyFn.secondCall.args[0]).to.have.property( + 'connectionString', + 'mongodb://localhost:27018/?readPreference=primary&ssl=false&appName=TEST' + ); }); } ); diff --git a/packages/compass-connections/src/components/legacy-connections.tsx b/packages/compass-connections/src/components/legacy-connections.tsx index 5538a043c52..b47138d32bb 100644 --- a/packages/compass-connections/src/components/legacy-connections.tsx +++ b/packages/compass-connections/src/components/legacy-connections.tsx @@ -11,30 +11,21 @@ import { import { useLogger } from '@mongodb-js/compass-logging/provider'; import ConnectionForm from '@mongodb-js/connection-form'; import type AppRegistry from 'hadron-app-registry'; -import type { connect } from 'mongodb-data-service'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { usePreference } from 'compass-preferences-model/provider'; import type { ConnectionInfo } from '../provider'; -import { - ConnectionStatus, - useConnectionRepository, - useConnections, -} from '../provider'; +import { useConnectionRepository } from '../hooks/use-connection-repository'; import Connecting from './connecting/connecting'; import ConnectionList from './connection-list/connection-list'; import FormHelp from './form-help/form-help'; import { useConnectionInfoStatus } from '../hooks/use-connections-with-status'; -import { createNewConnectionInfo } from '../stores/connections-store'; +import { useConnections } from '../stores/connections-store'; import { getConnectingStatusText, getConnectionErrorMessage, } from './connection-status-notifications'; import { useConnectionFormPreferences } from '../hooks/use-connection-form-preferences'; -type ConnectFn = typeof connect; - -export type { ConnectFn }; - const connectStyles = css({ position: 'absolute', left: 0, @@ -86,23 +77,6 @@ const formCardLightThemeStyles = css({ background: palette.white, }); -// Single connection form is a bit of a special case where form is always on -// screen so even when user is not explicitly editing any connections, we need a -// connection info object to be passed around. For that purposes this hook will -// either return an editing connection info or will create a new one as a -// fallback if nothing is actively being edited -function useActiveConnectionInfo( - editingConnectionInfo?: ConnectionInfo | null -) { - const [connectionInfo, setConnectionInfo] = useState(() => { - return editingConnectionInfo ?? createNewConnectionInfo(); - }); - useEffect(() => { - setConnectionInfo(editingConnectionInfo ?? createNewConnectionInfo()); - }, [editingConnectionInfo]); - return connectionInfo; -} - function Connections({ appRegistry, openConnectionImportExportModal, @@ -115,7 +89,11 @@ function Connections({ const { log, mongoLogId } = useLogger('COMPASS-CONNECTIONS'); const { - state: { editingConnectionInfo, connectionErrors, oidcDeviceAuthState }, + state: { + editingConnectionInfo: activeConnectionInfo, + connectionErrors, + oidcDeviceAuthState, + }, connect, disconnect, createNewConnection, @@ -126,31 +104,17 @@ function Connections({ saveEditedConnection, } = useConnections(); + const activeConnectionStatus = useConnectionInfoStatus( + activeConnectionInfo.id + ); + const { favoriteConnections, nonFavoriteConnections: recentConnections } = useConnectionRepository(); const darkMode = useDarkMode(); const connectionFormPreferences = useConnectionFormPreferences(); const isMultiConnectionEnabled = usePreference( - 'enableNewMultipleConnectionSystem' - ); - - const activeConnectionInfo = useActiveConnectionInfo( - // TODO(COMPASS-7397): Even though connection form interface expects - // connection info to only be "initial", some parts of the form UI actually - // read the values from the info as if they should be updated (favorite edit - // form), for that purpose instead of using state store directly, we will - // first try to find the connection in the list of connections that track - // the connection info updates instead of passing the store state directly. - // This should go away when we are normalizing this state and making sure - // that favorite form is correctly reading the state from a single store - [...favoriteConnections, ...recentConnections].find((info) => { - // Might be missing in case of "New connection" when it's not saved yet - return info.id === editingConnectionInfo?.id; - }) ?? editingConnectionInfo - ); - const activeConnectionStatus = useConnectionInfoStatus( - activeConnectionInfo.id + 'enableMultipleConnectionSystem' ); const onConnectClick = (connectionInfo: ConnectionInfo) => { @@ -169,6 +133,11 @@ function Connections({ const activeConnectionOidcAuthState = oidcDeviceAuthState[activeConnectionInfo.id]; + const openSettingsModal = useCallback( + (tab?: string) => appRegistry.emit('open-compass-settings', tab), + [appRegistry] + ); + return (
@@ -223,6 +192,7 @@ function Connections({ initialConnectionInfo={activeConnectionInfo} connectionErrorMessage={connectionErrorMessage} preferences={connectionFormPreferences} + openSettingsModal={openSettingsModal} />
@@ -230,7 +200,7 @@ function Connections({
- {activeConnectionStatus === ConnectionStatus.Connecting && ( + {activeConnectionStatus === 'connecting' && ( (null); + +const ConnectionIdContext = createContext(null); + +/** + * @deprecated define connection for your test separately + */ export const TEST_CONNECTION_INFO: ConnectionInfo = { id: 'TEST', connectionOptions: { connectionString: 'mongodb://localhost:27020', }, }; + export function useConnectionInfo() { const connectionInfo = useContext(ConnectionInfoContext); if (!connectionInfo) { @@ -35,8 +43,9 @@ export function useConnectionInfo() { } return connectionInfo; } + export const ConnectionInfoProvider: React.FC<{ - connectionInfoId?: string; + connectionInfoId: string; children?: | ((connectionInfo: ConnectionInfo) => React.ReactNode) | React.ReactNode; @@ -44,27 +53,51 @@ export const ConnectionInfoProvider: React.FC<{ connectionInfoId, children, }) { - const { getConnectionInfoById } = useConnectionRepository(); - const connectionInfo = connectionInfoId - ? getConnectionInfoById(connectionInfoId) - : undefined; - const connectionsManager = connectionsManagerLocator(); - const isConnected = - connectionInfo && - connectionsManager.statusOf(connectionInfo.id) === - ConnectionStatus.Connected; - return isConnected && connectionInfo ? ( - - {typeof children === 'function' ? children(connectionInfo) : children} - + const connection = useConnectionForId(connectionInfoId); + const isConnected = connection?.status === 'connected'; + return isConnected ? ( + + + {typeof children === 'function' ? children(connection.info) : children} + + ) : null; }); + +/** + * @deprecated use `useConnectionInfoRef` instead + */ export const useConnectionInfoAccess = (): ConnectionInfoAccess => { - const connectionInfo = useConnectionInfo(); - const connectionInfoRef = useRef(connectionInfo); - connectionInfoRef.current = connectionInfo; + let connectionId = useContext(ConnectionIdContext); + if (!connectionId) { + if (process.env.NODE_ENV !== 'test') { + throw new Error( + 'Could not find the current ConnectionInfo. Did you forget to setup the ConnectionInfoContext?' + ); + } + connectionId = TEST_CONNECTION_INFO.id; + } + // TODO: remove when all tests are using new testing helpers + if (!useContext(ConnectionsStoreContext) && process.env.NODE_ENV === 'test') { + return { + getCurrentConnectionInfo() { + return TEST_CONNECTION_INFO; + }, + }; + } + // This is stable in all environments + // eslint-disable-next-line react-hooks/rules-of-hooks + const connectionInfoRef = useConnectionInfoRefForId(connectionId); return { getCurrentConnectionInfo() { + if (!connectionInfoRef.current) { + if (process.env.NODE_ENV !== 'test') { + throw new Error( + 'Could not find the current ConnectionInfo. Did you forget to setup the ConnectionInfoContext?' + ); + } + return TEST_CONNECTION_INFO; + } return connectionInfoRef.current; }, }; @@ -80,6 +113,10 @@ type FirstArgument = F extends (...args: [infer A, ...any]) => any ? A : never; +/** + * @deprecated instead of using HOC, refactor class component to functional + * component + */ function withConnectionInfoAccess< T extends ((...args: any[]) => any) | { new (...args: any[]): any } >( diff --git a/packages/compass-connections/src/connections-manager.spec.ts b/packages/compass-connections/src/connections-manager.spec.ts deleted file mode 100644 index ff030e57027..00000000000 --- a/packages/compass-connections/src/connections-manager.spec.ts +++ /dev/null @@ -1,712 +0,0 @@ -import sinon from 'sinon'; -import { expect } from 'chai'; -import type { DataService, connect } from 'mongodb-data-service'; -import EventEmitter from 'events'; - -import { - ConnectionStatus, - ConnectionsManager, - ConnectionsManagerEvents, -} from './connections-manager'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; - -function getConnectionsManager(mockTestConnectFn?: typeof connect) { - const { log } = createNoopLogger(); - return new ConnectionsManager({ - logger: log.unbound, - __TEST_CONNECT_FN: mockTestConnectFn, - }); -} - -function canceledPromiseSetup( - connectionsToBeConnected: ConnectionInfo[], - mockTestConnectFn: typeof connect -) { - let resolveCanceledPromise; - const canceledPromise = new Promise((resolve) => { - resolveCanceledPromise = resolve; - }); - - const connectionsManager = getConnectionsManager(mockTestConnectFn); - - const originalConnectFn: typeof connectionsManager.connect = - connectionsManager.connect.bind(connectionsManager); - let failures = 0; - sinon.stub(connectionsManager, 'connect').callsFake((info, options) => { - return originalConnectFn(info, options).finally(() => { - if (++failures === connectionsToBeConnected.length) { - resolveCanceledPromise(); - } - }); - }); - - return { - canceledPromise, - connectionsManager, - }; -} - -describe('ConnectionsManager', function () { - const mockDataService = (id: number) => { - const eventEmitter = new EventEmitter(); - return { - id, - disconnect() {}, - addReauthenticationHandler() {}, - getUpdatedSecrets() { - return Promise.resolve(id); - }, - emit: eventEmitter.emit.bind(eventEmitter), - on: eventEmitter.on.bind(eventEmitter), - } as unknown as DataService; - }; - - const connectedDataService1 = mockDataService(1); - const connectedConnectionInfo1 = { - id: '1', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - }; - - const connectedDataService2 = mockDataService(2); - const connectedConnectionInfo2 = { - id: '2', - connectionOptions: { - connectionString: 'mongodb://localhost:27018', - }, - }; - let connectionsManager: ConnectionsManager; - let mockConnectFn: typeof connect; - - const forceConnectionOptions = []; - const browserCommandForOIDCAuth = undefined; - let onDatabaseSecretsChangeSpy = sinon.spy(); - - function getConnectionConfigurationOptions({ - onNotifyOIDCDeviceFlow, - onDatabaseSecretsChange, - }: { - onNotifyOIDCDeviceFlow?: ReturnType; - onDatabaseSecretsChange?: ReturnType; - } = {}) { - return { - forceConnectionOptions, - browserCommandForOIDCAuth, - onNotifyOIDCDeviceFlow, - onDatabaseSecretsChange, - }; - } - - beforeEach(function () { - onDatabaseSecretsChangeSpy = sinon.spy(); - - mockConnectFn = sinon.stub().callsFake(({ connectionOptions }) => { - if ( - connectionOptions.connectionString.startsWith( - connectedConnectionInfo1.connectionOptions.connectionString - ) - ) { - return Promise.resolve(connectedDataService1); - } else { - return Promise.resolve(connectedDataService2); - } - }); - connectionsManager = getConnectionsManager(mockConnectFn); - }); - - context( - 'when connecting to multiple connections simultaneously', - function () { - beforeEach(function () { - const originalMockFn = mockConnectFn; - mockConnectFn = async (connectionInfo) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - return await originalMockFn(connectionInfo); - }; - connectionsManager = getConnectionsManager(mockConnectFn); - }); - - it('should emit connection-attempt-started event', async function () { - const onConnectionStarted = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptStarted, - onConnectionStarted - ); - - await Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - - expect(onConnectionStarted).to.be.calledTwice; - expect(onConnectionStarted.getCall(0).args).to.deep.equal([ - connectedConnectionInfo1.id, - ]); - expect(onConnectionStarted.getCall(1).args).to.deep.equal([ - connectedConnectionInfo2.id, - ]); - }); - - it('#statusOf should return connecting', function () { - void Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - expect( - connectionsManager.statusOf(connectedConnectionInfo1.id) - ).to.equal(ConnectionStatus.Connecting); - expect( - connectionsManager.statusOf(connectedConnectionInfo2.id) - ).to.equal(ConnectionStatus.Connecting); - }); - - it('#getDataServiceForConnection should not return anything for connecting connection', function () { - void Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - expect(() => - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo1.id - ) - ).to.throw; - expect(() => - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo2.id - ) - ).to.throw; - }); - - context('when all connection attempts are cancelled', function () { - let canceledPromise; - beforeEach(function () { - const connectionsToBeConnected = [ - connectedConnectionInfo1, - connectedConnectionInfo2, - ]; - const { - canceledPromise: setupCanceledPromise, - connectionsManager: setupConnectionsManager, - } = canceledPromiseSetup(connectionsToBeConnected, mockConnectFn); - canceledPromise = setupCanceledPromise; - connectionsManager = setupConnectionsManager; - }); - - it('should emit connection-attempt-cancelled for all attempted connections', async function () { - const onConnectionAttemptCancelled = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptCancelled, - onConnectionAttemptCancelled - ); - void Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]).catch(() => { - // noop - }); - connectionsManager.cancelAllConnectionAttempts(); - await canceledPromise; - expect(onConnectionAttemptCancelled).to.be.calledTwice; - expect(onConnectionAttemptCancelled.getCall(0).args).to.deep.equal([ - connectedConnectionInfo1.id, - ]); - expect(onConnectionAttemptCancelled.getCall(1).args).to.deep.equal([ - connectedConnectionInfo2.id, - ]); - }); - - it('#statusOf should return disconnected', async function () { - void Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]).catch(() => { - // noop - }); - connectionsManager.cancelAllConnectionAttempts(); - await canceledPromise; - expect( - connectionsManager.statusOf(connectedConnectionInfo1.id) - ).to.equal(ConnectionStatus.Disconnected); - expect( - connectionsManager.statusOf(connectedConnectionInfo2.id) - ).to.equal(ConnectionStatus.Disconnected); - }); - - it('#getDataServiceForConnection should not return anything for canceled connections', async function () { - void Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]).catch(() => { - // noop - }); - connectionsManager.cancelAllConnectionAttempts(); - await canceledPromise; - expect(() => - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo1.id - ) - ).to.throw; - expect(() => - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo2.id - ) - ).to.throw; - }); - }); - } - ); - - context('when connecting to Atlas Streams', function () { - beforeEach(function () { - connectionsManager = getConnectionsManager(mockConnectFn); - }); - - it('should throw an error', async function () { - const maybeError = await connectionsManager - .connect( - { - id: '1', - connectionOptions: { - connectionString: - 'mongodb://atlas-stream-example.mongodb.net/?tls=true', - }, - }, - getConnectionConfigurationOptions() - ) - .catch((error) => error); - - expect(maybeError.message).to.equal( - 'Atlas Stream Processing is not yet supported on MongoDB Compass. To work with your Stream Processing Instance, connect with mongosh or MongoDB for VS Code.' - ); - }); - }); - - context('when a connection attempt is cancelled', function () { - let canceledPromise; - beforeEach(function () { - const connectionsToBeConnected = [connectedConnectionInfo1]; - const { - canceledPromise: setupCanceledPromise, - connectionsManager: setupConnectionsManager, - } = canceledPromiseSetup(connectionsToBeConnected, mockConnectFn); - canceledPromise = setupCanceledPromise; - connectionsManager = setupConnectionsManager; - }); - it('should emit connection attempt cancelled event', async function () { - const onConnectionCancelled = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptCancelled, - onConnectionCancelled - ); - - void connectionsManager - .connect(connectedConnectionInfo1, getConnectionConfigurationOptions()) - .catch(() => { - // - }); - void connectionsManager.closeConnection(connectedConnectionInfo1.id); - await canceledPromise; - expect(onConnectionCancelled).to.be.calledWithExactly( - connectedConnectionInfo1.id - ); - }); - - it('#statusOf should return ConnectionStatus.Disconnected', async function () { - void connectionsManager - .connect(connectedConnectionInfo1, getConnectionConfigurationOptions()) - .catch(() => { - // - }); - void connectionsManager.closeConnection(connectedConnectionInfo1.id); - await canceledPromise; - expect(connectionsManager.statusOf(connectedConnectionInfo1.id)).to.equal( - ConnectionStatus.Disconnected - ); - }); - - it('#getDataServiceForConnection should not return anything for cancelled connection attempt', async function () { - void connectionsManager - .connect(connectedConnectionInfo1, getConnectionConfigurationOptions()) - .catch(() => { - // - }); - void connectionsManager.closeConnection(connectedConnectionInfo1.id); - await canceledPromise; - expect(() => - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo1.id - ) - ).to.throw; - }); - - context( - 'when attempting to connect to a cancelled connection', - function () { - let canceledPromise; - beforeEach(function () { - const connectionsToBeConnected = [connectedConnectionInfo1]; - const { - canceledPromise: setupCanceledPromise, - connectionsManager: setupConnectionsManager, - } = canceledPromiseSetup(connectionsToBeConnected, () => - Promise.resolve(connectedDataService1) - ); - canceledPromise = setupCanceledPromise; - connectionsManager = setupConnectionsManager; - }); - - it('should be able to connect', async function () { - const onConnectionCancelled = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptCancelled, - onConnectionCancelled - ); - - void connectionsManager - .connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ) - .catch(() => { - // - }); - await connectionsManager.closeConnection(connectedConnectionInfo1.id); - await canceledPromise; - expect(onConnectionCancelled).to.be.calledWithExactly( - connectedConnectionInfo1.id - ); - expect( - connectionsManager.statusOf(connectedConnectionInfo1.id) - ).to.equal(ConnectionStatus.Disconnected); - - await connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ); - expect( - connectionsManager.statusOf(connectedConnectionInfo1.id) - ).to.equal(ConnectionStatus.Connected); - }); - } - ); - }); - - context( - 'when connected successfully to multiple connections simultaneously', - function () { - it('should emit connection successful event for each connected connection', async function () { - const onSuccessfulConnections = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - onSuccessfulConnections - ); - - await Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - expect(onSuccessfulConnections).to.be.calledTwice; - expect(onSuccessfulConnections.getCall(0).args).to.deep.equal([ - connectedConnectionInfo1.id, - connectedDataService1, - ]); - expect(onSuccessfulConnections.getCall(1).args).to.deep.equal([ - connectedConnectionInfo2.id, - connectedDataService2, - ]); - }); - - it('#statusOf should return ConnectionStatus.Connected', async function () { - await Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - expect( - connectionsManager.statusOf(connectedConnectionInfo1.id) - ).to.equal(ConnectionStatus.Connected); - expect( - connectionsManager.statusOf(connectedConnectionInfo2.id) - ).to.equal(ConnectionStatus.Connected); - }); - - it('#getDataServiceForConnection should be able to return connected dataService', async function () { - await Promise.all([ - connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ), - connectionsManager.connect( - connectedConnectionInfo2, - getConnectionConfigurationOptions() - ), - ]); - expect( - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo1.id - ) - ).to.deep.equal(connectedDataService1); - expect( - connectionsManager.getDataServiceForConnection( - connectedConnectionInfo2.id - ) - ).to.deep.equal(connectedDataService2); - }); - - it('should return the connected DataService for an already connected connection', async function () { - await connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ); - await connectionsManager.connect( - connectedConnectionInfo1, - getConnectionConfigurationOptions() - ); - expect(mockConnectFn).to.be.calledOnce; - }); - } - ); - - context('when a connection fails to connect', function () { - const failedConnectionInfo = { - id: '3', - connectionOptions: { - connectionString: 'mongodb://localhost:2', - }, - }; - const error = new Error('Connection rejected'); - beforeEach(function () { - mockConnectFn = () => Promise.reject(error); - connectionsManager = getConnectionsManager(mockConnectFn); - }); - - it('should emit connection failed event for the failed connection', async function () { - const onConnectionFailed = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionAttemptFailed, - onConnectionFailed - ); - - try { - await connectionsManager.connect( - failedConnectionInfo, - getConnectionConfigurationOptions() - ); - } catch (error) { - expect(error.message).to.equal('Connection rejected'); - expect(onConnectionFailed).to.be.calledWithExactly( - failedConnectionInfo.id, - error - ); - } - }); - - it('#statusOf should return ConnectionStatus.Failed', async function () { - try { - await connectionsManager.connect( - failedConnectionInfo, - getConnectionConfigurationOptions() - ); - } catch (error) { - // nothing - } - expect(connectionsManager.statusOf(failedConnectionInfo.id)).to.equal( - ConnectionStatus.Failed - ); - }); - - it('#getDataServiceForConnection should not return anything for failed connection', async function () { - try { - await connectionsManager.connect( - failedConnectionInfo, - getConnectionConfigurationOptions() - ); - } catch (error) { - // nothing - } - expect(() => - connectionsManager.getDataServiceForConnection(failedConnectionInfo.id) - ).to.throw; - }); - }); - - context('when an active connection is disconnected', function () { - const activeConnectionInfo = { - id: '4', - connectionOptions: { - connectionString: 'mongodb://localhost:27019', - }, - }; - const activeDataService = { - id: '4', - disconnect() {}, - addReauthenticationHandler() {}, - } as unknown as DataService; - beforeEach(function () { - mockConnectFn = () => Promise.resolve(activeDataService); - connectionsManager = getConnectionsManager(mockConnectFn); - }); - - it('should emit connection disconnected event', async function () { - const onConnectionDisconnected = sinon.stub(); - connectionsManager.on( - ConnectionsManagerEvents.ConnectionDisconnected, - onConnectionDisconnected - ); - - await connectionsManager.connect( - activeConnectionInfo, - getConnectionConfigurationOptions() - ); - expect(connectionsManager.statusOf(activeConnectionInfo.id)).to.equal( - ConnectionStatus.Connected - ); - - await connectionsManager.closeConnection(activeConnectionInfo.id); - expect(onConnectionDisconnected).to.be.calledWithExactly( - activeConnectionInfo.id - ); - }); - - it('#statusOf should return ConnectionStatus.Disconnected', async function () { - await connectionsManager.connect( - activeConnectionInfo, - getConnectionConfigurationOptions() - ); - expect(connectionsManager.statusOf(activeConnectionInfo.id)).to.equal( - ConnectionStatus.Connected - ); - - await connectionsManager.closeConnection(activeConnectionInfo.id); - expect(connectionsManager.statusOf(activeConnectionInfo.id)).to.equal( - ConnectionStatus.Disconnected - ); - }); - - it('#getDataServiceForConnection should not return anything for disconnected connection', async function () { - await connectionsManager.connect( - activeConnectionInfo, - getConnectionConfigurationOptions() - ); - await connectionsManager.closeConnection(activeConnectionInfo.id); - expect(() => - connectionsManager.getDataServiceForConnection(activeConnectionInfo.id) - ).to.throw; - }); - }); - - context('oidc connection', function () { - it('should notify the onDatabaseSecretsChange when dataservice secrets change', async function () { - const oidcConnectionInfo = { - connectionOptions: { - connectionString: `${connectedConnectionInfo1.connectionOptions.connectionString}&authMechanism=MONGODB-OIDC`, - oidc: {}, - }, - ...connectedConnectionInfo1, - }; - - await connectionsManager.connect( - oidcConnectionInfo, - getConnectionConfigurationOptions({ - onDatabaseSecretsChange: onDatabaseSecretsChangeSpy, - }) - ); - - connectedDataService1.emit('connectionInfoSecretsChanged'); - - expect(onDatabaseSecretsChangeSpy).to.have.been.calledWith( - { - connectionOptions: { - connectionString: 'mongodb://localhost:27017/', - oidc: {}, - }, - id: '1', - }, - connectedDataService1 - ); - }); - - it('should merge any oidc state from previous connections', async function () { - const previousOidcState = { - oidc: { - mockState: true, - }, - }; - - const inspectableConnectionsManager = connectionsManager as any; - inspectableConnectionsManager.oidcState.set( - connectedConnectionInfo1.id, - previousOidcState - ); - - const oidcConnectionInfo = { - connectionOptions: { - connectionString: `${connectedConnectionInfo1.connectionOptions.connectionString}&authMechanism=MONGODB-OIDC`, - }, - ...connectedConnectionInfo1, - }; - - await connectionsManager.connect( - oidcConnectionInfo, - getConnectionConfigurationOptions() - ); - - const connectionInfo = mockConnectFn.getCall(0).args[0]; - expect(connectionInfo.connectionOptions).to.deep.equal({ - connectionString: 'mongodb://localhost:27017/', - oidc: { - mockState: true, - }, - }); - }); - }); -}); diff --git a/packages/compass-connections/src/connections-manager.ts b/packages/compass-connections/src/connections-manager.ts deleted file mode 100644 index 429be0702ca..00000000000 --- a/packages/compass-connections/src/connections-manager.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { EventEmitter } from 'events'; -import { createConnectionAttempt } from 'mongodb-data-service'; -import type { Logger } from '@mongodb-js/compass-logging'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; -import type { ConnectionOptions } from 'mongodb-data-service'; -import type { - ConnectionAttempt, - DataService, - ReauthenticationHandler, - connect, -} from 'mongodb-data-service'; -import { mongoLogId } from '@mongodb-js/compass-logging/provider'; -import { cloneDeep, merge } from 'lodash'; -import { adjustConnectionOptionsBeforeConnect } from '@mongodb-js/connection-form'; -import mongodbBuildInfo from 'mongodb-build-info'; -import { openToast } from '@mongodb-js/compass-components'; -import { createCancelError, isCancelError } from '@mongodb-js/compass-utils'; - -type ConnectFn = typeof connect; -type ConnectionInfoId = ConnectionInfo['id']; - -export enum ConnectionsManagerEvents { - ConnectionAttemptStarted = 'connection-attempt-started', - ConnectionAttemptCancelled = 'connection-attempt-cancelled', - ConnectionAttemptSuccessful = 'connection-attempt-successful', - ConnectionAttemptFailed = 'connection-attempt-failed', - ConnectionDisconnected = 'connection-disconnected', -} - -export type ConnectionManagerEventListeners = { - [ConnectionsManagerEvents.ConnectionAttemptStarted]: ( - connectionInfoId: ConnectionInfoId - ) => void; - [ConnectionsManagerEvents.ConnectionAttemptCancelled]: ( - connectionInfoId: ConnectionInfoId - ) => void; - [ConnectionsManagerEvents.ConnectionAttemptSuccessful]: ( - connectionInfoId: ConnectionInfoId, - dataService: DataService - ) => void; - [ConnectionsManagerEvents.ConnectionAttemptFailed]: ( - connectionInfoId: ConnectionInfoId, - error: any - ) => void; - [ConnectionsManagerEvents.ConnectionDisconnected]: ( - connectionInfoId: ConnectionInfoId - ) => void; -}; - -export enum ConnectionStatus { - Disconnected = 'disconnected', - Connecting = 'connecting', - Connected = 'connected', - Failed = 'failed', -} - -type ConnectionStatusTransitions = { - [event in ConnectionsManagerEvents]?: { - [status in ConnectionStatus]?: ConnectionStatus; - }; -}; - -type ConnectionConfiguration = { - /** - * Overwrites user-provided connection options with the ones provided here. - */ - forceConnectionOptions?: [key: string, value: string][]; - /** - * Command to open the browser for OIDC Auth. - */ - browserCommandForOIDCAuth?: string; - /** - * Function to be called to notify the user that it should check the OIDC device for confirmation. - * Tipically, the browser. - */ - onNotifyOIDCDeviceFlow?: OnNotifyOIDCDeviceFlow; - /** - * Function to be called every time the secrets change on a connection dataservice. It's only - * used for OIDC now (like a refresh token) but can be used for any type of authentication that - * is temporary, like AWS IAM. - */ - onDatabaseSecretsChange?: OnDatabaseSecretsChangedCallback; -}; - -const connectionStatusTransitions: ConnectionStatusTransitions = { - [ConnectionsManagerEvents.ConnectionAttemptStarted]: { - [ConnectionStatus.Disconnected]: ConnectionStatus.Connecting, - [ConnectionStatus.Failed]: ConnectionStatus.Connecting, - }, - [ConnectionsManagerEvents.ConnectionAttemptCancelled]: { - [ConnectionStatus.Connecting]: ConnectionStatus.Disconnected, - }, - [ConnectionsManagerEvents.ConnectionAttemptFailed]: { - [ConnectionStatus.Connecting]: ConnectionStatus.Failed, - }, - [ConnectionsManagerEvents.ConnectionAttemptSuccessful]: { - [ConnectionStatus.Connecting]: ConnectionStatus.Connected, - }, - [ConnectionsManagerEvents.ConnectionDisconnected]: { - [ConnectionStatus.Connected]: ConnectionStatus.Disconnected, - }, -}; - -type OnNotifyOIDCDeviceFlow = (deviceFlowInformation: { - verificationUrl: string; - userCode: string; -}) => void; - -type OnDatabaseSecretsChangedCallback = ( - connectionInfo: ConnectionInfo, - dataService: DataService -) => void; - -export const CONNECTION_CANCELED_ERR = 'Connection attempt was canceled'; - -export class ConnectionsManager extends EventEmitter { - private readonly logger: Logger['log']['unbound']; - private readonly reAuthenticationHandler?: ReauthenticationHandler; - private readonly __TEST_CONNECT_FN?: ConnectFn; - private appName: string | undefined; - private connectionAttempts = new Map(); - private connectionStatuses = new Map(); - private dataServices = new Map(); - private oidcState = new Map>(); - - constructor({ - appName, - logger, - reAuthenticationHandler, - __TEST_CONNECT_FN, - }: { - appName?: string; - logger: Logger['log']['unbound']; - reAuthenticationHandler?: ReauthenticationHandler; - __TEST_CONNECT_FN?: ConnectFn; - }) { - super(); - this.appName = appName; - this.logger = logger; - this.reAuthenticationHandler = reAuthenticationHandler; - this.__TEST_CONNECT_FN = __TEST_CONNECT_FN; - } - - getDataServiceForConnection(connectionInfoId: ConnectionInfoId): DataService { - const dataService = this.dataServices.get(connectionInfoId); - if (!dataService) { - throw new Error( - `DataService for connectionId - ${connectionInfoId} not present in ConnectionsManager.` - ); - } - return dataService; - } - - statusOf(connectionInfoId: ConnectionInfoId): ConnectionStatus { - return ( - this.connectionStatuses.get(connectionInfoId) ?? - ConnectionStatus.Disconnected - ); - } - - cancelAllConnectionAttempts(): void { - for (const connectionInfoId of this.connectionAttempts.keys()) { - this.cancelConnectionAttempt(connectionInfoId); - } - } - - /** - * @param connectionInfo The adjusted ConnectionInfo object that already has - * parameters such as appName. - */ - async connect( - { id: connectionId, ...originalConnectionInfo }: ConnectionInfo, - { - forceConnectionOptions, - browserCommandForOIDCAuth, - onNotifyOIDCDeviceFlow, - onDatabaseSecretsChange, - }: ConnectionConfiguration = {} - ): Promise { - try { - const existingDataService = this.dataServices.get(connectionId); - - if ( - existingDataService && - this.statusOf(connectionId) === ConnectionStatus.Connected - ) { - return existingDataService; - } - - this.updateAndNotifyConnectionStatus( - connectionId, - ConnectionsManagerEvents.ConnectionAttemptStarted, - [connectionId] - ); - - const adjustedConnectionInfoForConnection: ConnectionInfo = merge( - cloneDeep({ id: connectionId, ...originalConnectionInfo }), - { - connectionOptions: adjustConnectionOptionsBeforeConnect({ - connectionOptions: merge( - cloneDeep(originalConnectionInfo.connectionOptions), - this.oidcState.get(connectionId) ?? {} - ), - defaultAppName: this.appName, - preferences: { - forceConnectionOptions: forceConnectionOptions ?? [], - browserCommandForOIDCAuth, - }, - notifyDeviceFlow: onNotifyOIDCDeviceFlow, - }), - } - ); - - const connectionAttempt = createConnectionAttempt({ - logger: this.logger, - connectFn: this.__TEST_CONNECT_FN, - }); - - this.connectionAttempts.set(connectionId, connectionAttempt); - - // Temporarily disable Atlas Streams connections until https://jira.mongodb.org/browse/STREAMS-862 - // is done. - if (isAtlasStreamsInstance(adjustedConnectionInfoForConnection)) { - throw new Error( - 'Atlas Stream Processing is not yet supported on MongoDB Compass. To work with your Stream Processing Instance, connect with mongosh or MongoDB for VS Code.' - ); - } - - const dataService = await connectionAttempt.connect( - adjustedConnectionInfoForConnection.connectionOptions - ); - - if (!dataService || connectionAttempt.isClosed()) { - throw createCancelError(CONNECTION_CANCELED_ERR); - } - - dataService.on?.('connectionInfoSecretsChanged', () => { - void dataService.getUpdatedSecrets().then((secrets) => { - this.oidcState.set(connectionId, secrets); - }); - - onDatabaseSecretsChange?.( - adjustedConnectionInfoForConnection, - dataService - ); - }); - - dataService.on?.('oidcAuthFailed', (error) => { - openToast('oidc-auth-failed', { - title: 'Failed to authenticate', - description: error, - variant: 'important', - }); - }); - - if (this.reAuthenticationHandler) { - dataService.addReauthenticationHandler(this.reAuthenticationHandler); - } - - this.dataServices.set(connectionId, dataService); - - this.updateAndNotifyConnectionStatus( - connectionId, - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - [connectionId, dataService] - ); - - return dataService; - } catch (error) { - if (isCancelError(error)) { - this.updateAndNotifyConnectionStatus( - connectionId, - ConnectionsManagerEvents.ConnectionAttemptCancelled, - [connectionId] - ); - } else { - this.updateAndNotifyConnectionStatus( - connectionId, - ConnectionsManagerEvents.ConnectionAttemptFailed, - [connectionId, error] - ); - } - throw error; - } finally { - this.connectionAttempts.delete(connectionId); - } - } - - async closeConnection(connectionInfoId: ConnectionInfoId): Promise { - const currentStatus = this.statusOf(connectionInfoId); - if (currentStatus === ConnectionStatus.Connecting) { - this.cancelConnectionAttempt(connectionInfoId); - } else if (currentStatus === ConnectionStatus.Connected) { - const dataService = this.dataServices.get(connectionInfoId); - if (dataService) { - await dataService.disconnect(); - this.dataServices.delete(connectionInfoId); - } else { - this.logger.warn( - 'ConnectionsManager', - mongoLogId(1_001_000_305), - 'closeConnection', - `Started closing connection but found no DataService to disconnect` - ); - } - this.updateAndNotifyConnectionStatus( - connectionInfoId, - ConnectionsManagerEvents.ConnectionDisconnected, - [connectionInfoId] - ); - this.oidcState.delete(connectionInfoId); - this.connectionAttempts.delete(connectionInfoId); - } else { - this.logger.warn( - 'ConnectionsManager', - mongoLogId(1_001_000_304), - 'closeConnection', - `Attempting to close a connection that is neither being connected to, nor connected but the status is ${currentStatus}` - ); - } - } - - /** - * Try to close all currently existing connections and ignore all errors if - * they happen during that process. This is a clean-up method that is supposed - * to be used in cases where there is probably no way for us to react to those - * errors anyway and all the errors will be already logged elsewhere - */ - async closeAllConnections(): Promise { - await Promise.allSettled( - Array.from(this.connectionStatuses.keys()).map((connectionId) => { - return this.closeConnection(connectionId); - }) - ); - } - - /** - * Returns the number of active connections. We count in-progress connections - * as "active" to make sure that the maximum connection allowed check takes - * those into account and doesn't allow to open more connections than allowed - * by starting too many connections in parallel - */ - getActiveConnectionsCount(): number { - return Array.from(this.connectionStatuses.values()).filter((status) => { - return [ConnectionStatus.Connected, ConnectionStatus.Connecting].includes( - status - ); - }).length; - } - - on( - eventName: T, - listener: ConnectionManagerEventListeners[T] - ): this { - return super.on(eventName, listener); - } - - off( - eventName: T, - listener: ConnectionManagerEventListeners[T] - ): this { - return super.off(eventName, listener); - } - - once( - eventName: T, - listener: ConnectionManagerEventListeners[T] - ): this { - return super.once(eventName, listener); - } - - removeListener( - eventName: T, - listener: ConnectionManagerEventListeners[T] - ): this { - return super.removeListener(eventName, listener); - } - - emit( - eventName: T, - ...args: Parameters - ): boolean { - return super.emit(eventName, ...args); - } - - private cancelConnectionAttempt(connectionInfoId: ConnectionInfoId): void { - const connectionAttempt = this.connectionAttempts.get(connectionInfoId); - if (connectionAttempt) { - connectionAttempt.cancelConnectionAttempt(); - this.connectionAttempts.delete(connectionInfoId); - } - } - - private updateAndNotifyConnectionStatus( - connectionInfoId: ConnectionInfoId, - connectionEvent: T, - connectionEventParams: Parameters - ) { - const currentStatus = this.statusOf(connectionInfoId); - const nextStatus = - connectionStatusTransitions[connectionEvent]?.[currentStatus]; - if (nextStatus === undefined) { - throw new Error( - `Unexpected state for ConnectionInfoId ${connectionInfoId}. Encountered ${connectionEvent} with currentStatus=${currentStatus}` - ); - } - this.connectionStatuses.set(connectionInfoId, nextStatus); - this.emit(connectionEvent, ...connectionEventParams); - } -} - -function isAtlasStreamsInstance( - adjustedConnectionInfoForConnection: ConnectionInfo -) { - try { - return mongodbBuildInfo.isAtlasStream( - adjustedConnectionInfoForConnection.connectionOptions.connectionString - ); - } catch { - // This catch-all is not ideal, but it safe-guards regular connections - // instead of making assumptions on the fact that the implementation - // of `mongodbBuildInfo.isAtlasStream` would never throw. - return false; - } -} diff --git a/packages/compass-connections/src/hooks/use-active-connections.spec.ts b/packages/compass-connections/src/hooks/use-active-connections.spec.ts index 5df27ec8283..ecc1940717b 100644 --- a/packages/compass-connections/src/hooks/use-active-connections.spec.ts +++ b/packages/compass-connections/src/hooks/use-active-connections.spec.ts @@ -1,22 +1,7 @@ -import { - useActiveConnections, - ConnectionsManager, - ConnectionsManagerProvider, -} from '../provider'; -import type { EventEmitter } from 'events'; -import { renderHook } from '@testing-library/react-hooks'; -import { createElement } from 'react'; -import { - ConnectionStorageEvents, - ConnectionStorageProvider, - InMemoryConnectionStorage, - type ConnectionInfo, - type ConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; +import { useActiveConnections } from './use-active-connections'; +import { cleanup, renderHookWithConnections } from '../test'; import { expect } from 'chai'; -import Sinon from 'sinon'; -import { waitFor } from '@testing-library/dom'; -import { ConnectionsManagerEvents } from '../connections-manager'; +import type { ConnectionInfo } from '@mongodb-js/connection-storage/provider'; const mockConnections: ConnectionInfo[] = [ { @@ -42,87 +27,59 @@ const mockConnections: ConnectionInfo[] = [ ]; describe('useActiveConnections', function () { - let renderHookWithContext: typeof renderHook; - let connectionsManager: ConnectionsManager; - let mockConnectionStorage: ConnectionStorage; - - beforeEach(function () { - connectionsManager = new ConnectionsManager({} as any); - mockConnectionStorage = new InMemoryConnectionStorage(mockConnections); - - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(ConnectionStorageProvider, { - value: mockConnectionStorage, - children: [ - createElement(ConnectionsManagerProvider, { - value: connectionsManager, - children: children, - }), - ], - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); + afterEach(cleanup); it('should return empty list of connections', function () { - const { result } = renderHookWithContext(() => useActiveConnections()); + const { result } = renderHookWithConnections(useActiveConnections, { + connections: mockConnections, + }); expect(result.current).to.have.length(0); }); it('should return active connections', async function () { - (connectionsManager as any).connectionStatuses.set('turtle', 'connected'); - const { result } = renderHookWithContext(() => useActiveConnections()); + const { result, connectionsStore } = renderHookWithConnections( + useActiveConnections, + { connections: mockConnections } + ); - await waitFor(() => { - expect(result.current).to.have.length(1); - expect(result.current[0]).to.have.property('id', 'turtle'); - }); + await connectionsStore.actions.connect(mockConnections[0]); + + expect(result.current).to.have.length(1); + expect(result.current[0]).to.have.property('id', 'turtle'); }); - it('should listen to connections manager updates', async function () { - (connectionsManager as any).connectionStatuses.set('turtle', 'connected'); - const { result } = renderHookWithContext(() => useActiveConnections()); + it('should listen to connections status updates', async function () { + const { result, connectionsStore } = renderHookWithConnections( + useActiveConnections, + { connections: mockConnections } + ); - await waitFor(() => { - expect(result.current).to.have.length(1); - }); + await connectionsStore.actions.connect(mockConnections[0]); - (connectionsManager as any).connectionStatuses.set('oranges', 'connected'); - connectionsManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - 'orange', - {} as any - ); + expect(result.current).to.have.length(1); - await waitFor(() => { - expect(result.current).to.have.length(2); - }); + await connectionsStore.actions.connect(mockConnections[1]); + + expect(result.current).to.have.length(2); }); - it('should listen to connections storage updates', async function () { - const loadAllStub = (mockConnectionStorage.loadAll = - Sinon.stub().resolves(mockConnections)); - (connectionsManager as any).connectionStatuses.set('turtle', 'connected'); - const { result } = renderHookWithContext(() => useActiveConnections()); - - loadAllStub.resolves([ - { - ...mockConnections[0], - savedConnectionType: 'recent', - }, - mockConnections[1], - ]); - (mockConnectionStorage as unknown as EventEmitter).emit( - ConnectionStorageEvents.ConnectionsChanged + it('should listen to connections state updates', async function () { + const { result, connectionsStore } = renderHookWithConnections( + useActiveConnections, + { connections: mockConnections } ); - await waitFor(() => { - expect(result.current).to.have.length(1); - expect(result.current[0]).to.have.property( - 'savedConnectionType', - 'recent' - ); - }); + await connectionsStore.actions.connect(mockConnections[0]); + + expect(result.current[0]).to.have.property( + 'savedConnectionType', + 'favorite' + ); + + connectionsStore.actions.toggleFavoritedConnectionStatus( + mockConnections[0].id + ); + + expect(result.current[0]).to.have.property('savedConnectionType', 'recent'); }); }); diff --git a/packages/compass-connections/src/hooks/use-active-connections.ts b/packages/compass-connections/src/hooks/use-active-connections.ts index 34a85fa58ff..4b800534b3d 100644 --- a/packages/compass-connections/src/hooks/use-active-connections.ts +++ b/packages/compass-connections/src/hooks/use-active-connections.ts @@ -1,13 +1,16 @@ import type { ConnectionInfo } from '@mongodb-js/connection-info'; import { useMemo } from 'react'; -import { useConnectionsWithStatus, ConnectionStatus } from '../provider'; +import { useConnectionsWithStatus } from '../provider'; +/** + * @deprecated use connection-store hooks instead + */ export function useActiveConnections(): ConnectionInfo[] { const connectionsWithStatus = useConnectionsWithStatus(); const activeConnections = useMemo(() => { return connectionsWithStatus .filter(({ connectionStatus }) => { - return connectionStatus === ConnectionStatus.Connected; + return connectionStatus === 'connected'; }) .map(({ connectionInfo }) => connectionInfo); }, [connectionsWithStatus]); diff --git a/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts b/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts deleted file mode 100644 index e2d037f80a5..00000000000 --- a/packages/compass-connections/src/hooks/use-can-open-new-connections.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { expect } from 'chai'; -import { waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; -import { createElement } from 'react'; -import { - type ConnectionInfo, - type ConnectionStatus, -} from '@mongodb-js/connection-info'; -import { - type PreferencesAccess, - createSandboxFromDefaultPreferences, -} from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; -import { ConnectionsManager, ConnectionsManagerProvider } from '../provider'; -import { - type ConnectionStorage, - ConnectionStorageProvider, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { useCanOpenNewConnections } from './use-can-open-new-connections'; - -const FAVORITE_CONNECTION_INFO: ConnectionInfo = { - id: 'favorite', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - savedConnectionType: 'favorite', -}; - -const NONFAVORITE_CONNECTION_INFO: ConnectionInfo = { - id: 'nonfavorite', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - savedConnectionType: 'recent', -}; - -describe('useCanOpenNewConnections', function () { - let renderHookWithContext: typeof renderHook; - let connectionStorage: ConnectionStorage; - let connectionManager: ConnectionsManager; - let preferencesAccess: PreferencesAccess; - - function withConnectionWithStatus( - connectionId: ConnectionInfo['id'], - status: ConnectionStatus - ) { - const connectionManagerInspectable = connectionManager as any; - connectionManagerInspectable.connectionStatuses.set(connectionId, status); - } - - async function withConnectionLimit(limit: number) { - await preferencesAccess.savePreferences({ - maximumNumberOfActiveConnections: limit, - }); - } - beforeEach(async function () { - preferencesAccess = await createSandboxFromDefaultPreferences(); - connectionManager = new ConnectionsManager({} as any); - connectionStorage = new InMemoryConnectionStorage([ - FAVORITE_CONNECTION_INFO, - NONFAVORITE_CONNECTION_INFO, - ]); - - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(PreferencesProvider, { - value: preferencesAccess, - children: [ - createElement(ConnectionStorageProvider, { - value: connectionStorage, - children: [ - createElement(ConnectionsManagerProvider, { - value: connectionManager, - children, - }), - ], - }), - ], - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); - - describe('number of active connections', function () { - it('should return the count of active connections', async function () { - withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected'); - - const { result } = renderHookWithContext(() => - useCanOpenNewConnections() - ); - - await waitFor(() => { - const { numberOfConnectionsOpen } = result.current; - expect(numberOfConnectionsOpen).to.equal(1); - }); - }); - }); - - describe('connection limiting', function () { - it('should not limit when the maximum number of connections is not reached', async function () { - await withConnectionLimit(1); - - const { result } = renderHookWithContext(() => - useCanOpenNewConnections() - ); - - await waitFor(() => { - const { numberOfConnectionsOpen, canOpenNewConnection } = - result.current; - expect(numberOfConnectionsOpen).to.equal(0); - expect(canOpenNewConnection).to.equal(true); - }); - }); - - it('should limit when the maximum number of connections is reached', async function () { - withConnectionWithStatus(FAVORITE_CONNECTION_INFO.id, 'connected'); - await withConnectionLimit(1); - - const { result } = renderHookWithContext(() => - useCanOpenNewConnections() - ); - - await waitFor(() => { - const { - numberOfConnectionsOpen, - canOpenNewConnection, - canNotOpenReason, - } = result.current; - expect(numberOfConnectionsOpen).to.equal(1); - expect(canOpenNewConnection).to.equal(false); - expect(canNotOpenReason).to.equal('maximum-number-exceeded'); - }); - }); - }); -}); diff --git a/packages/compass-connections/src/hooks/use-can-open-new-connections.ts b/packages/compass-connections/src/hooks/use-can-open-new-connections.ts deleted file mode 100644 index 6a4aa3607be..00000000000 --- a/packages/compass-connections/src/hooks/use-can-open-new-connections.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useActiveConnections } from './use-active-connections'; -import { usePreference } from 'compass-preferences-model/provider'; - -export type CanNotOpenConnectionReason = 'maximum-number-exceeded'; - -export function useCanOpenNewConnections(): { - numberOfConnectionsOpen: number; - maximumNumberOfConnectionsOpen: number; - canOpenNewConnection: boolean; - canNotOpenReason?: CanNotOpenConnectionReason; -} { - const activeConnections = useActiveConnections(); - const maximumNumberOfConnectionsOpen = - usePreference('maximumNumberOfActiveConnections') ?? 1; - - const numberOfConnectionsOpen = activeConnections.length; - const canOpenNewConnection = - numberOfConnectionsOpen < maximumNumberOfConnectionsOpen; - const canNotOpenReason = !canOpenNewConnection - ? 'maximum-number-exceeded' - : undefined; - - return { - numberOfConnectionsOpen, - maximumNumberOfConnectionsOpen, - canOpenNewConnection, - canNotOpenReason, - }; -} diff --git a/packages/compass-connections/src/hooks/use-connection-repository.spec.ts b/packages/compass-connections/src/hooks/use-connection-repository.spec.ts index 07aba917551..b9f7fd47724 100644 --- a/packages/compass-connections/src/hooks/use-connection-repository.spec.ts +++ b/packages/compass-connections/src/hooks/use-connection-repository.spec.ts @@ -1,352 +1,198 @@ import { useConnectionRepository } from './use-connection-repository'; import { expect } from 'chai'; -import { spy, restore } from 'sinon'; +import Sinon from 'sinon'; +import { cleanup } from '@testing-library/react'; import { - type ConnectionStorage, - InMemoryConnectionStorage, - ConnectionStorageProvider, - ConnectionStorageEvents, -} from '@mongodb-js/connection-storage/provider'; -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; -import { createElement } from 'react'; + createDefaultConnectionInfo, + renderHookWithConnections, +} from '../test'; + +const favoriteMockConnections = [ + { + ...createDefaultConnectionInfo(), + id: '12', + savedConnectionType: 'favorite', + favorite: { name: 'Bb' }, + }, + { + ...createDefaultConnectionInfo(), + id: '11', + savedConnectionType: 'favorite', + favorite: { name: 'Aa' }, + }, +]; + +const nonFavoriteMockConnections = [ + { + ...createDefaultConnectionInfo(), + id: '23', + savedConnectionType: 'recent', + favorite: { name: 'Cc' }, + }, + { + ...createDefaultConnectionInfo(), + id: '22', + savedConnectionType: 'recent', + favorite: { name: 'Bb' }, + }, + { + ...createDefaultConnectionInfo(), + id: '21', + savedConnectionType: 'recent', + favorite: { name: 'Aa' }, + }, +]; describe('useConnectionRepository', function () { - let renderHookWithContext: typeof renderHook; - let mockStorage: ConnectionStorage; - - beforeEach(function () { - mockStorage = new InMemoryConnectionStorage([]); - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(ConnectionStorageProvider, { - value: mockStorage, - children, - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); - - afterEach(function () { - restore(); + afterEach(() => { + cleanup(); + Sinon.restore(); }); describe('favoriteConnections', function () { - it('should return favourite connections sorted by name alphabetically', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '2', - savedConnectionType: 'favorite', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'favorite', - favorite: { name: 'Aa' }, - }, - { - id: '3', - savedConnectionType: 'autoConnectInfo', - }, - { - id: '4', - }, - ]); - - const { result } = renderHookWithContext(() => useConnectionRepository()); - await waitFor(() => { - const connections = result.current.favoriteConnections; - expect(connections.length).to.equal(2); - expect(connections[0].id).to.equal('1'); - expect(connections[1].id).to.equal('2'); - }); - }); - - it('should not change if only non favourite connections change', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '2', - savedConnectionType: 'favorite', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'favorite', - favorite: { name: 'Aa' }, - }, - ]); - - const { result } = renderHookWithContext(() => useConnectionRepository()); - - const initialFavoriteConnections = await waitFor(() => { - const favoriteConnections = result.current.favoriteConnections; - expect(favoriteConnections.length).to.equal(2); - return favoriteConnections; + it('should return favourite connections sorted by name alphabetically', function () { + const { result } = renderHookWithConnections(useConnectionRepository, { + connections: favoriteMockConnections, }); - mockStorage.loadAll = function () { - return Promise.resolve([ - { - id: '4', - savedConnectionType: 'autoConnectInfo', - }, - { - id: '3', - savedConnectionType: 'recent', - favorite: { name: 'Cc' }, - }, - { - id: '2', - savedConnectionType: 'favorite', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'favorite', - favorite: { name: 'Aa' }, - }, - ]); - }; - - mockStorage.emit(ConnectionStorageEvents.ConnectionsChanged); + const connections = result.current.favoriteConnections; - await waitFor(() => { - const favoriteConnections = result.current.favoriteConnections; - const nonFavoriteConnections = result.current.nonFavoriteConnections; + expect(connections.length).to.equal(2); + expect(connections[0].id).to.equal('11'); + expect(connections[1].id).to.equal('12'); + }); - expect(favoriteConnections).to.equal(initialFavoriteConnections); - expect(nonFavoriteConnections.length).to.equal(1); - expect(nonFavoriteConnections[0].id).to.equal('3'); - }); + it('should not change if only non favourite connections change', async function () { + const { result, connectionsStore } = renderHookWithConnections( + useConnectionRepository, + { connections: favoriteMockConnections } + ); + + const initialFavoriteConnections = result.current.favoriteConnections; + expect(initialFavoriteConnections.length).to.equal(2); + + await Promise.all( + nonFavoriteMockConnections.map((info) => { + return connectionsStore.actions.saveEditedConnection(info); + }) + ); + + expect(result.current.favoriteConnections).to.eq( + initialFavoriteConnections + ); }); }); describe('nonFavoriteConnections', function () { - it('should return non favourite connections sorted by name alphabetically', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '2', - savedConnectionType: 'recent', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'recent', - favorite: { name: 'Aa' }, - }, - { - id: '3', - savedConnectionType: 'autoConnectInfo', - }, - ]); - - const { result } = renderHookWithContext(() => useConnectionRepository()); - await waitFor(() => { - const connections = result.current.nonFavoriteConnections; - expect(connections.length).to.equal(2); - expect(connections[0].id).to.equal('1'); - expect(connections[1].id).to.equal('2'); + it('should return non favourite connections sorted by name alphabetically', function () { + const { result } = renderHookWithConnections(useConnectionRepository, { + connections: nonFavoriteMockConnections, }); - }); - - it('should not change if only favourite connections change', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '2', - savedConnectionType: 'recent', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'recent', - favorite: { name: 'Aa' }, - }, - ]); - - const { result } = renderHookWithContext(() => useConnectionRepository()); - - const initialNonFavoriteConnections = await waitFor(() => { - const nonFavoriteConnections = result.current.nonFavoriteConnections; - expect(nonFavoriteConnections.length).to.equal(2); - return nonFavoriteConnections; - }); - - mockStorage.loadAll = function () { - return Promise.resolve([ - { - id: '3', - savedConnectionType: 'favorite', - favorite: { name: 'Cc' }, - }, - { - id: '2', - savedConnectionType: 'recent', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'recent', - favorite: { name: 'Aa' }, - }, - ]); - }; - mockStorage.emit(ConnectionStorageEvents.ConnectionsChanged); + const connections = result.current.nonFavoriteConnections; - await waitFor(() => { - const favoriteConnections = result.current.favoriteConnections; - const nonFavoriteConnections = result.current.nonFavoriteConnections; - - expect(nonFavoriteConnections).to.equal(initialNonFavoriteConnections); - expect(favoriteConnections.length).to.equal(1); - expect(favoriteConnections[0].id).to.equal('3'); - }); + expect(connections.length).to.equal(3); + expect(connections[0].id).to.equal('21'); + expect(connections[1].id).to.equal('22'); + expect(connections[2].id).to.equal('23'); }); - }); - describe('when there is autoConnectInfo available from underlying storage', function () { - it('getConnectionInfoById should return the connection info for auto connection if the correct id is specified', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '2', - savedConnectionType: 'favorite', - favorite: { name: 'Bb' }, - }, - { - id: '1', - savedConnectionType: 'favorite', - favorite: { name: 'Aa' }, - }, - { - id: '3', - savedConnectionType: 'autoConnectInfo', - }, - ]); - - const { result } = renderHookWithContext(() => useConnectionRepository()); - await waitFor(() => { - expect(result.current.getConnectionInfoById('3')).to.deep.equal({ - id: '3', - savedConnectionType: 'autoConnectInfo', - }); - }); + it('should not change if only favourite connections change', async function () { + const { result, connectionsStore } = renderHookWithConnections( + useConnectionRepository, + { connections: nonFavoriteMockConnections } + ); + + const initialNonFavoriteConnections = + result.current.nonFavoriteConnections; + + await Promise.all( + favoriteMockConnections.map((info) => { + return connectionsStore.actions.saveEditedConnection(info); + }) + ); + + expect(result.current.nonFavoriteConnections).to.equal( + initialNonFavoriteConnections + ); }); }); - describe('#saveConnection', function () { + describe('store.saveEditedConnection', function () { it('should save a new connection if it has a valid connection string', async function () { - mockStorage = new InMemoryConnectionStorage([]); - const saveSpy = spy(mockStorage, 'save'); - const { result } = renderHookWithContext(() => useConnectionRepository()); + const connectionInfo = createDefaultConnectionInfo(); + const { result, connectionsStore, connectionStorage } = + renderHookWithConnections(useConnectionRepository, { + // We don't allow to save connections that are not in state with + // actions, so put one in the store + connections: [connectionInfo], + }); + + const saveSpy = Sinon.spy(connectionStorage, 'save'); + // Update connection string on existing connection const connectionToSave = { - id: '1', - connectionOptions: { connectionString: 'mongodb://localhost:27017' }, + ...connectionInfo, + connectionOptions: { connectionString: 'mongodb://example.com:1337' }, }; - - await result.current.saveConnection(connectionToSave); + await connectionsStore.actions.saveEditedConnection(connectionToSave); expect(saveSpy).to.have.been.calledOnceWith({ connectionInfo: connectionToSave, }); - }); - - it('should merge the connection info is one was already saved with the same id', async function () { - mockStorage = new InMemoryConnectionStorage([ - { id: '1', savedConnectionType: 'favorite' }, - ]); - const saveSpy = spy(mockStorage, 'save'); - const { result } = renderHookWithContext(() => useConnectionRepository()); - const connectionToSave = { - id: '1', - connectionOptions: { connectionString: 'mongodb://localhost:27017' }, - }; + expect(result.current.nonFavoriteConnections[0]).to.have.nested.property( + 'connectionOptions.connectionString', + 'mongodb://example.com:1337' + ); + }); - await result.current.saveConnection(connectionToSave); + it('should not save a new connection if it has an invalid connection string', async function () { + const connectionInfo = createDefaultConnectionInfo(); + const { result, connectionsStore, connectionStorage } = + renderHookWithConnections(useConnectionRepository, { + // We don't allow to save connections that are not in state with + // actions, so put one in the store + connections: [connectionInfo], + }); - expect(saveSpy).to.have.been.calledOnceWith({ - connectionInfo: { - id: '1', - savedConnectionType: 'favorite', - connectionOptions: { connectionString: 'mongodb://localhost:27017' }, - }, - }); - }); + const saveSpy = Sinon.spy(connectionStorage, 'save'); - it('should merge oidc connection info if exists', async function () { - mockStorage = new InMemoryConnectionStorage([ - { - id: '1', - savedConnectionType: 'favorite', - connectionOptions: { - connectionString: - 'mongodb://127.0.0.1:34455/?authMechanism=MONGODB-OIDC', - }, - }, - ]); - const saveSpy = spy(mockStorage, 'save'); - - const { result } = renderHookWithContext(() => useConnectionRepository()); + // Update connection string on existing connection const connectionToSave = { - id: '1', - connectionOptions: { - connectionString: - 'mongodb://127.0.0.1:34455/?authMechanism=MONGODB-OIDC', - oidc: { serializedState: 'someNewState' }, - }, + ...connectionInfo, + connectionOptions: { connectionString: 'foobar' }, }; + await connectionsStore.actions.saveEditedConnection(connectionToSave); - await result.current.saveConnection(connectionToSave); + expect(saveSpy).not.to.have.been.called; - expect(saveSpy).to.have.been.calledOnceWith({ - connectionInfo: { - id: '1', - savedConnectionType: 'favorite', - connectionOptions: { - connectionString: - 'mongodb://127.0.0.1:34455/?authMechanism=MONGODB-OIDC', - oidc: { serializedState: 'someNewState' }, - }, - }, - }); + expect(result.current.nonFavoriteConnections[0]).to.have.nested.property( + 'connectionOptions.connectionString', + 'mongodb://localhost:27017' + ); }); + }); - it('should not save a new connection if it has an invalid connection string', async function () { - mockStorage = new InMemoryConnectionStorage([]); - const saveSpy = spy(mockStorage, 'save'); - const { result } = renderHookWithContext(() => useConnectionRepository()); - - try { - await result.current.saveConnection({ - id: '1', - connectionOptions: { connectionString: 'mongo://table.row:8080' }, + describe('store.removeConnection', function () { + it('should delete a saved connection from the underlying storage', function () { + const connectionInfo = createDefaultConnectionInfo(); + const { result, connectionsStore, connectionStorage } = + renderHookWithConnections(useConnectionRepository, { + // We don't allow to save connections that are not in state with + // actions, so put one in the store + connections: [connectionInfo], }); - expect.fail('Expected saveConnection to throw an exception.'); - expect(saveSpy).to.not.have.been.calledOnce; - } catch (ex) { - expect(ex).to.have.property( - 'message', - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' - ); - } - }); - }); + const deleteSpy = Sinon.spy(connectionStorage, 'delete'); - describe('#deleteConnection', function () { - it('should delete a saved connection from the underlying storage', async function () { - mockStorage = new InMemoryConnectionStorage([]); - const deleteSpy = spy(mockStorage, 'delete'); - const { result } = renderHookWithContext(() => useConnectionRepository()); - const connectionToDelete = { - id: '1', - connectionOptions: { connectionString: '' }, - }; + expect(result.current.nonFavoriteConnections).to.have.lengthOf(1); + + connectionsStore.actions.removeConnection(connectionInfo.id); - await result.current.deleteConnection(connectionToDelete); + expect(deleteSpy).to.have.been.calledOnceWith({ id: connectionInfo.id }); - expect(deleteSpy).to.have.been.calledOnceWith(connectionToDelete); + expect(result.current.nonFavoriteConnections).to.have.lengthOf(0); }); }); }); diff --git a/packages/compass-connections/src/hooks/use-connection-repository.ts b/packages/compass-connections/src/hooks/use-connection-repository.ts index 13e11dbd6f2..8114ec4fafd 100644 --- a/packages/compass-connections/src/hooks/use-connection-repository.ts +++ b/packages/compass-connections/src/hooks/use-connection-repository.ts @@ -1,263 +1,59 @@ -import { usePreferencesContext } from 'compass-preferences-model/provider'; +import React, { useMemo } from 'react'; import { getConnectionTitle, type ConnectionInfo, } from '@mongodb-js/connection-info'; -import ConnectionString from 'mongodb-connection-string-url'; -import { merge } from 'lodash'; -import isEqual from 'lodash/isEqual'; -import { - ConnectionStorageEvents, - useConnectionStorageContext, -} from '@mongodb-js/connection-storage/provider'; -import { useState, useEffect, useCallback, useRef } from 'react'; -import { BSON } from 'bson'; - -type DeepPartial = T extends object - ? { [P in keyof T]?: DeepPartial } - : T; - -export type PartialConnectionInfo = DeepPartial & - Pick; - -/** - * Same as _.isEqual but taking into consideration BSON values. - */ -export function areConnectionsEqual(listA: T[], listB: T[]): boolean { - return isEqual( - listA.map((a: any) => BSON.serialize(a)), - listB.map((b: any) => BSON.serialize(b)) - ); -} - -function ensureWellFormedConnectionString(connectionString: string) { - new ConnectionString(connectionString); -} - -function sortedAlphabetically(a: ConnectionInfo, b: ConnectionInfo): number { - const aTitle = getConnectionTitle(a).toLocaleLowerCase(); - const bTitle = getConnectionTitle(b).toLocaleLowerCase(); - return aTitle.localeCompare(bTitle); -} +import { useCallback, useRef } from 'react'; +import { createServiceLocator } from 'hadron-app-registry'; +import { useConnections, useConnectionsList } from '../stores/store-context'; export type ConnectionRepository = { favoriteConnections: ConnectionInfo[]; nonFavoriteConnections: ConnectionInfo[]; - autoConnectInfo?: ConnectionInfo; - saveConnection: (info: PartialConnectionInfo) => Promise; - deleteConnection: (info: ConnectionInfo) => Promise; - findConnectionInfo: ( - fn: ( - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => boolean - ) => ConnectionInfo | undefined; - filterConnectionInfo: ( - fn: ( - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => boolean - ) => ConnectionInfo[]; - reduceConnectionInfo: ( - fn: ( - previousValue: T, - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => T, - initialValue: T - ) => T; getConnectionInfoById: ( id: ConnectionInfo['id'] ) => ConnectionInfo | undefined; getConnectionTitleById: (id: ConnectionInfo['id']) => string | undefined; }; +/** + * @deprecated use connections-store hooks instead + */ export function useConnectionRepository(): ConnectionRepository { - const storage = useConnectionStorageContext(); - - const [favoriteConnections, setFavoriteConnections] = useState< - ConnectionInfo[] - >([]); - - const [nonFavoriteConnections, setNonFavoriteConnections] = useState< - ConnectionInfo[] - >([]); - - const [autoConnectInfo, setAutoConnectInfo] = useState< - ConnectionInfo | undefined - >(undefined); - - const favoriteConnectionsRef = useRef(favoriteConnections); - favoriteConnectionsRef.current = favoriteConnections; - - const nonFavoriteConnectionsRef = useRef(nonFavoriteConnections); - nonFavoriteConnectionsRef.current = nonFavoriteConnections; - - const autoConnectInfoRef = useRef(autoConnectInfo); - autoConnectInfoRef.current = autoConnectInfo; - - const preferences = usePreferencesContext(); - - useEffect(() => { - async function updateListsOfConnections() { - const allConnections = (await storage.loadAll()) || []; - - const newFavoriteConnections = allConnections - .filter((connection) => connection.savedConnectionType === 'favorite') - .sort(sortedAlphabetically); - - const newNonFavoriteConnections = allConnections - .filter( - ({ savedConnectionType }) => - savedConnectionType !== 'favorite' && - savedConnectionType !== 'autoConnectInfo' - ) - .sort(sortedAlphabetically); - - const autoConnectInfo = allConnections.find( - ({ savedConnectionType }) => savedConnectionType === 'autoConnectInfo' - ); - - setFavoriteConnections((prevList) => { - if (areConnectionsEqual(prevList, newFavoriteConnections)) { - return prevList; - } else { - return newFavoriteConnections; - } - }); - - setNonFavoriteConnections((prevList) => { - if (areConnectionsEqual(prevList, newNonFavoriteConnections)) { - return prevList; - } else { - return newNonFavoriteConnections; - } - }); - - if (autoConnectInfo) { - setAutoConnectInfo((prevAutoConnectInfo) => { - if (prevAutoConnectInfo?.id !== autoConnectInfo.id) { - return autoConnectInfo; - } else { - return prevAutoConnectInfo; - } - }); - } - } - - void updateListsOfConnections(); - - function updateListsOfConnectionsSubscriber() { - void updateListsOfConnections(); - } - - storage.on( - ConnectionStorageEvents.ConnectionsChanged, - updateListsOfConnectionsSubscriber + const nonFavoriteConnections = useConnectionsList((connection) => { + return ( + !connection.isBeingCreated && + !connection.isAutoconnectInfo && + connection.info.savedConnectionType !== 'favorite' ); + }); + + const nonFavoriteConnectionsInfoOnly = useMemo(() => { + return nonFavoriteConnections.map((connection) => { + return connection.info; + }); + }, [nonFavoriteConnections]); + + const favoriteConnections = useConnectionsList((connection) => { + return ( + !connection.isBeingCreated && + connection.info.savedConnectionType === 'favorite' + ); + }); - return () => { - storage.off( - ConnectionStorageEvents.ConnectionsChanged, - updateListsOfConnectionsSubscriber - ); - }; - }, [storage]); - - const saveConnection = useCallback( - async (info: PartialConnectionInfo) => { - const oldConnectionInfo = await storage.load({ id: info.id }); - const infoToSave = ( - oldConnectionInfo ? merge(oldConnectionInfo, info) : info - ) as ConnectionInfo; - - ensureWellFormedConnectionString( - infoToSave.connectionOptions?.connectionString - ); - - if (!preferences.getPreferences().persistOIDCTokens) { - if (infoToSave.connectionOptions.oidc?.serializedState) { - infoToSave.connectionOptions.oidc.serializedState = undefined; - } - } - - await storage.save?.({ connectionInfo: infoToSave }); - return infoToSave; - }, - [storage, preferences] - ); - - const deleteConnection = useCallback( - async (info: ConnectionInfo) => { - await storage.delete?.(info); - }, - [storage] - ); - - const findConnectionInfo = useCallback( - ( - fn: ( - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => boolean - ) => { - const allConnections = [ - ...favoriteConnectionsRef.current, - ...nonFavoriteConnectionsRef.current, - ...(autoConnectInfoRef.current ? [autoConnectInfoRef.current] : []), - ]; - return allConnections.find(fn); - }, - [] - ); - - const filterConnectionInfo = useCallback( - ( - fn: ( - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => boolean - ) => { - const allConnections = [ - ...favoriteConnectionsRef.current, - ...nonFavoriteConnectionsRef.current, - ...(autoConnectInfoRef.current ? [autoConnectInfoRef.current] : []), - ]; - return allConnections.filter(fn); - }, - [] - ); + const favoriteConnectionsInfoOnly = useMemo(() => { + return favoriteConnections.map((connection) => { + return connection.info; + }); + }, [favoriteConnections]); - const reduceConnectionInfo = useCallback( - ( - fn: ( - previousValue: T, - value: ConnectionInfo, - index: number, - items: ConnectionInfo[] - ) => T, - initialValue: T - ): T => { - const allConnections = [ - ...favoriteConnectionsRef.current, - ...nonFavoriteConnectionsRef.current, - ...(autoConnectInfoRef.current ? [autoConnectInfoRef.current] : []), - ]; - return allConnections.reduce(fn, initialValue); - }, - [] - ); + const { getConnectionById } = useConnections(); const getConnectionInfoById = useCallback( (connectionInfoId: ConnectionInfo['id']) => { - return findConnectionInfo(({ id }) => id === connectionInfoId); + return getConnectionById(connectionInfoId)?.info; }, - [findConnectionInfo] + [getConnectionById] ); const getConnectionTitleById = useCallback( @@ -271,15 +67,60 @@ export function useConnectionRepository(): ConnectionRepository { ); return { - findConnectionInfo, - filterConnectionInfo, - reduceConnectionInfo, getConnectionInfoById, getConnectionTitleById, - favoriteConnections, - nonFavoriteConnections, - autoConnectInfo, - saveConnection, - deleteConnection, + favoriteConnections: favoriteConnectionsInfoOnly, + nonFavoriteConnections: nonFavoriteConnectionsInfoOnly, + }; +} + +type FirstArgument = F extends (...args: [infer A, ...any]) => any + ? A + : F extends { new (...args: [infer A, ...any]): any } + ? A + : never; + +/** + * @deprecated instead of using HOC, refactor class component to functional + * component + */ +function withConnectionRepository< + T extends ((...args: any[]) => any) | { new (...args: any[]): any } +>( + ReactComponent: T +): React.FunctionComponent, 'connectionRepository'>> { + const WithConnectionRepository = ( + props: Omit, 'connectionRepository'> & React.Attributes + ) => { + const connectionRepository = useConnectionRepository(); + return React.createElement(ReactComponent, { + ...props, + connectionRepository, + }); }; + return WithConnectionRepository; } + +export { withConnectionRepository }; + +export type ConnectionRepositoryAccess = Pick< + ConnectionRepository, + 'getConnectionInfoById' +>; + +/** + * @deprecated use `connectionsLocator` instead + */ +export const connectionRepositoryAccessLocator = createServiceLocator( + (): ConnectionRepositoryAccess => { + const repository = useConnectionRepository(); + const repositoryRef = useRef(repository); + repositoryRef.current = repository; + return { + getConnectionInfoById(id: ConnectionInfo['id']) { + return repositoryRef.current.getConnectionInfoById(id); + }, + }; + }, + 'connectionRepositoryAccessLocator' +); diff --git a/packages/compass-connections/src/hooks/use-connection-status.spec.ts b/packages/compass-connections/src/hooks/use-connection-status.spec.ts deleted file mode 100644 index 266d5011604..00000000000 --- a/packages/compass-connections/src/hooks/use-connection-status.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { expect } from 'chai'; -import { useConnectionStatus } from './use-connection-status'; -import { renderHook } from '@testing-library/react-hooks'; -import { createElement } from 'react'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; -import { - ConnectionsManager, - ConnectionsManagerEvents, - ConnectionsManagerProvider, -} from '../provider'; - -const CONNECTION_INFO: ConnectionInfo = { - id: '1234', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, -}; - -describe('useConnectionStatus', function () { - let renderHookWithContext: typeof renderHook; - let connectionManager: ConnectionsManager; - - beforeEach(function () { - connectionManager = new ConnectionsManager({} as any); - - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(ConnectionsManagerProvider, { - value: connectionManager, - children: children, - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); - - describe('status of a connection', function () { - it('should return it from the connection manager', function () { - const { result } = renderHookWithContext(() => - useConnectionStatus(CONNECTION_INFO.id) - ); - const status = result.current.status; - expect(status).to.equal('disconnected'); - }); - - describe('when there is an update', function () { - let result: ReturnType['result']; - - beforeEach(function () { - const hookResult = renderHookWithContext(() => - useConnectionStatus(CONNECTION_INFO.id) - ); - result = hookResult.result; - - const connectionManagerInspectable = connectionManager as any; - connectionManagerInspectable.connectionStatuses.set( - '1234', - 'connected' - ); - - connectionManager.emit( - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - '1234', - {} as any - ); - }); - - it('should update the status', function () { - expect(result.current.status).to.equal('connected'); - }); - }); - }); -}); diff --git a/packages/compass-connections/src/hooks/use-connection-status.ts b/packages/compass-connections/src/hooks/use-connection-status.ts deleted file mode 100644 index 1de401775aa..00000000000 --- a/packages/compass-connections/src/hooks/use-connection-status.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useConnectionsManagerContext } from '../provider'; -import { - type ConnectionStatus, - ConnectionsManagerEvents, -} from '../connections-manager'; - -import { type ConnectionInfo } from '@mongodb-js/connection-info'; -import { useEffect, useState } from 'react'; - -export function useConnectionStatus(connectionInfoId: ConnectionInfo['id']): { - status: ConnectionStatus; -} { - const connectionManager = useConnectionsManagerContext(); - const [status, setStatus] = useState( - connectionManager.statusOf(connectionInfoId) - ); - - useEffect(() => { - const updateStatus = () => { - setStatus(connectionManager.statusOf(connectionInfoId)); - }; - - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionManager.on(event, updateStatus); - } - - return () => { - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionManager.off(event, updateStatus); - } - }; - }, [connectionManager, connectionInfoId]); - - return { status }; -} diff --git a/packages/compass-connections/src/hooks/use-connections-with-status.spec.ts b/packages/compass-connections/src/hooks/use-connections-with-status.spec.ts deleted file mode 100644 index f9788c4d43c..00000000000 --- a/packages/compass-connections/src/hooks/use-connections-with-status.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - ConnectionsManager, - ConnectionsManagerProvider, - useConnectionsWithStatus, -} from '../provider'; -import { renderHook } from '@testing-library/react-hooks'; -import { createElement } from 'react'; -import { - ConnectionStorageProvider, - InMemoryConnectionStorage, - type ConnectionInfo, - type ConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { expect } from 'chai'; -import Sinon from 'sinon'; -import { waitFor } from '@testing-library/dom'; -import { ConnectionStatus } from '../connections-manager'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; - -const mockConnections: ConnectionInfo[] = [ - { - id: 'turtle', - connectionOptions: { - connectionString: 'mongodb://turtle', - }, - favorite: { - name: 'turtles', - }, - savedConnectionType: 'favorite', - }, - { - id: 'oranges', - connectionOptions: { - connectionString: 'mongodb://peaches', - }, - favorite: { - name: 'peaches', - }, - savedConnectionType: 'favorite', - }, -]; - -function createConnectionsManager(connectFn?: any) { - return new ConnectionsManager({ - logger: createNoopLogger().log.unbound, - __TEST_CONNECT_FN: connectFn, - }); -} - -describe('useConnectionsWithStatus', function () { - let renderHookWithContext: typeof renderHook; - let connectionsManager: ConnectionsManager; - let mockConnectionStorage: ConnectionStorage; - - beforeEach(function () { - connectionsManager = createConnectionsManager(); - mockConnectionStorage = new InMemoryConnectionStorage(mockConnections); - - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(ConnectionStorageProvider, { - value: mockConnectionStorage, - children: [ - createElement(ConnectionsManagerProvider, { - value: connectionsManager, - children: children, - }), - ], - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); - - it('should return all connections with disconnected state', async function () { - const { result } = renderHookWithContext(() => useConnectionsWithStatus()); - await waitFor(() => expect(result.current).to.have.length(2)); - expect(result.current[0].connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - expect(result.current[1].connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - }); - - it('should update the list when a connection switches it status', async function () { - let connectBehaviour: - | 'mimick-connecting' - | 'mimick-failed' - | 'mimick-connected' = 'mimick-connecting'; - const testConnectFn = Sinon.stub().callsFake(async () => { - // mimicking the connecting state here - if (connectBehaviour === 'mimick-connecting') { - await new Promise((resolve) => { - setTimeout(() => resolve({}), 1500); - }); - } else if (connectBehaviour === 'mimick-failed') { - return Promise.reject(new Error('DataService kaput')); - } else if (connectBehaviour === 'mimick-connected') { - return Promise.resolve({ disconnect() {} }); - } - }); - connectionsManager = createConnectionsManager(testConnectFn); - - const { result } = renderHookWithContext(() => useConnectionsWithStatus()); - await waitFor(() => expect(result.current).to.have.length(2)); - // Starts with disconnected - expect(result.current[0].connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - expect(result.current[1].connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - - // Now switching to connecting state - void connectionsManager.connect(mockConnections[0]).catch(() => { - // ignore this silently - }); - await waitFor(() => { - const turtleConnection = result.current.find(({ connectionInfo }) => { - return connectionInfo.id === 'turtle'; - }); - return expect(turtleConnection?.connectionStatus).to.equal( - ConnectionStatus.Connecting - ); - }); - - // Now cancelling the connection - void connectionsManager.cancelAllConnectionAttempts(); - await waitFor(() => { - const turtleConnection = result.current.find(({ connectionInfo }) => { - return connectionInfo.id === 'turtle'; - }); - return expect(turtleConnection?.connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - }); - - // Now connecting again but to fail - connectBehaviour = 'mimick-failed'; - void connectionsManager.connect(mockConnections[0]).catch(() => { - // ignore this silently - }); - await waitFor(() => { - const turtleConnection = result.current.find(({ connectionInfo }) => { - return connectionInfo.id === 'turtle'; - }); - return expect(turtleConnection?.connectionStatus).to.equal( - ConnectionStatus.Failed - ); - }); - - // Now connecting again but to succeed - connectBehaviour = 'mimick-connected'; - void connectionsManager.connect(mockConnections[0]).catch(() => { - // ignore this silently - }); - await waitFor(() => { - const turtleConnection = result.current.find(({ connectionInfo }) => { - return connectionInfo.id === 'turtle'; - }); - return expect(turtleConnection?.connectionStatus).to.equal( - ConnectionStatus.Connected - ); - }); - - // Now close the connection - void connectionsManager.closeConnection('turtle').catch(() => { - // ignore this silently - }); - await waitFor(() => { - const turtleConnection = result.current.find(({ connectionInfo }) => { - return connectionInfo.id === 'turtle'; - }); - return expect(turtleConnection?.connectionStatus).to.equal( - ConnectionStatus.Disconnected - ); - }); - }); -}); diff --git a/packages/compass-connections/src/hooks/use-connections-with-status.spec.tsx b/packages/compass-connections/src/hooks/use-connections-with-status.spec.tsx new file mode 100644 index 00000000000..bbe3957841e --- /dev/null +++ b/packages/compass-connections/src/hooks/use-connections-with-status.spec.tsx @@ -0,0 +1,109 @@ +import { useConnectionsWithStatus } from './use-connections-with-status'; +import { type ConnectionInfo } from '@mongodb-js/connection-storage/provider'; +import { expect } from 'chai'; +import Sinon from 'sinon'; +import { renderHookWithConnections } from '../test'; + +const mockConnections: ConnectionInfo[] = [ + { + id: 'turtle', + connectionOptions: { + connectionString: 'mongodb://turtle', + }, + favorite: { + name: 'turtles', + }, + savedConnectionType: 'favorite', + }, + { + id: 'oranges', + connectionOptions: { + connectionString: 'mongodb://peaches', + }, + favorite: { + name: 'peaches', + }, + savedConnectionType: 'favorite', + }, +]; + +describe('useConnectionsWithStatus', function () { + it('should return all connections with initial state', function () { + const { result } = renderHookWithConnections(useConnectionsWithStatus, { + connections: mockConnections, + }); + expect(result.current).to.have.lengthOf(2); + expect(result.current[0]).to.have.property('connectionStatus', 'initial'); + expect(result.current[1]).to.have.property('connectionStatus', 'initial'); + }); + + it('should update the list when a connection switches it status', async function () { + const connectFnStub = Sinon.stub() + .onFirstCall() + .callsFake(() => { + return new Promise(() => { + // do not resolve, we will cancel this one + }); + }) + .onSecondCall() + .rejects(new Error('Failed to connect')) + .onThirdCall() + .callsFake(() => { + return {}; + }); + + const { result, connectionsStore } = renderHookWithConnections( + useConnectionsWithStatus, + { + connectFn: connectFnStub, + connections: mockConnections, + } + ); + + function getConnectionById(id) { + return result.current.find((conn) => { + return conn.connectionInfo.id === id; + }); + } + + // Starts with initial + expect(result.current[0]).to.have.property('connectionStatus', 'initial'); + + // Now switching to connecting state + const connectPromise = connectionsStore.actions.connect(mockConnections[0]); + expect(getConnectionById(mockConnections[0].id)).to.have.property( + 'connectionStatus', + 'connecting' + ); + + // Now cancelling the connection + connectionsStore.actions.disconnect(mockConnections[0].id); + // Wait for connection process to fully resolve + await connectPromise; + expect(getConnectionById(mockConnections[0].id)).to.have.property( + 'connectionStatus', + 'disconnected' + ); + + // Now connecting again but to fail + await connectionsStore.actions.connect(mockConnections[0]); + expect(getConnectionById(mockConnections[0].id)).to.have.property( + 'connectionStatus', + 'failed' + ); + + // Now connecting again but to succeed + await connectionsStore.actions.connect(mockConnections[0]); + expect(getConnectionById(mockConnections[0].id)).to.have.property( + 'connectionStatus', + 'connected' + ); + + // Now close the connection + connectionsStore.actions.disconnect(mockConnections[0].id); + expect(getConnectionById(mockConnections[0].id)).to.have.property( + 'connectionStatus', + 'disconnected' + ); + }); +}); diff --git a/packages/compass-connections/src/hooks/use-connections-with-status.ts b/packages/compass-connections/src/hooks/use-connections-with-status.ts index cedd2e8fd6b..5af286e2a63 100644 --- a/packages/compass-connections/src/hooks/use-connections-with-status.ts +++ b/packages/compass-connections/src/hooks/use-connections-with-status.ts @@ -1,108 +1,39 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import type { ConnectionStatus, ConnectionInfo } from '../provider'; +import { useMemo } from 'react'; +import type { ConnectionInfo } from '../provider'; +import type { ConnectionState } from '../stores/connections-store-redux'; import { - ConnectionsManagerEvents, - useConnectionRepository, - useConnectionsManagerContext, - areConnectionsEqual, -} from '../provider'; + useConnectionForId, + useConnectionsList, +} from '../stores/store-context'; type ConnectionInfoWithStatus = { connectionInfo: ConnectionInfo; - connectionStatus: ConnectionStatus; + connectionStatus: ConnectionState['status']; }; +/** + * @deprecated use connections-store hooks instead + */ export function useConnectionInfoStatus( connectionId: string -): ConnectionStatus | null { - const connectionsManager = useConnectionsManagerContext(); - const [status, setStatus] = useState(() => { - return connectionsManager.statusOf(connectionId); - }); - useEffect(() => { - const updateOnStatusChange = () => { - setStatus(connectionsManager.statusOf(connectionId)); - }; - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionsManager.on(event, updateOnStatusChange); - } - return () => { - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionsManager.off(event, updateOnStatusChange); - } - }; - }, [connectionId, connectionsManager]); - return status; +): ConnectionInfoWithStatus['connectionStatus'] | null { + const connection = useConnectionForId(connectionId); + return connection?.status ?? 'disconnected'; } +/** + * @deprecated use connections-store hooks instead + */ export function useConnectionsWithStatus(): ConnectionInfoWithStatus[] { - // TODO(COMPASS-7397): services should not be used directly in render method, - // when this code is refactored to use the hadron plugin interface, storage - // should be handled through the plugin activation lifecycle - const connectionsManager = useConnectionsManagerContext(); - const { favoriteConnections, nonFavoriteConnections, autoConnectInfo } = - useConnectionRepository(); - const allConnections = useMemo(() => { - return favoriteConnections.concat( - nonFavoriteConnections, - autoConnectInfo ? autoConnectInfo : [] - ); - }, [favoriteConnections, nonFavoriteConnections, autoConnectInfo]); - - const [connectionsWithStatus, setConnectionsWithStatus] = useState< - ConnectionInfoWithStatus[] - >(() => { - return allConnections.map((connection) => { - return { - connectionInfo: connection, - connectionStatus: connectionsManager.statusOf(connection.id), - }; - }); - }); - - const updateListRef = useRef(() => { - // We need a stable, always up to date, ref for update method. To make TS - // happy, we initially assign a no-op and then immediately reassign with the - // implementation instead of starting with undefined and a generic type - // provided (otherwise we end up with `MutableRef` type that's harder to - // account for on the call site) + const connections = useConnectionsList((connection) => { + return !connection.isBeingCreated; }); - updateListRef.current = () => { - const newConnectionsList = allConnections.map((connection) => { + return useMemo(() => { + return connections.map((connection) => { return { - connectionInfo: connection, - connectionStatus: connectionsManager.statusOf(connection.id), + connectionInfo: connection.info, + connectionStatus: connection.status, }; }); - setConnectionsWithStatus((prevList) => { - return areConnectionsEqual( - prevList, - newConnectionsList - ) - ? prevList - : newConnectionsList; - }); - }; - - useEffect(() => { - updateListRef.current(); - }, [favoriteConnections, nonFavoriteConnections, autoConnectInfo]); - - useEffect(() => { - const updateOnStatusChange = () => { - updateListRef.current(); - }; - - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionsManager.on(event, updateOnStatusChange); - } - - return () => { - for (const event of Object.values(ConnectionsManagerEvents)) { - connectionsManager.off(event, updateOnStatusChange); - } - }; - }, [connectionsManager]); - - return connectionsWithStatus; + }, [connections]); } diff --git a/packages/compass-connections/src/hooks/use-tab-connection-theme.spec.ts b/packages/compass-connections/src/hooks/use-tab-connection-theme.spec.ts index 9e017fedce6..5b0682ef54b 100644 --- a/packages/compass-connections/src/hooks/use-tab-connection-theme.spec.ts +++ b/packages/compass-connections/src/hooks/use-tab-connection-theme.spec.ts @@ -1,21 +1,8 @@ import { expect } from 'chai'; -import { renderHook } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; -import { createElement } from 'react'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; import { useTabConnectionTheme } from '../provider'; -import { - type ConnectionStorage, - ConnectionStorageProvider, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { - type PreferencesAccess, - createSandboxFromDefaultPreferences, -} from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; +import { renderHookWithConnections } from '../test'; -const CONNECTION_INFO: ConnectionInfo = { +const CONNECTION_INFO = { id: '1234', connectionOptions: { connectionString: 'mongodb://localhost:27017', @@ -26,7 +13,7 @@ const CONNECTION_INFO: ConnectionInfo = { }, }; -const CONNECTION_INFO_NO_COLOR: ConnectionInfo = { +const CONNECTION_INFO_NO_COLOR = { id: '1234', connectionOptions: { connectionString: 'mongodb://localhost:27017', @@ -36,7 +23,7 @@ const CONNECTION_INFO_NO_COLOR: ConnectionInfo = { }, }; -const CONNECTION_INFO_INVALID_COLOR: ConnectionInfo = { +const CONNECTION_INFO_INVALID_COLOR = { id: '1234', connectionOptions: { connectionString: 'mongodb://localhost:27017', @@ -48,84 +35,58 @@ const CONNECTION_INFO_INVALID_COLOR: ConnectionInfo = { }; describe('useTabConnectionTheme', function () { - let renderHookWithContext: typeof renderHook; - let mockStorage: ConnectionStorage; - let preferencesAccess: PreferencesAccess; - - beforeEach(async function () { - preferencesAccess = await createSandboxFromDefaultPreferences(); - await preferencesAccess.savePreferences({ - enableNewMultipleConnectionSystem: true, - }); - - mockStorage = new InMemoryConnectionStorage([CONNECTION_INFO]); - renderHookWithContext = (callback, options) => { - const wrapper: React.FC = ({ children }) => - createElement(PreferencesProvider, { - value: preferencesAccess, - children: createElement(ConnectionStorageProvider, { - value: mockStorage, - children, - }), - }); - return renderHook(callback, { wrapper, ...options }); - }; - }); - describe('when a connection does not exist', function () { it('should not return a theme', function () { - const { result } = renderHookWithContext(() => { - const { getThemeOf } = useTabConnectionTheme(); - return getThemeOf('NON_EXISTING'); + const { result } = renderHookWithConnections(useTabConnectionTheme, { + preferences: { enableMultipleConnectionSystem: true }, }); - expect(result.current).to.be.undefined; + expect(result.current.getThemeOf('NON_EXISTING')).to.be.undefined; }); }); describe('when a connection exists', function () { - it('should return the theme with the connection colors', async function () { - const { result } = renderHookWithContext(() => { - const { getThemeOf } = useTabConnectionTheme(); - return getThemeOf(CONNECTION_INFO.id); + it('should return the theme with the connection colors', function () { + const { result } = renderHookWithConnections(useTabConnectionTheme, { + preferences: { enableMultipleConnectionSystem: true }, + connections: [CONNECTION_INFO], }); - await waitFor(() => { - expect(result.current).to.deep.equal({ - '&:focus-visible': { - '--workspace-tab-border-color': '#016BF8', - '--workspace-tab-selected-color': '#016BF8', - }, - '--workspace-tab-border-color': '#E8EDEB', - '--workspace-tab-color': '#5C6C75', - '--workspace-tab-selected-background-color': '#FFFFFF', - '--workspace-tab-selected-color': '#1C2D38', - '--workspace-tab-selected-top-border-color': '#C2E5FF', - '--workspace-tab-top-border-color': '#D5EFFF', - }); + expect(result.current.getThemeOf(CONNECTION_INFO.id)).to.deep.equal({ + '&:focus-visible': { + '--workspace-tab-border-color': '#016BF8', + '--workspace-tab-selected-color': '#016BF8', + }, + '--workspace-tab-background-color': '#D5EFFF', + '--workspace-tab-border-color': '#E8EDEB', + '--workspace-tab-color': '#5C6C75', + '--workspace-tab-selected-background-color': '#FFFFFF', + '--workspace-tab-selected-color': '#1C2D38', + '--workspace-tab-selected-top-border-color': '#C2E5FF', + '--workspace-tab-top-border-color': '#D5EFFF', }); }); - it('should not return a theme when there is no color', async function () { - const { result } = renderHookWithContext(() => { - const { getThemeOf } = useTabConnectionTheme(); - return getThemeOf(CONNECTION_INFO_NO_COLOR.id); + it('should not return a theme when there is no color', function () { + const { result } = renderHookWithConnections(useTabConnectionTheme, { + preferences: { enableMultipleConnectionSystem: true }, + connections: [CONNECTION_INFO_NO_COLOR], }); - await waitFor(() => { - expect(result.current).to.equal(undefined); - }); + expect(result.current.getThemeOf(CONNECTION_INFO_NO_COLOR.id)).to.equal( + undefined + ); }); - it('should not return a theme when the color is invalid', async function () { - const { result } = renderHookWithContext(() => { - const { getThemeOf } = useTabConnectionTheme(); - return getThemeOf(CONNECTION_INFO_INVALID_COLOR.id); + it('should not return a theme when the color is invalid', function () { + const { result } = renderHookWithConnections(useTabConnectionTheme, { + preferences: { enableMultipleConnectionSystem: true }, + connections: [CONNECTION_INFO_INVALID_COLOR], }); - await waitFor(() => { - expect(result.current).to.equal(undefined); - }); + expect( + result.current.getThemeOf(CONNECTION_INFO_INVALID_COLOR.id) + ).to.equal(undefined); }); }); }); diff --git a/packages/compass-connections/src/hooks/use-tab-connection-theme.ts b/packages/compass-connections/src/hooks/use-tab-connection-theme.ts index f673cf9fa57..643cf950ab2 100644 --- a/packages/compass-connections/src/hooks/use-tab-connection-theme.ts +++ b/packages/compass-connections/src/hooks/use-tab-connection-theme.ts @@ -1,6 +1,6 @@ import { type ConnectionInfo } from '@mongodb-js/connection-info'; import { useConnectionColor } from '@mongodb-js/connection-form'; -import { useConnectionRepository } from '../provider'; +import { useConnectionRepository } from './use-connection-repository'; import { useDarkMode, type TabTheme } from '@mongodb-js/compass-components'; import { palette } from '@mongodb-js/compass-components'; import { useCallback } from 'react'; @@ -19,9 +19,13 @@ export function useTabConnectionTheme(): ThemeProvider { const { getConnectionInfoById } = useConnectionRepository(); const darkTheme = useDarkMode(); const isMultipleConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); + // TODO: this method is not reactive and works only by accident, refactor the + // hook to explicitly track changes to color in connections, otherwise the + // value of the theme might be stale when we remove `useConnectionRepository` + // hook completely const getThemeOf = useCallback( (connectionId: ConnectionInfo['id']) => { const connectionInfo = getConnectionInfoById(connectionId); @@ -39,6 +43,7 @@ export function useTabConnectionTheme(): ThemeProvider { } return { + '--workspace-tab-background-color': bgColor, '--workspace-tab-top-border-color': bgColor, '--workspace-tab-border-color': darkTheme ? palette.gray.dark2 diff --git a/packages/compass-connections/src/index.ts b/packages/compass-connections/src/index.ts deleted file mode 100644 index 38f06e2d113..00000000000 --- a/packages/compass-connections/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as SingleConnectionForm } from './components/legacy-connections'; -export { ConnectionsProvider as default } from './components/connections-provider'; -export { LegacyConnectionsModal } from './components/legacy-connections-modal'; -export { useConnectionFormPreferences } from './hooks/use-connection-form-preferences'; diff --git a/packages/compass-connections/src/index.tsx b/packages/compass-connections/src/index.tsx new file mode 100644 index 00000000000..06bfdbfd35d --- /dev/null +++ b/packages/compass-connections/src/index.tsx @@ -0,0 +1,102 @@ +import { registerHadronPlugin } from 'hadron-app-registry'; +import { + autoconnectCheck, + configureStore, + loadConnections, +} from './stores/connections-store-redux'; +import React, { useContext, useRef } from 'react'; +import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; +import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; +import { preferencesLocator } from 'compass-preferences-model/provider'; +import type { ConnectionInfo } from '@mongodb-js/connection-storage/provider'; +import { connectionStorageLocator } from '@mongodb-js/connection-storage/provider'; +import { + ConnectionActionsProvider, + ConnectionsStoreContext, +} from './stores/store-context'; +export { default as SingleConnectionForm } from './components/legacy-connections'; +export { LegacyConnectionsModal } from './components/legacy-connections-modal'; +export { useConnectionFormPreferences } from './hooks/use-connection-form-preferences'; +import type { connect as devtoolsConnect } from 'mongodb-data-service'; + +const ConnectionsComponent: React.FunctionComponent<{ + appName: string; + onExtraConnectionDataRequest: ( + connectionInfo: ConnectionInfo + ) => Promise<[Record, string | null]>; + onAutoconnectInfoRequest?: () => Promise; + connectFn?: typeof devtoolsConnect | undefined; + preloadStorageConnectionInfos?: ConnectionInfo[]; +}> = ({ children }) => { + return {children}; +}; + +const CompassConnectionsPlugin = registerHadronPlugin( + { + name: 'CompassConnections', + component: ConnectionsComponent, + activate( + initialProps, + { logger, preferences, connectionStorage, track }, + helpers + ) { + const store = configureStore(initialProps.preloadStorageConnectionInfos, { + logger, + preferences, + connectionStorage, + track, + getExtraConnectionData: initialProps.onExtraConnectionDataRequest, + appName: initialProps.appName, + connectFn: initialProps.connectFn, + }); + + setTimeout(() => { + void store.dispatch(loadConnections()); + if (initialProps.onAutoconnectInfoRequest) { + void store.dispatch( + autoconnectCheck(initialProps.onAutoconnectInfoRequest) + ); + } + }); + + return { + store, + deactivate: helpers.cleanup, + context: ConnectionsStoreContext, + }; + }, + }, + { + logger: createLoggerLocator('COMPASS-CONNECTIONS'), + preferences: preferencesLocator, + connectionStorage: connectionStorageLocator, + track: telemetryLocator, + } +); + +const ConnectFnContext = React.createContext< + typeof devtoolsConnect | undefined +>(undefined); + +export const ConnectFnProvider: React.FunctionComponent<{ + connect?: typeof devtoolsConnect | undefined; +}> = ({ connect, children }) => { + const ref = useRef(connect); + return ( + + {children} + + ); +}; + +export default function CompassConnections( + props: Omit, 'connectFn'> +) { + const connectFn = useContext(ConnectFnContext); + return ( + + ); +} diff --git a/packages/compass-connections/src/provider.ts b/packages/compass-connections/src/provider.ts index 1dcbf347b62..e0f1c64ed81 100644 --- a/packages/compass-connections/src/provider.ts +++ b/packages/compass-connections/src/provider.ts @@ -1,48 +1,32 @@ -import { createContext, useContext } from 'react'; import { createServiceLocator } from 'hadron-app-registry'; import { useConnectionInfo } from './connection-info-provider'; -import { EventEmitter } from 'events'; import type { DataService } from 'mongodb-data-service'; -import { ConnectionsManager } from './connections-manager'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; +import { + useConnectionActions, + useConnectionForId, + useConnectionIds, + useConnections, +} from './stores/store-context'; +import { useConnections as useConnectionsStore } from './stores/connections-store'; export type { DataService }; -export * from './connections-manager'; -export { useConnections } from './components/connections-provider'; export { useConnectionsWithStatus } from './hooks/use-connections-with-status'; export { useActiveConnections } from './hooks/use-active-connections'; -class TestConnectionsManager extends EventEmitter { - getDataServiceForConnection() { - return new EventEmitter() as unknown as DataService; - } -} - -const ConnectionsManagerContext = createContext( - process.env.NODE_ENV === 'test' - ? (new TestConnectionsManager() as unknown as ConnectionsManager) - : null -); -export const ConnectionsManagerProvider = ConnectionsManagerContext.Provider; - -export const useConnectionsManagerContext = (): ConnectionsManager => { - const connectionsManager = useContext(ConnectionsManagerContext); - - if (!connectionsManager) { - if (process.env.NODE_ENV !== 'test') { - throw new Error( - 'ConnectionsManager not available in context. Did you forget to setup ConnectionsManagerProvider' - ); - } - return new ConnectionsManager({ - logger: createNoopLogger().log.unbound, - }); - } - return connectionsManager; -}; +export type ConnectionsManager = Pick< + ReturnType, + | 'getDataServiceForConnection' + | 'getConnectionById' + | 'on' + | 'off' + | 'removeListener' +>; +/** + * @deprecated use `connectionsLocator` instead + */ export const connectionsManagerLocator = createServiceLocator( - useConnectionsManagerContext, + useConnections, 'connectionsManagerLocator' ); @@ -62,38 +46,107 @@ export const dataServiceLocator = createServiceLocator( L extends keyof DataService = K >(): Pick & Partial> { const connectionInfo = useConnectionInfo(); + const connectionsManager = connectionsManagerLocator(); if (!connectionInfo) { throw new Error( 'ConnectionInfo for an active connection not available in context. Did you forget to setup ConnectionInfoProvider' ); } - const connectionsManager = useConnectionsManagerContext(); - const ds = connectionsManager.getDataServiceForConnection( - connectionInfo.id - ); - return ds; + return connectionsManager.getDataServiceForConnection(connectionInfo.id); } ); -export { useConnectionStatus } from './hooks/use-connection-status'; export { connectionScopedAppRegistryLocator, ConnectionScopedAppRegistryImpl, type ConnectionScopedAppRegistry, type ConnectionScopedAppRegistryLocator, } from './connection-scoped-app-registry'; + +export type { + ConnectionInfoAccess, + ConnectionInfo, +} from './connection-info-provider'; + export { - type CanNotOpenConnectionReason, - useCanOpenNewConnections, -} from './hooks/use-can-open-new-connections'; + ConnectionInfoProvider, + useConnectionInfo, + useConnectionInfoAccess, + withConnectionInfoAccess, + connectionInfoAccessLocator, + TEST_CONNECTION_INFO, +} from './connection-info-provider'; + +export { useTabConnectionTheme } from './hooks/use-tab-connection-theme'; + +export type { + ConnectionRepository, + ConnectionRepositoryAccess, +} from './hooks/use-connection-repository'; + export { - type ConnectionRepository, - useConnectionRepository, withConnectionRepository, - areConnectionsEqual, + useConnectionRepository, connectionRepositoryAccessLocator, - type ConnectionRepositoryAccess, -} from './components/connections-provider'; -export * from './connection-info-provider'; +} from './hooks/use-connection-repository'; -export { useTabConnectionTheme } from './hooks/use-tab-connection-theme'; +export { + useConnectionActions, + useConnectionForId, + useConnectionIds, + useConnectionInfoForId, + useConnectionInfoRefForId, + connectionsLocator, +} from './stores/store-context'; + +export { useConnectionsStore as useConnections }; + +const ConnectionStatus = { + /** + * @deprecated use a string literal directly + */ + Connected: 'connected', + /** + * @deprecated use a string literal directly + */ + Disconnected: 'disconnected', + /** + * @deprecated use a string literal directly + */ + Failed: 'failed', +} as const; + +export { ConnectionStatus }; + +/** + * @deprecated compatibility for single connection mode: in single connection + * mode the first "connected" connection is the current application connection + */ +export function useSingleConnectionModeConnectionInfoStatus() { + const [connectionId = '-1'] = useConnectionIds((connection) => { + return ( + connection.status === 'connected' || + connection.status === 'connecting' || + connection.status === 'failed' + ); + }); + const connectionState = useConnectionForId(connectionId); + const { disconnect } = useConnectionActions(); + return connectionState && connectionState.status === 'connected' + ? { + isConnected: true as const, + connectionInfo: connectionState.info, + connectionError: null, + disconnect: () => { + disconnect(connectionState.info.id); + return undefined; + }, + } + : { + isConnected: false as const, + connectionInfo: connectionState?.info ?? null, + connectionError: + connectionState?.status === 'failed' ? connectionState.error : null, + disconnect: () => undefined, + }; +} diff --git a/packages/compass-connections/src/stores/connections-store-redux.ts b/packages/compass-connections/src/stores/connections-store-redux.ts new file mode 100644 index 00000000000..0b9d928c152 --- /dev/null +++ b/packages/compass-connections/src/stores/connections-store-redux.ts @@ -0,0 +1,2105 @@ +import type { Reducer, AnyAction, Action } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; +import type { ThunkAction } from 'redux-thunk'; +import thunk from 'redux-thunk'; +import { + getConnectionTitle, + type ConnectionInfo, +} from '@mongodb-js/connection-info'; +import type { ConnectionStorage } from '@mongodb-js/connection-storage/provider'; +import type { TrackFunction } from '@mongodb-js/compass-telemetry/provider'; +import type { Logger } from '@mongodb-js/compass-logging/provider'; +import type { + connect as devtoolsConnect, + ConnectionAttempt, + ConnectionOptions, + DataService, +} from 'mongodb-data-service'; +import { createConnectionAttempt } from 'mongodb-data-service'; +import { UUID } from 'bson'; +import { assign, cloneDeep, isEqual, merge } from 'lodash'; +import type { PreferencesAccess } from 'compass-preferences-model/provider'; +import { getNotificationTriggers } from '../components/connection-status-notifications'; +import { openToast, showConfirmation } from '@mongodb-js/compass-components'; +import { adjustConnectionOptionsBeforeConnect } from '@mongodb-js/connection-form'; +import mongodbBuildInfo, { getGenuineMongoDB } from 'mongodb-build-info'; +import EventEmitter from 'events'; +import { showNonGenuineMongoDBWarningModal as _showNonGenuineMongoDBWarningModal } from '../components/non-genuine-connection-modal'; +import ConnectionString from 'mongodb-connection-string-url'; + +export type ConnectionsEventMap = { + connected: ( + connectionId: ConnectionId, + connectionInfo: ConnectionInfo + ) => void; + disconnected: ( + connectionId: ConnectionId, + connectionInfo: ConnectionInfo + ) => void; +}; + +export interface ConnectionsEventEmitter { + emit( + this: void, + event: K, + ...args: Parameters + ): boolean; + on( + this: void, + event: K, + listener: ConnectionsEventMap[K] + ): ConnectionsEventEmitter; + off( + this: void, + event: K, + listener: ConnectionsEventMap[K] + ): ConnectionsEventEmitter; + removeListener( + this: void, + event: K, + listener: ConnectionsEventMap[K] + ): ConnectionsEventEmitter; + once( + this: void, + event: K, + listener: ConnectionsEventMap[K] + ): ConnectionsEventEmitter; +} + +const emitter = new EventEmitter(); + +export const connectionsEventEmitter: ConnectionsEventEmitter = { + emit: (event, ...args) => { + try { + return emitter.emit(event, ...args); + } catch { + return false; + } + }, + on: (event, listener) => { + emitter.on(event, listener); + return connectionsEventEmitter; + }, + off: (event, listener) => { + emitter.on(event, listener); + return connectionsEventEmitter; + }, + removeListener: (event, listener) => { + emitter.on(event, listener); + return connectionsEventEmitter; + }, + once: (event, listener) => { + emitter.on(event, listener); + return connectionsEventEmitter; + }, +}; + +export type ConnectionState = { + info: ConnectionInfo; + /** + * This flag will be true when connection is just being created: during + * duplication or "new connection" creation. We keep this value in state so + * that the connection can be hidden from the sidebar until you start + * connecting or save it + */ + isBeingCreated?: boolean; + /** + * Flag set on the info that was resolved when compass is autoconnecting, + * can't be edited or saved + */ + isAutoconnectInfo?: boolean; +} & ( + | { + status: + | 'initial' + | 'connecting' + | 'connected' + | 'disconnected' + | 'canceled'; + error: null; + } + | { status: 'failed'; error: Error } +); + +export type ConnectionId = ConnectionInfo['id']; + +/** + * Connections list stored following Redux data normalization guidelines + * @see {@link https://redux.js.org/usage/structuring-reducers/normalizing-state-shape#designing-a-normalized-state} + */ +type NormalizedConnectionsList = { + ids: ConnectionId[]; + byId: Record; +}; + +export type State = { + // State of all connections currently known to the application state. + // Populated from the connection storage initially, but also keeps reference + // to all non-stored connections in the app, like autoconnect info or + // connections being edited before they are saved + connections: NormalizedConnectionsList & + ( + | { + status: 'initial' | 'ready'; + error: null; + } + | { status: 'loading' | 'refreshing'; error: Error | null } + | { status: 'error'; error: Error } + ); + + // Device auth flow info, stored in state so that it can be appended to the + // "Connecting..." modal. Only required for single connection mode and can be + // cleaned-up when multiple connections is the only mode of the app + oidcDeviceAuthInfo: Record< + ConnectionId, + { + verificationUrl: string; + userCode: string; + } + >; + + // State related to connection editing form. Can't be null in single + // connection mode, so we always have a default connection set here even if + // nothing is being actively edited. Can be updated when single-connection + // mode is removed from the app + editingConnectionInfoId: ConnectionId; + isEditingConnectionInfoModalOpen: boolean; + + // State related to connection favorite fields editing modal form (right now + // only relevant for single connection mode, this might change) + editingConnectionFavoriteInfoId: ConnectionId | null; + isEditingConnectionFavoriteInfoModalOpen: boolean; +}; + +type ThunkExtraArg = { + appName: string; + preferences: PreferencesAccess; + connectionStorage: ConnectionStorage; + track: TrackFunction; + logger: Logger; + getExtraConnectionData: ( + connectionInfo: ConnectionInfo + ) => Promise<[Record, string | null]>; + connectFn?: typeof devtoolsConnect; +}; + +export type ConnectionsThunkAction< + R, + A extends AnyAction = AnyAction +> = ThunkAction; + +export const enum ActionTypes { + // Actions related to getting connections from the persistent store (like disk + // or cloud backend) + ConnectionsLoadStart = 'ConnectionsLoadStart', + ConnectionsLoadSuccess = 'ConnectionsLoadSuccess', + ConnectionsLoadError = 'ConnectionsLoadError', + + ConnectionsRefreshStart = 'ConnectionsRefreshStart', + ConnectionsRefreshSuccess = 'ConnectionsRefreshSuccess', + ConnectionsRefreshError = 'ConnectionsRefreshError', + + // Desktop-only connections import feature + ConnectionsImportParsingStart = 'ConnectionsImportParsingStart', + ConnectionsImportParsingFinish = 'ConnectionsImportParsingFinish', + ConnectionsImportStart = 'ConnectionsImportStart', + ConnectionsImportFinish = 'ConnectionsImportFinish', + + // Desktop-only connections export feature + ConnectionsExportStart = 'ConnectionsExportStart', + ConnectionsExportFinish = 'ConnectionsExportFinish', + + // Checking for the need to autoconnect on application start + ConnectionAutoconnectCheck = 'ConnectionAutoconnectCheck', + + // Connection attempt related actions. Connection attempt can be triggered by + // user actions or autoconnect + ConnectionAttemptStart = 'ConnectionAttemptStart', + ConnectionAttemptSuccess = 'ConnectionAttemptSuccess', + ConnectionAttemptError = 'ConnectionAttemptError', + ConnectionAttemptCancelled = 'ConnectionAttemptCancelled', + // During the connection attempt process if OIDC auth flow requires manually + // entering a device code on the external website. Only required for single + // connection mode and can be removed afterwards + OidcNotifyDeviceAuth = 'OidcNotifyDeviceAuth', + Disconnect = 'Disconnect', + + // Actions related to modifying connection info + + // Anything that is triggered by the user from the UI through action buttons + // and connection editing form + CreateNewConnection = 'CreateNewConnection', + DuplicateConnection = 'DuplicateConnection', + EditConnection = 'EditConnection', + // Opens a special favorite editing modal form. Only applicable for single + // connection mode where the special form is accessible + EditConnectionFavoriteInfo = 'EditConnectionFavoriteInfo', + ToggleFavoriteConnection = 'ToggleFavoriteConnection', + CancelEditConnection = 'CancelEditConnection', + SaveEditedConnection = 'SaveEditedConnection', + // When connection info is actually updated in storage. Can be a result of + // events above, or implicitly triggered by the application flow: for example + // when secrets are changed and we want to store the updated ones + SaveConnectionInfo = 'SaveConnectionInfo', + RemoveConnection = 'RemoveConnection', + RemoveAllRecentConnections = 'RemoveAllRecentConnections', +} + +type ConnectionsLoadStartAction = { + type: ActionTypes.ConnectionsLoadStart; +}; + +type ConnectionsLoadSuccessAction = { + type: ActionTypes.ConnectionsLoadSuccess; + connections: ConnectionInfo[]; +}; + +type ConnectionsLoadErrorAction = { + type: ActionTypes.ConnectionsLoadError; + error: Error; +}; + +type ConnectionsRefreshStartAction = { + type: ActionTypes.ConnectionsRefreshStart; +}; + +type ConnectionsRefreshSuccessAction = { + type: ActionTypes.ConnectionsRefreshSuccess; + connections: ConnectionInfo[]; +}; + +type ConnectionsRefreshErrorAction = { + type: ActionTypes.ConnectionsRefreshError; + error: Error; +}; + +// TODO: move all import / export actions to connections store + +// type ConnectionsImportParsingStartAction = { +// type: ActionTypes.ConnectionsImportParsingStart; +// }; + +// type ConnectionsImportParsingFinishAction = { +// type: ActionTypes.ConnectionsImportParsingFinish; +// connections: ConnectionInfo[]; +// }; + +type ConnectionsImportStartAction = { + type: ActionTypes.ConnectionsImportStart; +}; + +type ConnectionsImportFinishAction = { + type: ActionTypes.ConnectionsImportFinish; + connections: ConnectionInfo[]; +}; + +// type ConnectionsExportStartAction = { +// type: ActionTypes.ConnectionsExportStart; +// }; + +// type ConnectionsExportFinishAction = { +// type: ActionTypes.ConnectionsExportFinish; +// }; + +type ConnectionAutoconnectCheckAction = { + type: ActionTypes.ConnectionAutoconnectCheck; + connectionInfo: ConnectionInfo | undefined; +}; + +type ConnectionAttemptStartAction = { + type: ActionTypes.ConnectionAttemptStart; + connectionInfo: ConnectionInfo; +}; + +type ConnectionAttemptSuccessAction = { + type: ActionTypes.ConnectionAttemptSuccess; + connectionId: ConnectionId; +}; + +type ConnectionAttemptErrorAction = { + type: ActionTypes.ConnectionAttemptError; + connectionId: ConnectionId | null; + error: Error; +}; + +type ConnectionAttemptCancelledAction = { + type: ActionTypes.ConnectionAttemptCancelled; + connectionId: ConnectionId; +}; + +type DisconnectAction = { + type: ActionTypes.Disconnect; + connectionId: ConnectionId; +}; + +type OidcNotifyDeviceAuthAction = { + type: ActionTypes.OidcNotifyDeviceAuth; + connectionId: ConnectionId; + verificationUrl: string; + userCode: string; +}; + +type CreateNewConnectionAction = { + type: ActionTypes.CreateNewConnection; +}; + +type DuplicateConnectionAction = { + type: ActionTypes.DuplicateConnection; + duplicateInfo: ConnectionInfo; + isAutoDuplicate: boolean; +}; + +type EditConnectionAction = { + type: ActionTypes.EditConnection; + connectionId: ConnectionId; +}; + +type ToggleFavoriteConnectionAction = { + type: ActionTypes.ToggleFavoriteConnection; + connectionId: ConnectionId; +}; + +type CancelEditConnectionAction = { + type: ActionTypes.CancelEditConnection; + connectionId: ConnectionId; +}; + +type SaveEditedConnectionAction = { + type: ActionTypes.SaveEditedConnection; + connectionId: ConnectionId; +}; + +type SaveConnectionInfoAction = { + type: ActionTypes.SaveConnectionInfo; + connectionInfo: ConnectionInfo; +}; + +type RemoveConnectionAction = { + type: ActionTypes.RemoveConnection; + connectionId: ConnectionId; +}; + +type RemoveAllRecentConnectionsActions = { + type: ActionTypes.RemoveAllRecentConnections; +}; + +type EditConnectionFavoriteInfoAction = { + type: ActionTypes.EditConnectionFavoriteInfo; + connectionId: ConnectionId; +}; + +function isAction( + action: AnyAction, + type: A['type'] +): action is A { + return action.type === type; +} + +const InFlightConnections = new Map>(); + +const ConnectionAttemptForConnection = new Map< + ConnectionId, + ConnectionAttempt +>(); + +const DataServiceForConnection = new Map(); + +export function getDataServiceForConnection(connectionId: ConnectionId) { + const ds = DataServiceForConnection.get(connectionId); + if (!ds) { + throw new Error( + `Failed to locate DataService instance for connection ${connectionId}` + ); + } + return ds; +} + +/** + * Connection secrets are stored in-memory even after disconnecting (even if + * storing secrets to persistent store is disabled). That was when re-connecting + * to connections with secrets inside the same session, we make it a bit easier + */ +const SecretsForConnection = new Map< + ConnectionId, + Partial +>(); + +export function createDefaultConnectionInfo() { + return { + id: new UUID().toString(), + connectionOptions: { + // NB: it is imperative that this value stays in sync with the one used in + // connection-form package, otherwise it breaks some form behavior + connectionString: 'mongodb://localhost:27017', + }, + }; +} + +export function createDefaultConnectionState( + connectionInfo: ConnectionInfo = createDefaultConnectionInfo() +): ConnectionState { + return { + info: connectionInfo, + status: 'initial', + error: null, + }; +} + +// For single connection mode we always have to start with the initial empty +// connection in the state already. This can be removed when single connection +// mode doesn't exist anymore +const INITIAL_CONNECTION_STATE = createDefaultConnectionState(); +INITIAL_CONNECTION_STATE.isBeingCreated = true; + +const INITIAL_STATE: State = { + connections: { + ids: [INITIAL_CONNECTION_STATE.info.id], + byId: { + [INITIAL_CONNECTION_STATE.info.id]: INITIAL_CONNECTION_STATE, + }, + status: 'initial', + error: null, + }, + oidcDeviceAuthInfo: {}, + editingConnectionInfoId: INITIAL_CONNECTION_STATE.info.id, + isEditingConnectionInfoModalOpen: false, + editingConnectionFavoriteInfoId: null, + isEditingConnectionFavoriteInfoModalOpen: false, +}; + +export function getInitialConnectionsStateForConnectionInfos( + connectionInfos: ConnectionInfo[] = [] +): State['connections'] { + const byId = Object.fromEntries( + [ + [INITIAL_CONNECTION_STATE.info.id, INITIAL_CONNECTION_STATE] as const, + ].concat( + connectionInfos.map((info) => { + return [info.id, createDefaultConnectionState(info)]; + }) + ) + ); + return { + byId, + ids: getSortedIdsForConnections(Object.values(byId)), + // Keep initial state if we're not preloading any connections + status: connectionInfos.length > 0 ? 'ready' : 'initial', + error: null, + }; +} + +function savedTypeCompare(a: ConnectionInfo, b: ConnectionInfo) { + const isFavA = a.savedConnectionType === 'favorite'; + const isFavB = b.savedConnectionType === 'favorite'; + return isFavA === isFavB ? 0 : isFavA && !isFavB ? -1 : 1; +} + +function getSortedIdsForConnections( + connections: ConnectionState[] +): ConnectionId[] { + return connections + .slice() + .sort((a, b) => { + const aTitle = getConnectionTitle(a.info).toLocaleLowerCase(); + const bTitle = getConnectionTitle(b.info).toLocaleLowerCase(); + return ( + // Favorites are always first + savedTypeCompare(a.info, b.info) || + // Then compare by title + aTitle.localeCompare(bTitle) || + // If titles are the same, compare the ids just to make sure that the + // sorting is stable no matter what's the input initial order was + a.info.id.localeCompare(b.info.id) + ); + }) + .map((connectionState) => { + return connectionState.info.id; + }); +} + +function mergeConnections( + connectionsState: State['connections'], + newConnections: ConnectionInfo | ConnectionInfo[] +): State['connections'] { + newConnections = Array.isArray(newConnections) + ? newConnections + : [newConnections]; + + let newConnectionsById = connectionsState.byId; + + for (const connectionInfo of newConnections) { + const existingConnection = newConnectionsById[connectionInfo.id]; + + // If we got a new connection, just create a default state for this + // connection and update new connections by id + if (!existingConnection) { + newConnectionsById = { + ...newConnectionsById, + [connectionInfo.id]: createDefaultConnectionState(connectionInfo), + }; + } + + // If connection already exists, only update the info if new connection info + // is different + if ( + existingConnection && + !isEqual(existingConnection.info, connectionInfo) + ) { + newConnectionsById = { + ...newConnectionsById, + [connectionInfo.id]: { + ...existingConnection, + info: connectionInfo, + }, + }; + } + } + + // If we haven't modified newConnectionsById at this point, we can stop: none + // of the new connections are different from what we have in the state already + if (newConnectionsById === connectionsState.byId) { + return connectionsState; + } + + const newIds = getSortedIdsForConnections(Object.values(newConnectionsById)); + + return { + ...connectionsState, + byId: newConnectionsById, + // In cases where some data irrelevant for the sorting information was + // changed, it is possible for ids to stay the same even if info was + // updated. If ids didn't change, return previous state + ids: isEqual(connectionsState.ids, newIds) ? connectionsState.ids : newIds, + }; +} + +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; + +function mergeConnectionStateById( + connections: State['connections'], + connectionId: ConnectionId, + connectionState: RecursivePartial, + { shallowMerge = false }: { shallowMerge?: boolean } = {} +): State['connections'] { + const existingConnectionState = connections.byId[connectionId]; + const mergeFn = shallowMerge ? assign : merge; + + const newConnectionState = mergeFn( + existingConnectionState + ? cloneDeep(existingConnectionState) + : createDefaultConnectionState(), + connectionState + ); + + if ( + existingConnectionState && + isEqual(existingConnectionState, newConnectionState) + ) { + return connections; + } + + const newConnectionsById = { + ...connections.byId, + [newConnectionState.info.id]: newConnectionState, + }; + + const newIds = getSortedIdsForConnections(Object.values(newConnectionsById)); + + return { + ...connections, + byId: newConnectionsById, + ids: isEqual(newIds, connections.ids) ? connections.ids : newIds, + }; +} + +function createConnectionInfoDuplicate( + connectionInfo: ConnectionInfo, + existingConnections: ConnectionInfo[] +): ConnectionInfo { + function parseFavoriteNameToNameAndCopyCount( + favoriteName: string + ): [string, number] { + const { groups = {} } = + favoriteName.match(/^(?.+?)(\s\((?\d+)\))?$/) ?? {}; + return [ + groups.name ?? favoriteName, + groups.count ? Number(groups.count) : 0, + ]; + } + + const duplicate: ConnectionInfo = { + ...cloneDeep(connectionInfo), + id: new UUID().toString(), + }; + + if (!duplicate.favorite || !duplicate.favorite.name) { + duplicate.favorite = { + ...duplicate.favorite, + name: getConnectionTitle(duplicate), + }; + } + + const [nameWithoutCount, copyCount] = parseFavoriteNameToNameAndCopyCount( + duplicate.favorite.name + ); + + const newCount = existingConnections.reduce((topCount, connectionInfo) => { + if (connectionInfo.favorite?.name) { + const [name, count] = parseFavoriteNameToNameAndCopyCount( + connectionInfo.favorite.name + ); + if (name === nameWithoutCount && count >= topCount) { + return count + 1; + } + return topCount; + } + return topCount; + }, copyCount + 1); + + duplicate.favorite.name = `${nameWithoutCount} (${newCount})`; + + delete duplicate.lastUsed; + + return duplicate; +} + +function hasConnectionForId(state: State, connectionId: ConnectionId): boolean { + return !!state.connections.byId[connectionId]; +} + +const reducer: Reducer = (state = INITIAL_STATE, action) => { + if ( + isAction( + action, + ActionTypes.ConnectionsLoadStart + ) + ) { + return { + ...state, + connections: { + ...state.connections, + status: 'loading', + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsLoadSuccess + ) + ) { + return { + ...state, + connections: { + ...mergeConnections(state.connections, action.connections), + status: 'ready', + error: null, + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsLoadError + ) + ) { + return { + ...state, + connections: { + ...state.connections, + status: 'error', + error: action.error, + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsRefreshStart + ) + ) { + return { + ...state, + connections: { + ...state.connections, + status: 'refreshing', + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsRefreshSuccess + ) + ) { + return { + ...state, + connections: { + ...mergeConnections(state.connections, action.connections), + status: 'ready', + error: null, + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsRefreshError + ) + ) { + return { + ...state, + connections: { + ...state.connections, + status: 'error', + error: action.error, + }, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionsImportFinish + ) + ) { + return { + ...state, + connections: mergeConnections(state.connections, action.connections), + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionAutoconnectCheck + ) + ) { + if (!action.connectionInfo) { + return state; + } + + const connectionState = createDefaultConnectionState(action.connectionInfo); + + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + connectionState.info.id, + { + ...connectionState, + isAutoconnectInfo: true, + } + ), + // Single connection mode special case: when autoconnecting set + // autoconnect info as editing so that the always-visible connection form + // is populated correctly and the error is mapped to it if it happend + editingConnectionInfoId: connectionState.info.id, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionAttemptStart + ) + ) { + // Clean-up existing device auth info before starting new connection so that + // we don't show anything until driver actually provides it. Can be removed + // when this state is not in the store anymore + const oidcDeviceAuthInfo = { ...state.oidcDeviceAuthInfo }; + delete oidcDeviceAuthInfo[action.connectionInfo.id]; + + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.connectionInfo.id, + { + // For new connections, update the state with new info right away (we + // will also save it to the storage at the end) + ...(isNewConnection(state, action.connectionInfo.id) && { + info: action.connectionInfo, + }), + status: 'connecting', + error: null, + } + ), + oidcDeviceAuthInfo, + isEditingConnectionInfoModalOpen: + // Close the modal when connection starts for edited connection + state.editingConnectionInfoId === action.connectionInfo.id + ? false + : state.isEditingConnectionInfoModalOpen, + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionAttemptSuccess + ) + ) { + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.connectionId, + { + isBeingCreated: false, + status: 'connected', + error: null, + } + ), + }; + } + if ( + isAction( + action, + ActionTypes.ConnectionAttemptError + ) + ) { + let connectionState = action.connectionId + ? state.connections.byId[action.connectionId] + : null; + + // Special autoconnect case for single connection: if autoconnection failed + // before we even managed to load connection info, we won't have a + // connection state to map the error to. It's not an issue for multiple + // connections because the connection form is not always on the screen, but + // in single connection mode we need some connection info to map the error + // to, so we create one and add it to the connection list + if (!connectionState) { + connectionState = createDefaultConnectionState(); + connectionState.isAutoconnectInfo = true; + } + + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + connectionState.info.id, + { + ...connectionState, + isBeingCreated: false, + status: 'failed', + error: action.error, + } + ), + editingConnectionInfoId: connectionState.info.id, + }; + } + if (isAction(action, ActionTypes.Disconnect)) { + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.connectionId, + { status: 'disconnected' } + ), + }; + } + if ( + isAction( + action, + ActionTypes.OidcNotifyDeviceAuth + ) + ) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + return { + ...state, + oidcDeviceAuthInfo: { + ...state.oidcDeviceAuthInfo, + [action.connectionId]: { + userCode: action.userCode, + verificationUrl: action.verificationUrl, + }, + }, + }; + } + if ( + isAction(action, ActionTypes.CreateNewConnection) + ) { + const newConnection = createDefaultConnectionState(); + newConnection.isBeingCreated = true; + + let newConnectionsState = state.connections; + + // Only relevant for single connections mode: if we're currently editing + // "new connection", clean it up from state before creating state for a new + // one + if ( + state.editingConnectionInfoId && + state.connections.byId[state.editingConnectionInfoId].isBeingCreated + ) { + newConnectionsState = { + ...newConnectionsState, + byId: { + ...newConnectionsState.byId, + }, + }; + delete newConnectionsState.byId[state.editingConnectionInfoId]; + } + + return { + ...state, + connections: mergeConnectionStateById( + newConnectionsState, + newConnection.info.id, + newConnection + ), + editingConnectionInfoId: newConnection.info.id, + isEditingConnectionInfoModalOpen: true, + }; + } + if ( + isAction(action, ActionTypes.DuplicateConnection) + ) { + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.duplicateInfo.id, + { + info: action.duplicateInfo, + isBeingCreated: !action.isAutoDuplicate, + } + ), + editingConnectionInfoId: action.duplicateInfo.id, + ...(!action.isAutoDuplicate && { + isEditingConnectionInfoModalOpen: true, + }), + }; + } + if (isAction(action, ActionTypes.EditConnection)) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + return { + ...state, + editingConnectionInfoId: action.connectionId, + isEditingConnectionInfoModalOpen: true, + }; + } + if ( + isAction( + action, + ActionTypes.ToggleFavoriteConnection + ) + ) { + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.connectionId, + { + info: { + savedConnectionType: + state.connections.byId[action.connectionId].info + .savedConnectionType === 'favorite' + ? 'recent' + : 'favorite', + }, + } + ), + }; + } + if ( + isAction( + action, + ActionTypes.CancelEditConnection + ) + ) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + let connections = state.connections; + let editingConnectionInfoId = state.editingConnectionInfoId; + + // In cases where connection was never saved or used before, we remove it + // from the connections state + if (state.connections.byId[action.connectionId].isBeingCreated) { + const newConnectionsById = { ...state.connections.byId }; + delete newConnectionsById[action.connectionId]; + + const newIds = connections.ids.filter((id) => { + return id !== action.connectionId; + }); + + connections = { + ...connections, + byId: newConnectionsById, + ids: newIds, + }; + + // Special case for single connection: after removing connection, we + // automatically create a new connection and will "select" it for editing. + // Can go away when single connection mode is removed + if (state.editingConnectionInfoId === action.connectionId) { + const newDefaultConnection = createDefaultConnectionState(); + newDefaultConnection.isBeingCreated = true; + connections = mergeConnectionStateById( + connections, + newDefaultConnection.info.id, + newDefaultConnection + ); + editingConnectionInfoId = newDefaultConnection.info.id; + } + } + + return { + ...state, + connections, + editingConnectionInfoId, + ...(state.editingConnectionInfoId === action.connectionId && { + isEditingConnectionInfoModalOpen: false, + }), + ...(state.editingConnectionFavoriteInfoId === action.connectionId && { + isEditingConnectionFavoriteInfoModalOpen: false, + }), + }; + } + if ( + isAction( + action, + ActionTypes.SaveEditedConnection + ) + ) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + return { + ...state, + ...(state.editingConnectionInfoId === action.connectionId && { + isEditingConnectionInfoModalOpen: false, + }), + ...(state.editingConnectionFavoriteInfoId === action.connectionId && { + isEditingConnectionFavoriteInfoModalOpen: false, + }), + }; + } + if ( + isAction(action, ActionTypes.SaveConnectionInfo) + ) { + if (!hasConnectionForId(state, action.connectionInfo.id)) { + return state; + } + + return { + ...state, + connections: mergeConnectionStateById( + state.connections, + action.connectionInfo.id, + { info: action.connectionInfo, isBeingCreated: false }, + // Completely replace the connection info with the new one on save + { shallowMerge: true } + ), + }; + } + if (isAction(action, ActionTypes.RemoveConnection)) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + // Shallow cloning properties that we are going to change + let newConnectionsState = { + ...state.connections, + byId: { + ...state.connections.byId, + }, + }; + delete newConnectionsState.byId[action.connectionId]; + + const isEditingRemovedConnection = + state.editingConnectionInfoId === action.connectionId; + + const newConnection = isEditingRemovedConnection + ? createDefaultConnectionState() + : undefined; + + // Special case for single connection: when deleting a connection that is + // currently selected in the sidebar, we automatically create a new + // connection and will "select" it for editing. Can go away when single + // connection mode is removed + if (newConnection) { + newConnection.isBeingCreated = true; + newConnectionsState = mergeConnectionStateById( + newConnectionsState, + newConnection.info.id, + newConnection + ); + } else { + // Only in the else case because `mergeConnectionStateById` will already + // filter it out + newConnectionsState.ids = newConnectionsState.ids.filter((id) => { + return id !== action.connectionId; + }); + } + + return { + ...state, + connections: newConnectionsState, + ...(isEditingRemovedConnection && { + editingConnectionInfoId: + newConnection?.info.id ?? state.editingConnectionInfoId, + isEditingConnectionInfoModalOpen: false, + }), + ...(state.editingConnectionFavoriteInfoId === action.connectionId && { + editingConnectionFavoriteInfoId: null, + isEditingConnectionFavoriteInfoModalOpen: false, + }), + }; + } + if ( + isAction( + action, + ActionTypes.RemoveAllRecentConnections + ) + ) { + const idsToRemove = Object.values(state.connections.byId) + .filter((connection) => { + return isRecentConnection(connection); + }) + .map((connection) => { + return connection.info.id; + }); + + if (idsToRemove.length === 0) { + return state; + } + + // Shallow cloning properties that we are going to change + let newConnectionsState = { + ...state.connections, + byId: { + ...state.connections.byId, + }, + }; + + for (const id of idsToRemove) { + delete newConnectionsState.byId[id]; + } + + const isEditingRemovedConnection = + !!state.editingConnectionInfoId && + idsToRemove.some((id) => { + return id === state.editingConnectionInfoId; + }); + + const isEditingFavoriteRemoveConnections = + !!state.editingConnectionFavoriteInfoId && + idsToRemove.some((id) => { + return id === state.editingConnectionFavoriteInfoId; + }); + + const newConnection = isEditingRemovedConnection + ? createDefaultConnectionState() + : undefined; + + // Special case for single connection: see RemoveConnectionAction reducer + if (newConnection) { + newConnection.isBeingCreated = true; + newConnectionsState = mergeConnectionStateById( + newConnectionsState, + newConnection.info.id, + newConnection + ); + } else { + newConnectionsState.ids = newConnectionsState.ids.filter((id) => { + return !idsToRemove.includes(id); + }); + } + + return { + ...state, + connections: newConnectionsState, + ...(isEditingRemovedConnection && { + editingConnectionInfoId: + newConnection?.info.id ?? state.editingConnectionInfoId, + isEditingConnectionInfoModalOpen: false, + }), + ...(isEditingFavoriteRemoveConnections && { + editingConnectionFavoriteInfoId: null, + isEditingConnectionFavoriteInfoModalOpen: false, + }), + }; + } + if ( + isAction( + action, + ActionTypes.EditConnectionFavoriteInfo + ) + ) { + if (!hasConnectionForId(state, action.connectionId)) { + return state; + } + + return { + ...state, + editingConnectionFavoriteInfoId: action.connectionId, + isEditingConnectionFavoriteInfoModalOpen: true, + }; + } + return state; +}; + +export const loadConnections = (): ConnectionsThunkAction< + Promise, + | ConnectionsLoadStartAction + | ConnectionsLoadSuccessAction + | ConnectionsLoadErrorAction +> => { + return async (dispatch, getState, { connectionStorage }) => { + if (getState().connections.status !== 'initial') { + return; + } + dispatch({ type: ActionTypes.ConnectionsLoadStart }); + try { + const connections = await connectionStorage.loadAll(); + dispatch({ type: ActionTypes.ConnectionsLoadSuccess, connections }); + } catch (err) { + dispatch({ type: ActionTypes.ConnectionsLoadError, error: err as any }); + } + }; +}; + +export const refreshConnections = (): ConnectionsThunkAction< + Promise, + | ConnectionsRefreshStartAction + | ConnectionsRefreshErrorAction + | ConnectionsRefreshSuccessAction +> => { + return async (dispatch, getState, { connectionStorage }) => { + if ( + getState().connections.status !== 'ready' && + getState().connections.status !== 'error' + ) { + return; + } + dispatch({ type: ActionTypes.ConnectionsRefreshStart }); + try { + const connections = await connectionStorage.loadAll(); + dispatch({ type: ActionTypes.ConnectionsRefreshSuccess, connections }); + } catch (err) { + dispatch({ + type: ActionTypes.ConnectionsRefreshError, + error: err as any, + }); + } + }; +}; + +const connectionAttemptError = ( + connectionInfo: ConnectionInfo | null, + err: any +): ConnectionsThunkAction => { + return ( + dispatch, + _getState, + { preferences, track, getExtraConnectionData } + ) => { + const { openConnectionFailedToast } = getNotificationTriggers( + preferences.getPreferences().enableMultipleConnectionSystem + ); + + openConnectionFailedToast(connectionInfo, err, () => { + if (connectionInfo) { + dispatch(editConnection(connectionInfo.id)); + } + }); + + track( + 'Connection Failed', + async () => { + const trackParams = { + error_code: err.code, + error_name: err.codeName ?? err.name, + }; + if (connectionInfo) { + const [extraInfo] = await getExtraConnectionData(connectionInfo); + Object.assign(trackParams, extraInfo); + } + return trackParams; + }, + connectionInfo ?? undefined + ); + + dispatch({ + type: ActionTypes.ConnectionAttemptError, + connectionId: connectionInfo?.id ?? null, + error: err, + }); + }; +}; + +export const autoconnectCheck = ( + getAutoconnectInfo: () => Promise +): ConnectionsThunkAction< + Promise, + ConnectionAutoconnectCheckAction | ConnectionAttemptErrorAction +> => { + return async (dispatch, _getState, { logger: { log, mongoLogId } }) => { + try { + log.info( + mongoLogId(1_001_000_160), + 'Connection Store', + 'Performing automatic connection attempt' + ); + const connectionInfo = await getAutoconnectInfo(); + dispatch({ + type: ActionTypes.ConnectionAutoconnectCheck, + connectionInfo: connectionInfo, + }); + if (connectionInfo) { + void dispatch(connect(connectionInfo)); + } + } catch (err) { + dispatch(connectionAttemptError(null, err)); + } + }; +}; + +function isAutoconnectInfo(state: State, connectionId: ConnectionId) { + return ( + state.connections.byId[connectionId] && + !!state.connections.byId[connectionId].isAutoconnectInfo + ); +} + +/** + * New connection is connection that is in the process of being created and was + * never saved or connected to before. Indicated by `isBeingCreated` state of + * the connection or just completely missing from the current state + */ +function isNewConnection(state: State, connectionId: ConnectionId) { + return ( + !state.connections.byId[connectionId] || + !!state.connections.byId[connectionId].isBeingCreated + ); +} + +function getCurrentConnectionInfo( + state: State, + connectionId: ConnectionId +): ConnectionInfo | undefined { + return state.connections.byId[connectionId]?.info; +} + +/** + * Returns the number of active connections. We count in-progress connections + * as "active" to make sure that the maximum connection allowed check takes + * those into account and doesn't allow to open more connections than allowed + * by starting too many connections in parallel + */ +function getActiveConnectionsCount(connections: State['connections']) { + return Object.values(connections.byId).filter((connectionState) => { + return ['connected', 'connecting'].includes(connectionState.status); + }).length; +} + +async function showOIDCReauthModal(connectionInfo: ConnectionInfo) { + const confirmed = await showConfirmation({ + title: `Authentication expired for ${getConnectionTitle(connectionInfo)}`, + description: + 'You need to re-authenticate to the database in order to continue.', + }); + if (!confirmed) { + throw new Error('Reauthentication declined by user'); + } +} + +function isAtlasStreamsInstance( + adjustedConnectionInfoForConnection: ConnectionInfo +) { + try { + return mongodbBuildInfo.isAtlasStream( + adjustedConnectionInfoForConnection.connectionOptions.connectionString + ); + } catch { + // This catch-all is not ideal, but it safe-guards regular connections + // instead of making assumptions on the fact that the implementation + // of `mongodbBuildInfo.isAtlasStream` would never throw. + return false; + } +} + +export const connect = ( + connectionInfo: ConnectionInfo +): ConnectionsThunkAction< + Promise, + | ConnectionAttemptStartAction + | ConnectionAttemptErrorAction + | ConnectionAttemptSuccessAction + | ConnectionAttemptCancelledAction + | OidcNotifyDeviceAuthAction +> => { + return async ( + dispatch, + getState, + { + preferences, + logger: { log, debug, mongoLogId }, + track, + appName, + getExtraConnectionData, + connectFn, + } + ) => { + let inflightConnection = InFlightConnections.get(connectionInfo.id); + if (inflightConnection) { + return inflightConnection; + } + inflightConnection = (async () => { + const isAutoconnectAttempt = isAutoconnectInfo( + getState(), + connectionInfo.id + ); + + const deviceAuthAbortController = new AbortController(); + + connectionInfo = cloneDeep(connectionInfo); + + const { + forceConnectionOptions, + browserCommandForOIDCAuth, + maximumNumberOfActiveConnections, + enableMultipleConnectionSystem, + } = preferences.getPreferences(); + + const connectionProgress = getNotificationTriggers( + enableMultipleConnectionSystem + ); + + if ( + typeof maximumNumberOfActiveConnections !== 'undefined' && + getActiveConnectionsCount(getState().connections) >= + maximumNumberOfActiveConnections + ) { + connectionProgress.openMaximumConnectionsReachedToast( + maximumNumberOfActiveConnections + ); + return; + } + + dispatch({ + type: ActionTypes.ConnectionAttemptStart, + connectionInfo, + }); + + track( + 'Connection Attempt', + { + is_favorite: connectionInfo.savedConnectionType === 'favorite', + is_recent: + !!connectionInfo.lastUsed && + connectionInfo.savedConnectionType !== 'favorite', + is_new: isNewConnection(getState(), connectionInfo.id), + }, + connectionInfo + ); + + debug('connecting with connectionInfo', connectionInfo); + + log.info( + mongoLogId(1_001_000_004), + 'Connection UI', + 'Initiating connection attempt', + { isAutoconnectAttempt } + ); + + try { + // Connection form allows to start connecting with invalid connection + // strings, so throw fast if it's not valid before doing anything else + ensureWellFormedConnectionString( + connectionInfo.connectionOptions.connectionString + ); + + connectionProgress.openConnectionStartedToast(connectionInfo, () => { + dispatch(disconnect(connectionInfo.id)); + }); + + const adjustedConnectionInfoForConnection: ConnectionInfo = merge( + cloneDeep(connectionInfo), + { + connectionOptions: adjustConnectionOptionsBeforeConnect({ + connectionOptions: merge( + cloneDeep(connectionInfo.connectionOptions), + SecretsForConnection.get(connectionInfo.id) ?? {} + ), + defaultAppName: appName, + preferences: { + forceConnectionOptions: forceConnectionOptions ?? [], + browserCommandForOIDCAuth, + }, + notifyDeviceFlow: (deviceFlowInfo) => { + dispatch({ + type: ActionTypes.OidcNotifyDeviceAuth, + connectionId: connectionInfo.id, + ...deviceFlowInfo, + }); + + connectionProgress.openNotifyDeviceAuthModal( + connectionInfo, + deviceFlowInfo.verificationUrl, + deviceFlowInfo.userCode, + () => { + void dispatch(disconnect(connectionInfo.id)); + }, + deviceAuthAbortController.signal + ); + }, + }), + } + ); + + // Temporarily disable Atlas Streams connections until https://jira.mongodb.org/browse/STREAMS-862 + // is done. + if (isAtlasStreamsInstance(adjustedConnectionInfoForConnection)) { + throw new Error( + 'Atlas Stream Processing is not yet supported on MongoDB Compass. To work with your Stream Processing Instance, connect with mongosh or MongoDB for VS Code.' + ); + } + + const connectionAttempt = createConnectionAttempt({ + logger: log.unbound, + connectFn, + }); + + ConnectionAttemptForConnection.set( + connectionInfo.id, + connectionAttempt + ); + + const dataService = await connectionAttempt.connect( + adjustedConnectionInfoForConnection.connectionOptions + ); + + // This is how connection attempt indicates that the connection was + // aborted + if (!dataService || connectionAttempt.isClosed()) { + dispatch({ + type: ActionTypes.ConnectionAttemptCancelled, + connectionId: connectionInfo.id, + }); + return; + } + + dataService.on('oidcAuthFailed', (error) => { + openToast('oidc-auth-failed', { + title: `Failed to authenticate for ${getConnectionTitle( + connectionInfo + )}`, + description: error, + variant: 'important', + }); + }); + + dataService.on('connectionInfoSecretsChanged', () => { + void dataService.getUpdatedSecrets().then( + (secrets) => { + SecretsForConnection.set(connectionInfo.id, secrets); + if (!preferences.getPreferences().persistOIDCTokens) { + return; + } + const info = getCurrentConnectionInfo( + getState(), + connectionInfo.id + ); + if (!info) { + return; + } + void dispatch( + saveConnectionInfo( + merge(cloneDeep(info), { connectionOptions: secrets }) + ) + ); + }, + () => { + // Do nothing if getting secrets failed + } + ); + }); + + dataService.addReauthenticationHandler(() => { + return showOIDCReauthModal(connectionInfo); + }); + + DataServiceForConnection.set(connectionInfo.id, dataService); + + try { + await dispatch( + saveConnectionInfo( + merge( + cloneDeep( + // See `ConnectionAttemptStartAction` handler in the reducer: + // in case of existing connection from storage, we keep the + // stored version in the state, in case of new connection, + // this is the whole info as was passed to the connect method + getCurrentConnectionInfo(getState(), connectionInfo.id) + ), + { + // Update lastUsed and secrets if connection was successful + lastUsed: new Date(), + ...(preferences.getPreferences().persistOIDCTokens + ? { + connectionOptions: + await dataService.getUpdatedSecrets(), + } + : {}), + } + ) + ) + ); + } catch (err) { + debug( + 'failed to update connection info after successful connect', + err + ); + } + + track( + 'New Connection', + async () => { + const [ + { dataLake, genuineMongoDB, host, build, isAtlas, isLocalAtlas }, + [extraInfo, resolvedHostname], + ] = await Promise.all([ + dataService.instance(), + getExtraConnectionData(connectionInfo), + ]); + + return { + is_atlas: isAtlas, + atlas_hostname: isAtlas ? resolvedHostname : null, + is_local_atlas: isLocalAtlas, + is_dataLake: dataLake.isDataLake, + is_enterprise: build.isEnterprise, + is_genuine: genuineMongoDB.isGenuine, + non_genuine_server_name: genuineMongoDB.dbType, + server_version: build.version, + server_arch: host.arch, + server_os_family: host.os_family, + topology_type: dataService.getCurrentTopologyType(), + ...extraInfo, + }; + }, + connectionInfo + ); + + debug( + 'connection attempt succeeded with connection info', + connectionInfo + ); + + connectionProgress.openConnectionSucceededToast(connectionInfo); + + // Emit before changing state because some plugins rely on this + connectionsEventEmitter.emit( + 'connected', + connectionInfo.id, + connectionInfo + ); + + dispatch({ + type: ActionTypes.ConnectionAttemptSuccess, + connectionId: connectionInfo.id, + }); + + if ( + getGenuineMongoDB(connectionInfo.connectionOptions.connectionString) + .isGenuine === false + ) { + dispatch(showNonGenuineMongoDBWarningModal(connectionInfo.id)); + } + } catch (err) { + dispatch(connectionAttemptError(connectionInfo, err)); + } finally { + deviceAuthAbortController.abort(); + ConnectionAttemptForConnection.delete(connectionInfo.id); + InFlightConnections.delete(connectionInfo.id); + } + })(); + InFlightConnections.set(connectionInfo.id, inflightConnection); + return inflightConnection; + }; +}; + +function ensureWellFormedConnectionString(connectionString: string) { + new ConnectionString(connectionString); +} + +const saveConnectionInfo = ( + connectionInfo: ConnectionInfo +): ConnectionsThunkAction< + Promise, + SaveConnectionInfoAction +> => { + return async ( + dispatch, + getState, + { connectionStorage, track, logger: { debug } } + ) => { + // Never save autoconnection info + if (isAutoconnectInfo(getState(), connectionInfo.id)) { + return null; + } + + try { + // Only allow saving if connection string is valid + ensureWellFormedConnectionString( + connectionInfo.connectionOptions.connectionString + ); + + const savedConnectionInfo = + (await connectionStorage.save?.({ connectionInfo })) ?? connectionInfo; + + if (isNewConnection(getState(), connectionInfo.id)) { + track( + 'Connection Created', + { color: savedConnectionInfo.favorite?.color }, + savedConnectionInfo + ); + } + dispatch({ + type: ActionTypes.SaveConnectionInfo, + connectionInfo: savedConnectionInfo, + }); + return savedConnectionInfo; + } catch (err) { + debug(`error saving connection with id ${connectionInfo.id}`, err); + openToast(`save-connection-error-${connectionInfo.id}`, { + title: 'An error occurred while saving the connection', + description: (err as Error).message, + variant: 'warning', + }); + return null; + } + }; +}; + +export const saveEditedConnectionInfo = ( + connectionInfo: ConnectionInfo +): ConnectionsThunkAction, SaveEditedConnectionAction> => { + return async (dispatch) => { + await dispatch(saveConnectionInfo(connectionInfo)); + dispatch({ + type: ActionTypes.SaveEditedConnection, + connectionId: connectionInfo.id, + }); + }; +}; + +export const createNewConnection = (): ConnectionsThunkAction< + void, + CreateNewConnectionAction +> => { + return (dispatch, getState, { preferences }) => { + // In multiple connections mode we don't allow another edit to start while + // there is one in progress + if ( + preferences.getPreferences().enableMultipleConnectionSystem && + getState().isEditingConnectionInfoModalOpen + ) { + return; + } + dispatch({ type: ActionTypes.CreateNewConnection }); + }; +}; + +export const editConnection = ( + connectionId: ConnectionId +): ConnectionsThunkAction => { + return (dispatch, getState, { preferences }) => { + // In multiple connections mode we don't allow another edit to start while + // there is one in progress + if ( + preferences.getPreferences().enableMultipleConnectionSystem && + getState().isEditingConnectionInfoModalOpen + ) { + return; + } + dispatch({ type: ActionTypes.EditConnection, connectionId }); + }; +}; + +export const duplicateConnection = ( + connectionId: ConnectionId, + { autoDuplicate }: { autoDuplicate: boolean } = { autoDuplicate: false } +): ConnectionsThunkAction => { + return (dispatch, getState, { preferences }) => { + // In multiple connections mode we don't allow another edit to start while + // there is one in progress + if ( + preferences.getPreferences().enableMultipleConnectionSystem && + getState().isEditingConnectionInfoModalOpen + ) { + return; + } + + const currentConnectionInfo = getCurrentConnectionInfo( + getState(), + connectionId + ); + + if (!currentConnectionInfo) { + return; + } + + const duplicateInfo = createConnectionInfoDuplicate( + currentConnectionInfo, + Object.values(getState().connections.byId).map((connectionState) => { + return connectionState.info; + }) + ); + + dispatch({ + type: ActionTypes.DuplicateConnection, + duplicateInfo, + isAutoDuplicate: autoDuplicate, + }); + + if (autoDuplicate) { + void dispatch(saveConnectionInfo(duplicateInfo)); + } + }; +}; + +export const cancelEditConnection = ( + connectionId: ConnectionId +): CancelEditConnectionAction => { + return { type: ActionTypes.CancelEditConnection, connectionId }; +}; + +const cleanupConnection = ( + connectionId: ConnectionId +): ConnectionsThunkAction => { + return ( + _dispatch, + getState, + { preferences, logger: { log, debug, mongoLogId }, track } + ) => { + log.info( + mongoLogId(1_001_000_313), + 'Connection UI', + 'Initiating disconnect attempt' + ); + + // We specifically want to track Disconnected even when it's not really + // triggered by user at all, so we put it in the cleanup function that is + // called every time you disconnect, or remove a connection, or all of them, + // or close the app + track( + 'Connection Disconnected', + {}, + getCurrentConnectionInfo(getState(), connectionId) + ); + + const { closeConnectionStatusToast } = getNotificationTriggers( + preferences.getPreferences().enableMultipleConnectionSystem + ); + + const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); + + closeConnectionStatusToast(connectionId); + + const connectionAttempt = ConnectionAttemptForConnection.get(connectionId); + const dataService = DataServiceForConnection.get(connectionId); + + void Promise.all([ + connectionAttempt?.cancelConnectionAttempt(), + dataService?.disconnect(), + ]).then( + () => { + debug('connection closed', connectionId); + }, + (err) => { + log.error( + mongoLogId(1_001_000_314), + 'Connection UI', + 'Disconnect attempt failed', + { error: (err as Error).message } + ); + } + ); + + ConnectionAttemptForConnection.delete(connectionId); + DataServiceForConnection.delete(connectionId); + + connectionsEventEmitter.emit('disconnected', connectionId, connectionInfo!); + }; +}; + +export const disconnect = ( + connectionId: ConnectionId +): ConnectionsThunkAction => { + return (dispatch, getState, { logger: { debug } }) => { + debug('closing connection with connectionId', connectionId); + + dispatch(cleanupConnection(connectionId)); + + dispatch({ type: ActionTypes.Disconnect, connectionId }); + }; +}; + +export const removeConnection = ( + connectionId: ConnectionId +): ConnectionsThunkAction => { + return ( + dispatch, + getState, + { connectionStorage, track, logger: { debug } } + ) => { + const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); + + if (!connectionInfo) { + return; + } + + dispatch(cleanupConnection(connectionInfo.id)); + + void connectionStorage.delete?.({ id: connectionId }).catch((err) => { + debug('failed to delete connection', err); + }); + + dispatch({ + type: ActionTypes.RemoveConnection, + connectionId, + }); + + track('Connection Removed', {}, connectionInfo); + }; +}; + +export const toggleConnectionFavoritedStatus = ( + connectionId: ConnectionId +): ConnectionsThunkAction => { + return (dispatch, getState) => { + if (isAutoconnectInfo(getState(), connectionId)) { + return; + } + + if (!hasConnectionForId(getState(), connectionId)) { + return; + } + + dispatch({ type: ActionTypes.ToggleFavoriteConnection, connectionId }); + + // After ToggleFavoriteConnection was dispatched, connectionInfo in state + // was already updated to toggle the value, we can use it now to save the + // connection in storage + const newConnectionInfo = getCurrentConnectionInfo( + getState(), + connectionId + ); + + // Making TS happy, should never end up here + if (!newConnectionInfo) { + throw new Error('No connection info to save'); + } + + void dispatch(saveConnectionInfo(newConnectionInfo)); + }; +}; + +function isRecentConnection(connection: ConnectionState) { + return ( + !connection.info.savedConnectionType || + connection.info.savedConnectionType === 'recent' + ); +} + +export const removeAllRecentConnections = (): ConnectionsThunkAction< + void, + RemoveAllRecentConnectionsActions +> => { + return (dispatch, getState, { connectionStorage, track }) => { + const toRemove = Object.values(getState().connections.byId).filter( + (connection) => { + return isRecentConnection(connection); + } + ); + + void Promise.allSettled( + toRemove.map((connection) => { + dispatch(cleanupConnection(connection.info.id)); + track('Connection Removed', {}, connection.info); + return connectionStorage.delete?.({ id: connection.info.id }); + }) + ); + + dispatch({ type: ActionTypes.RemoveAllRecentConnections }); + }; +}; + +export const showNonGenuineMongoDBWarningModal = ( + connectionId: string +): ConnectionsThunkAction => { + return (_dispatch, getState, { track }) => { + const connectionInfo = getCurrentConnectionInfo(getState(), connectionId); + track('Screen', { name: 'non_genuine_mongodb_modal' }, connectionInfo); + void _showNonGenuineMongoDBWarningModal(connectionInfo); + }; +}; + +type ImportConnectionsFn = Required['importConnections']; + +export const importConnections = ( + ...args: Parameters +): ConnectionsThunkAction< + Promise, + ConnectionsImportStartAction | ConnectionsImportFinishAction +> => { + return async (dispatch, _getState, { connectionStorage }) => { + dispatch({ type: ActionTypes.ConnectionsImportStart }); + let connections: ConnectionInfo[] = []; + let error; + try { + if (connectionStorage.importConnections) { + await connectionStorage.importConnections(...args); + connections = await connectionStorage.loadAll(); + } + } catch (err) { + error = err; + } + dispatch({ + type: ActionTypes.ConnectionsImportFinish, + connections: connections, + }); + // Because most of the import state and logic is still in a separate package + // we throw here to allow the import flow to continue working as it was + // before + if (error) { + throw error; + } + }; +}; + +export function configureStore( + preloadConnectionInfos: ConnectionInfo[] = [], + thunkArg: ThunkExtraArg +) { + return createStore( + // (state, action) => { + // const newState = reducer(state, action); + // console.log(action.type); + // // console.log(action.type, { action, old: state, new: newState }); + // return newState; + // }, + reducer, + { + ...INITIAL_STATE, + connections: getInitialConnectionsStateForConnectionInfos( + preloadConnectionInfos + ), + }, + applyMiddleware(thunk.withExtraArgument(thunkArg)) + ); +} diff --git a/packages/compass-connections/src/stores/connections-store.spec.tsx b/packages/compass-connections/src/stores/connections-store.spec.tsx index 8635a9a7af8..01bb6857804 100644 --- a/packages/compass-connections/src/stores/connections-store.spec.tsx +++ b/packages/compass-connections/src/stores/connections-store.spec.tsx @@ -1,60 +1,16 @@ import { expect } from 'chai'; -import { waitFor, cleanup, screen, render } from '@testing-library/react'; import sinon from 'sinon'; -import { createNewConnectionInfo } from './connections-store'; -import type { PreferencesAccess } from 'compass-preferences-model'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; -import { type ConnectionInfo } from '@mongodb-js/connection-storage/main'; +import { useConnections } from './connections-store'; import { - InMemoryConnectionStorage, - type ConnectionStorage, - ConnectionStorageProvider, -} from '@mongodb-js/connection-storage/provider'; -import { ConnectionsManager, ConnectionsManagerProvider } from '../provider'; -import type { DataService, connect } from 'mongodb-data-service'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import type { ComponentProps } from 'react'; -import React from 'react'; -import { - ConfirmationModalArea, - ToastArea, -} from '@mongodb-js/compass-components'; -import { - ConnectionsProvider, - useConnections, -} from '../components/connections-provider'; -import { - TelemetryProvider, - type TrackFunction, -} from '@mongodb-js/compass-telemetry/provider'; - -function getConnectionsManager(mockTestConnectFn?: typeof connect) { - const { log } = createNoopLogger(); - return new ConnectionsManager({ - logger: log.unbound, - __TEST_CONNECT_FN: mockTestConnectFn, - }); -} - -function wait(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -function createMockDataService() { - return { - mockDataService: 'yes', - addReauthenticationHandler() {}, - getUpdatedSecrets() { - return Promise.resolve({}); - }, - disconnect() {}, - } as unknown as DataService; -} - -const mockConnections: ConnectionInfo[] = [ + cleanup, + renderHookWithConnections, + waitFor, + screen, + createDefaultConnectionInfo, + wait, +} from '../test'; + +const mockConnections = [ { id: 'turtle', connectionOptions: { @@ -77,66 +33,12 @@ const mockConnections: ConnectionInfo[] = [ }, ]; -describe('useConnections', function () { - let connectionsManager: ConnectionsManager; - let mockConnectionStorage: ConnectionStorage; - let preferences: PreferencesAccess; - let renderHookWithContext: ( - props?: ComponentProps - ) => { current: ReturnType }; - let trackSpy: TrackFunction; - - before(async function () { - preferences = await createSandboxFromDefaultPreferences(); - trackSpy = sinon.spy(); - renderHookWithContext = (props) => { - const wrapper: React.FC = ({ children }) => { - return ( - - - - - - - - {children} - - - - - - - - ); - }; - const hookResult: { current: ReturnType } = { - current: {} as any, - }; - const UseConnections = () => { - hookResult.current = useConnections(); - return null; - }; - render(, { wrapper }); - return hookResult; - }; - }); - - beforeEach(async function () { - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: true, - maximumNumberOfActiveConnections: undefined, - }); - mockConnectionStorage = new InMemoryConnectionStorage([...mockConnections]); - connectionsManager = getConnectionsManager(async () => { - await wait(200); - return createMockDataService(); - }); - }); +const defaultPreferences = { + enableMultipleConnectionSystem: true, + maximumNumberOfActiveConnections: undefined, +}; +describe('useConnections', function () { afterEach(() => { cleanup(); sinon.resetHistory(); @@ -144,45 +46,63 @@ describe('useConnections', function () { }); it('autoconnects on mount and does not save autoconnect info', async function () { - const onConnected = sinon.spy(); - sinon.stub(mockConnectionStorage, 'getAutoConnectInfo').resolves({ - id: 'new', - connectionOptions: { - connectionString: 'mongodb://autoconnect', - }, - }); - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); - - renderHookWithContext({ onConnected }); + const { connectionsStore, connectionStorage } = renderHookWithConnections( + useConnections, + { + preferences: defaultPreferences, + connections: mockConnections, + onAutoconnectInfoRequest() { + return Promise.resolve({ + id: 'autoconnect', + connectionOptions: { + connectionString: 'mongodb://autoconnect', + }, + }); + }, + } + ); await waitFor(() => { - expect(onConnected).to.have.been.calledOnce; + expect(connectionsStore.getState().connections.byId) + .to.have.property('autoconnect') + .have.property('status', 'connected'); + }); + + const storedConnection = await connectionStorage.load({ + id: 'autoconnect', }); // autoconnect info should never be saved - expect(saveSpy).to.not.have.been.called; + expect(storedConnection).to.eq(undefined); }); describe('#connect', function () { - it('should show notifications throughout connection flow and save connection on disk', async function () { - const onConnectionAttemptStarted = sinon.spy(); - const onConnected = sinon.spy(); - const connections = renderHookWithContext({ - onConnectionAttemptStarted, - onConnected, + it('should show notifications throughout connection flow and save connection to persistent store', async function () { + const { result, connectionStorage, track } = renderHookWithConnections( + useConnections, + { + preferences: defaultPreferences, + connectFn: async () => { + await wait(100); + return {}; + }, + } + ); + + const connectionInfo = createDefaultConnectionInfo(); + + const storedConnectionBeforeConnect = await connectionStorage.load({ + id: connectionInfo.id, }); - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); + // Verifying it's not in storage + expect(storedConnectionBeforeConnect).to.eq(undefined); - const connectionInfo = createNewConnectionInfo(); - const connectPromise = connections.current.connect(connectionInfo); + const connectPromise = result.current.connect(connectionInfo); await waitFor(() => { - expect(onConnectionAttemptStarted).to.have.been.calledOnce; + expect(track).to.have.been.calledWith('Connection Attempt'); }); - // First time to save new connection in the storage - expect(saveSpy).to.have.been.calledOnce; - await waitFor(() => { expect(screen.getByText('Connecting to localhost:27017')).to.exist; }); @@ -191,25 +111,28 @@ describe('useConnections', function () { expect(screen.getByText('Connected to localhost:27017')).to.exist; - // Second time to update the connection lastUsed time - expect(saveSpy).to.have.been.calledTwice; + // Saved after connect + const storedConnectionAfterConnect = await connectionStorage.load({ + id: connectionInfo.id, + }); + expect(storedConnectionAfterConnect).to.exist; - expect(onConnected).to.have.been.calledOnce; + await waitFor(() => { + expect(track.getCall(1).firstArg).to.eq('New Connection'); + }); }); it('should show error toast if connection failed', async function () { - const onConnectionFailed = sinon.spy(); - const connections = renderHookWithContext({ - onConnectionFailed, + const { result } = renderHookWithConnections(useConnections, { + preferences: defaultPreferences, + connectFn: sinon + .stub() + .rejects(new Error('Failed to connect to cluster')), }); - const connectionInfo = createNewConnectionInfo(); + const connectionInfo = createDefaultConnectionInfo(); - sinon - .stub(connectionsManager, 'connect') - .rejects(new Error('Failed to connect to cluster')); - - const connectPromise = connections.current.connect(connectionInfo); + const connectPromise = result.current.connect(connectionInfo); await waitFor(() => { expect(screen.getByText('Failed to connect to cluster')).to.exist; @@ -217,7 +140,7 @@ describe('useConnections', function () { try { // Connect method should not reject, all the logic is encapsulated, - // there is no reason to expose it + // there is no reason to expose the error outside the store await connectPromise; } catch (err) { expect.fail('Expected connect() method to not throw'); @@ -225,9 +148,11 @@ describe('useConnections', function () { }); it('should show non-genuine modal at the end of connection if non genuine mongodb detected', async function () { - const connections = renderHookWithContext(); + const { result } = renderHookWithConnections(useConnections, { + preferences: defaultPreferences, + }); - await connections.current.connect({ + await result.current.connect({ id: '123', connectionOptions: { connectionString: @@ -242,14 +167,16 @@ describe('useConnections', function () { }); it('should show max connections toast if maximum connections number reached', async function () { - await preferences.savePreferences({ - maximumNumberOfActiveConnections: 0, + const { result } = renderHookWithConnections(useConnections, { + preferences: { + ...defaultPreferences, + maximumNumberOfActiveConnections: 0, + }, }); - const connections = renderHookWithContext(); - const connectionInfo = createNewConnectionInfo(); - - const connectPromise = connections.current.connect(connectionInfo); + const connectPromise = result.current.connect( + createDefaultConnectionInfo() + ); await waitFor(() => { expect(screen.getByText(/First disconnect from another connection/)).to @@ -261,22 +188,29 @@ describe('useConnections', function () { }); it('should show device auth code modal when OIDC flow triggers the notification', async function () { - const connections = renderHookWithContext(); let resolveConnect; - const spy = sinon.stub(connectionsManager, 'connect').returns( - new Promise((resolve) => { - resolveConnect = () => resolve(createMockDataService()); - }) - ); - const connectPromise = connections.current.connect( - createNewConnectionInfo() + const connectFn = sinon.stub().callsFake(() => { + return new Promise((resolve) => { + resolveConnect = () => resolve({}); + }); + }); + + const { result } = renderHookWithConnections(useConnections, { + preferences: defaultPreferences, + connectFn, + }); + + const connectPromise = result.current.connect( + createDefaultConnectionInfo() ); await waitFor(() => { - expect(spy).to.have.been.calledOnce; + expect(connectFn).to.have.been.calledOnce; }); - spy.getCall(0).lastArg.onNotifyOIDCDeviceFlow({ + const connectionOptions = connectFn.getCall(0).firstArg; + + connectionOptions.oidc.notifyDeviceFlow({ verificationUrl: 'http://example.com/device-auth', userCode: 'ABCabc123', }); @@ -309,47 +243,50 @@ describe('useConnections', function () { multipleConnectionsEnabled ? 'enabled' : 'disabled' }`, function () { it('should NOT update existing connection with new props when existing connection is successfull', async function () { - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: multipleConnectionsEnabled, - }); - - const connections = renderHookWithContext(); - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); + const { result, connectionStorage } = renderHookWithConnections( + useConnections, + { + connections: mockConnections, + preferences: { + ...defaultPreferences, + enableMultipleConnectionSystem: multipleConnectionsEnabled, + }, + } + ); - await connections.current.connect({ + await result.current.connect({ ...mockConnections[0], favorite: { name: 'foobar' }, }); - // Only once on success so that we're not updating existing connections if - // they failed - expect(saveSpy).to.have.been.calledOnce; - expect(saveSpy.getCall(0)).to.have.nested.property( - 'args[0].connectionInfo.favorite.name', - 'turtles' - ); + // Connection in the storage wasn't updated + expect( + await connectionStorage.load({ id: mockConnections[0].id }) + ).to.have.nested.property('favorite.name', 'turtles'); }); it('should not update existing connection if connection failed', async function () { - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: multipleConnectionsEnabled, - }); - - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); - const onConnectionFailed = sinon.spy(); - const connections = renderHookWithContext({ onConnectionFailed }); - - sinon - .stub(connectionsManager, 'connect') - .rejects(new Error('Failed to connect')); + const { result, connectionStorage } = renderHookWithConnections( + useConnections, + { + connections: mockConnections, + preferences: { + ...defaultPreferences, + enableMultipleConnectionSystem: multipleConnectionsEnabled, + }, + connectFn: sinon.stub().rejects(new Error('Failed to connect')), + } + ); - await connections.current.connect({ + await result.current.connect({ ...mockConnections[0], favorite: { name: 'foobar' }, }); - expect(onConnectionFailed).to.have.been.calledOnce; - expect(saveSpy).to.not.have.been.called; + // Connection in the storage wasn't updated + expect( + await connectionStorage.load({ id: mockConnections[0].id }) + ).to.have.nested.property('favorite.name', 'turtles'); }); }); } @@ -357,55 +294,54 @@ describe('useConnections', function () { describe('#disconnect', function () { it('disconnect even if connection is in progress cleaning up progress toasts', async function () { - const connections = renderHookWithContext(); - - sinon.spy(connectionsManager, 'closeConnection'); + const { result, track } = renderHookWithConnections(useConnections, { + preferences: defaultPreferences, + connectFn() { + return new Promise(() => { + // going to cancel this one + }); + }, + }); - const connectionInfo = createNewConnectionInfo(); - const connectPromise = connections.current.connect(connectionInfo); + const connectionInfo = createDefaultConnectionInfo(); + const connectPromise = result.current.connect(connectionInfo); await waitFor(() => { expect(screen.getByText(/Connecting to/)).to.exist; }); - await connections.current.disconnect(connectionInfo.id); + result.current.disconnect(connectionInfo.id); await connectPromise; - expect(trackSpy).to.have.been.calledWith('Connection Disconnected'); + expect(track).to.have.been.calledWith('Connection Disconnected'); expect(() => screen.getByText(/Connecting to/)).to.throw; - expect(connectionsManager).to.have.property('closeConnection').have.been - .calledOnce; }); }); describe('#createNewConnection', function () { - it('should "open" connection form create new connection info for editing every time', function () { - const connections = renderHookWithContext(); + it('in single connection mode should "open" connection form create new connection info for editing every time', function () { + const { result } = renderHookWithConnections(useConnections, { + preferences: { enableMultipleConnectionSystem: false }, + }); - expect(connections.current.state.isEditingConnectionInfoModalOpen).to.eq( + expect(result.current.state.isEditingConnectionInfoModalOpen).to.eq( false ); - connections.current.createNewConnection(); - const conn1 = connections.current.state.editingConnectionInfo; + result.current.createNewConnection(); + const conn1 = result.current.state.editingConnectionInfo; - expect(connections.current.state.isEditingConnectionInfoModalOpen).to.eq( - true - ); + expect(result.current.state.isEditingConnectionInfoModalOpen).to.eq(true); - connections.current.createNewConnection(); - const conn2 = connections.current.state.editingConnectionInfo; + result.current.createNewConnection(); + const conn2 = result.current.state.editingConnectionInfo; - expect(connections.current.state.isEditingConnectionInfoModalOpen).to.eq( - true - ); + expect(result.current.state.isEditingConnectionInfoModalOpen).to.eq(true); - connections.current.createNewConnection(); - const conn3 = connections.current.state.editingConnectionInfo; + result.current.createNewConnection(); + const conn3 = result.current.state.editingConnectionInfo; - expect(connections.current.state.isEditingConnectionInfoModalOpen).to.eq( - true - ); + expect(result.current.state.isEditingConnectionInfoModalOpen).to.eq(true); expect(conn1).to.not.deep.eq(conn2); expect(conn1).to.not.deep.eq(conn3); @@ -414,117 +350,98 @@ describe('useConnections', function () { describe('#saveEditedConnection', function () { it('new connection: should call save and track the creation', async function () { - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); - const connections = renderHookWithContext(); + const { result, track, connectionStorage } = renderHookWithConnections( + useConnections, + { + preferences: defaultPreferences, + } + ); - // Waiting for connections to load first - await waitFor(() => { - expect(connections.current.favoriteConnections).to.have.lengthOf.gt(0); - }); + // We can't save non-existent connections, create new one before + // proceeding + result.current.createNewConnection(); const newConnection = { - ...createNewConnectionInfo(), + ...result.current.state.editingConnectionInfo, favorite: { name: 'peaches (50) peaches', }, savedConnectionType: 'favorite', }; - await connections.current.saveEditedConnection(newConnection); + await result.current.saveEditedConnection(newConnection); - expect(saveSpy).to.have.been.calledOnce; - expect(trackSpy).to.have.been.calledWith('Connection Created'); + expect(track).to.have.been.calledWith('Connection Created'); - await waitFor(() => { - expect( - connections.current.favoriteConnections.find((info) => { - return info.id === newConnection.id; - }) - ).to.exist; - }); + expect(await connectionStorage.load({ id: newConnection.id })).to.exist; }); it('existing connection: should call save and should not track the creation', async function () { - const saveSpy = sinon.spy(mockConnectionStorage, 'save'); - const connections = renderHookWithContext(); - - // Waiting for connections to load first - await waitFor(() => { - expect(connections.current.favoriteConnections).to.have.lengthOf.gt(0); - }); + const { result, track, connectionStorage } = renderHookWithConnections( + useConnections, + { + connections: mockConnections, + preferences: defaultPreferences, + } + ); const updatedConnection = { ...mockConnections[0], savedConnectionType: 'recent', }; - await connections.current.saveEditedConnection(updatedConnection); + await result.current.saveEditedConnection(updatedConnection); - expect(saveSpy).to.have.been.calledOnce; - expect(trackSpy).to.have.been.calledWith('Connection Created'); + expect(track).not.to.have.been.called; - await waitFor(() => { - expect( - connections.current.recentConnections.find((info) => { - return info.id === updatedConnection.id; - }) - ).to.exist; - }); + expect( + await connectionStorage.load({ id: updatedConnection.id }) + ).to.have.property('savedConnectionType', 'recent'); }); }); describe('#removeConnection', function () { it('should disconnect and call delete and track the deletion', async function () { - const deleteSpy = sinon.spy(mockConnectionStorage, 'delete'); - const closeConnectionSpy = sinon.spy( - connectionsManager, - 'closeConnection' - ); - const connections = renderHookWithContext(); - - // Waiting for connections to load first - await waitFor(() => { - expect(connections.current.favoriteConnections).to.have.lengthOf.gt(0); - }); - - await connections.current.removeConnection(mockConnections[0].id); + const { result, connectionsStore, connectionStorage, track } = + renderHookWithConnections(useConnections, { + connections: mockConnections, + preferences: defaultPreferences, + }); - expect(closeConnectionSpy).to.have.been.calledOnce; - expect(trackSpy).to.have.been.calledWith('Connection Removed'); - expect(deleteSpy).to.have.been.calledOnce; - expect(trackSpy).to.have.been.calledWith('Connection Disconnected'); + result.current.removeConnection(mockConnections[0].id); await waitFor(() => { - expect( - connections.current.favoriteConnections.find((info) => { - return info.id === mockConnections[0].id; - }) - ).not.to.exist; + expect(track).to.have.been.calledWith('Connection Removed'); + expect(track).to.have.been.calledWith('Connection Disconnected'); }); + + expect(connectionsStore.getState().connections.byId).to.not.have.property( + mockConnections[0].id + ); + expect(await connectionStorage.load({ id: mockConnections[0].id })).to.not + .exist; }); }); describe('#editConnection', function () { - it('should only allow to edit existing connections', async function () { - const connections = renderHookWithContext(); - - // Waiting for connections to load first - await waitFor(() => { - expect(connections.current.favoriteConnections).to.have.lengthOf.gt(0); + it('should only allow to edit existing connections', function () { + const { result } = renderHookWithConnections(useConnections, { + connections: mockConnections, + preferences: defaultPreferences, }); - connections.current.editConnection('123'); - expect(connections.current.state).to.have.property( + result.current.editConnection('123'); + expect(result.current.state).to.have.property( 'isEditingConnectionInfoModalOpen', false ); - connections.current.editConnection(mockConnections[0].id); - expect(connections.current.state).to.have.property( + result.current.editConnection(mockConnections[0].id); + expect(result.current.state).to.have.property( 'isEditingConnectionInfoModalOpen', true ); - expect(connections.current.state).to.have.property( + expect(result.current.state).to.have.property( 'editingConnectionInfo', mockConnections[0] ); @@ -532,87 +449,95 @@ describe('useConnections', function () { }); describe('#duplicateConnection', function () { - it('should copy connection and add a copy number at the end', async function () { - const connections = renderHookWithContext(); + it('should copy connection and add a copy number at the end', function () { + const { result, connectionsStore } = renderHookWithConnections( + useConnections, + { + connections: mockConnections, + preferences: defaultPreferences, + } + ); for (let i = 0; i <= 30; i++) { - await connections.current.duplicateConnection(mockConnections[1].id, { + result.current.duplicateConnection(mockConnections[1].id, { autoDuplicate: true, }); } expect( - connections.current.favoriteConnections.find((info) => { - return info.favorite.name === 'peaches (30)'; - }) - ).to.exist; + Object.values(connectionsStore.getState().connections.byId).findIndex( + (connection) => { + return connection.info.favorite?.name === 'peaches (30)'; + } + ) + ).to.be.gte(0); }); it('should create a name if there is none', async function () { - const connectionWithoutName: ConnectionInfo = { - id: '123', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - favorite: { - name: '', - color: 'color2', - }, - savedConnectionType: 'recent', - }; - mockConnectionStorage = new InMemoryConnectionStorage([ - connectionWithoutName, - ]); - const connections = renderHookWithContext(); - - // Waiting for connections to load first - await waitFor(() => { - expect(connections.current.recentConnections).to.have.lengthOf.gt(0); - }); + const { result, connectionsStore } = renderHookWithConnections( + useConnections, + { + connections: [ + { + id: '123', + connectionOptions: { + connectionString: 'mongodb://localhost:27017', + }, + favorite: { + name: '', + color: 'color2', + }, + savedConnectionType: 'recent', + }, + ], + preferences: defaultPreferences, + } + ); - await connections.current.duplicateConnection(connectionWithoutName.id, { + result.current.duplicateConnection('123', { autoDuplicate: true, }); await waitFor(() => { expect( - connections.current.recentConnections.find((info) => { - return info.favorite.name === 'localhost:27017 (1)'; - }) - ).to.exist.and.to.have.nested.property('favorite.color', 'color2'); + Object.values(connectionsStore.getState().connections.byId).find( + (connection) => { + return connection.info.favorite?.name === 'localhost:27017 (1)'; + } + ) + ).to.exist.and.to.have.nested.property('info.favorite.color', 'color2'); }); }); it('should only look for copy number at the end of the connection name', async function () { - const connections = renderHookWithContext(); - const newConnection = { - ...createNewConnectionInfo(), + ...createDefaultConnectionInfo(), favorite: { name: 'peaches (50) peaches', }, savedConnectionType: 'favorite', }; - await connections.current.saveEditedConnection(newConnection); - - await waitFor(() => { - expect( - connections.current.favoriteConnections.find((info) => { - return info.id === newConnection.id; - }) - ).to.exist; - }); + const { result, connectionsStore } = renderHookWithConnections( + useConnections, + { + connections: [newConnection], + } + ); - await connections.current.duplicateConnection(newConnection.id, { + result.current.duplicateConnection(newConnection.id, { autoDuplicate: true, }); await waitFor(() => { expect( - connections.current.favoriteConnections.find((info) => { - return info.favorite.name === 'peaches (50) peaches (1)'; - }) + Object.values(connectionsStore.getState().connections.byId).find( + (connection) => { + return ( + connection.info.favorite?.name === 'peaches (50) peaches (1)' + ); + } + ) ).to.exist; }); }); diff --git a/packages/compass-connections/src/stores/connections-store.tsx b/packages/compass-connections/src/stores/connections-store.tsx index c8bc86daf3d..1259a364e11 100644 --- a/packages/compass-connections/src/stores/connections-store.tsx +++ b/packages/compass-connections/src/stores/connections-store.tsx @@ -1,884 +1,80 @@ -import { useCallback, useEffect, useReducer, useRef } from 'react'; -import type { DataService, connect } from 'mongodb-data-service'; -import { useConnectionsManagerContext } from '../provider'; -import { type ConnectionInfo } from '@mongodb-js/connection-storage/provider'; -import { cloneDeep } from 'lodash'; -import { UUID } from 'bson'; -import { useToast } from '@mongodb-js/compass-components'; -import { createLogger } from '@mongodb-js/compass-logging'; -import type { PreferencesAccess } from 'compass-preferences-model/provider'; -import { usePreferencesContext } from 'compass-preferences-model/provider'; -import { useConnectionRepository } from '../provider'; -import { useConnectionStorageContext } from '@mongodb-js/connection-storage/provider'; -import { useConnectionStatusNotifications } from '../components/connection-status-notifications'; -import { isCancelError } from '@mongodb-js/compass-utils'; -import { showNonGenuineMongoDBWarningModal as _showNonGenuineMongoDBWarningModal } from '../components/non-genuine-connection-modal'; -import { getGenuineMongoDB } from 'mongodb-build-info'; -import { useTelemetry } from '@mongodb-js/compass-telemetry/provider'; -import { getConnectionTitle } from '@mongodb-js/connection-info'; -import { - trackConnectionCreatedEvent, - trackConnectionDisconnectedEvent, - trackConnectionRemovedEvent, -} from '../utils/telemetry'; +import type { ConnectionInfo } from '@mongodb-js/connection-storage/provider'; +import { useConnectionActions, useConnectionsState } from './store-context'; -const { debug, mongoLogId, log } = createLogger('COMPASS-CONNECTIONS'); - -type ConnectFn = typeof connect; - -export type { ConnectFn }; - -export type RecursivePartial = { - [P in keyof T]?: T[P] extends (infer U)[] - ? RecursivePartial[] - : T[P] extends object | undefined - ? RecursivePartial - : T[P]; -}; - -export type PartialConnectionInfo = Pick & - RecursivePartial; - -export function createNewConnectionInfo(): ConnectionInfo { - return { - id: new UUID().toString(), - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - }; -} - -const InFlightConnections = new Map>(); - -// TODO(COMPASS-7397): Only contains state that is required for the connection -// flow but can't be accessed from connection storage, connections manager, or -// connections repository. Following-up we will be converting this to a redux -// store and merging connection manager and connection repository state to this -// store type State = { - connectionErrors: Record; - editingConnectionInfo: ConnectionInfo | null; + connectionErrors: Record; + editingConnectionInfo: ConnectionInfo; isEditingConnectionInfoModalOpen: boolean; oidcDeviceAuthState: Record; }; -type Action = - | { - // When user triggered connection for a certain connection info through - // the UI - type: 'attempt-connect'; - connectionInfo: ConnectionInfo; - isAutoConnect: boolean; - } - | { - // When connection attempt resolved with success - type: 'connection-attempt-succeeded'; - connectionInfo: ConnectionInfo; - } - | { - // When connection attempt failed with an error - type: 'connection-attempt-errored'; - connectionInfo: ConnectionInfo; - error: Error; - isAutoConnect: boolean; - } - | { - // When connection process requires user to go through the OIDC device - // auth flow that requires manually opening a link and posting a code in - // the browser - type: 'oidc-attempt-connect-notify-device-auth'; - connectionInfo: ConnectionInfo; - verificationUrl: string; - userCode: string; - } - | { - // When user creates a new, "empty" connection from anywhere in the app - type: 'new-connection'; - } - | { - // When user duplicates an existing connection from the sidebar actions UI - type: 'duplicate-connection'; - connectionInfo: ConnectionInfo; - autoDuplicate: boolean; - } - | { - // When user opens a connection for editing from anywhere in the app - type: 'edit-connection'; - connectionInfo: ConnectionInfo; - } - | { - // When user saves changes on an edited connection - type: 'save-edited-connection'; - connectionInfo: ConnectionInfo | null; - } - | { - // When user cancels editing without connecting or saving the connection - type: 'cancel-edit-connection'; - } - | { - // When user deletes connection using the sidebar actions UI - type: 'delete-connection'; - connectionInfo: ConnectionInfo; - }; - -export const createConnectionsReducer = (preferences: PreferencesAccess) => { - return (state: State, action: Action): State => { - switch (action.type) { - case 'attempt-connect': - return { - ...state, - // Autoconnect is a bit of a special case where we preset the - // connection form with the autoconnect info so that if it fails in - // single connection mode, the form is pre-populated as expected - editingConnectionInfo: action.isAutoConnect - ? action.connectionInfo - : state.editingConnectionInfo, - isEditingConnectionInfoModalOpen: - // Close connection editing modal when connection starts - action.connectionInfo.id === state.editingConnectionInfo?.id - ? false - : state.isEditingConnectionInfoModalOpen, - }; - case 'connection-attempt-succeeded': { - const connectionErrors = { ...state.connectionErrors }; - delete connectionErrors[action.connectionInfo.id]; - - const oidcDeviceAuthState = { ...state.oidcDeviceAuthState }; - delete oidcDeviceAuthState[action.connectionInfo.id]; - - return { - ...state, - connectionErrors, - oidcDeviceAuthState, - }; - } - case 'connection-attempt-errored': - return { - ...state, - editingConnectionInfo: - // Autoconnect special case for single connection: if we failed when - // autoconnecting, there might be no info set in the form yet if we - // failed while trying to read the autoconnect info. For that case - // we prepopulate the editing state so that the form can map the - // error correctly - action.isAutoConnect && !state.editingConnectionInfo - ? action.connectionInfo - : state.editingConnectionInfo, - connectionErrors: { - ...state.connectionErrors, - [action.connectionInfo.id]: action.error, - }, - }; - case 'oidc-attempt-connect-notify-device-auth': - return { - ...state, - oidcDeviceAuthState: { - ...state.oidcDeviceAuthState, - [action.connectionInfo.id]: { - url: action.verificationUrl, - code: action.userCode, - }, - }, - }; - case 'new-connection': - return { - ...state, - editingConnectionInfo: createNewConnectionInfo(), - isEditingConnectionInfoModalOpen: true, - }; - case 'duplicate-connection': - return { - ...state, - editingConnectionInfo: action.connectionInfo, - isEditingConnectionInfoModalOpen: action.autoDuplicate - ? // When autoduplicating we just save a copy right away without - // opening a editing modal - state.isEditingConnectionInfoModalOpen - : true, - }; - case 'edit-connection': { - if ( - // In multiple connections mode we don't allow to start another edit - // when one is already in progress - preferences.getPreferences().enableNewMultipleConnectionSystem && - state.isEditingConnectionInfoModalOpen - ) { - return state; - } - - // Do not update the state if we're already editing the same connection - if ( - state.isEditingConnectionInfoModalOpen && - state.editingConnectionInfo?.id === action.connectionInfo.id - ) { - return state; - } - - const connectionErrors = { ...state.connectionErrors }; - - // When switching from one connection editing form to another, reset the - // errors on the previous one - if ( - state.editingConnectionInfo && - action.connectionInfo.id !== state.editingConnectionInfo.id - ) { - delete connectionErrors[state.editingConnectionInfo.id]; - } - - return { - ...state, - connectionErrors, - editingConnectionInfo: action.connectionInfo, - isEditingConnectionInfoModalOpen: true, - }; - } - case 'save-edited-connection': { - return { - ...state, - isEditingConnectionInfoModalOpen: false, - }; - } - case 'cancel-edit-connection': { - return { - ...state, - isEditingConnectionInfoModalOpen: false, - }; - } - case 'delete-connection': { - const connectionErrors = { ...state.connectionErrors }; - delete connectionErrors[action.connectionInfo.id]; - - const oidcDeviceAuthState = { ...state.oidcDeviceAuthState }; - delete oidcDeviceAuthState[action.connectionInfo.id]; - - const isDeletingCurrentlyEditedConnection = - action.connectionInfo.id === state.editingConnectionInfo?.id; - - return { - ...state, - editingConnectionInfo: isDeletingCurrentlyEditedConnection - ? null - : state.editingConnectionInfo, - isEditingConnectionInfoModalOpen: isDeletingCurrentlyEditedConnection - ? false - : state.isEditingConnectionInfoModalOpen, - connectionErrors, - oidcDeviceAuthState, - }; - } - } - }; -}; - -function useCurrentRef(val: T): { current: T } { - const ref = useRef(val); - ref.current = val; - return ref; -} - -export function useConnections({ - onConnected, // TODO(COMPASS-7397): move connection-telemetry inside connections - onConnectionFailed, - onConnectionAttemptStarted, -}: { - onConnected?: ( - connectionInfo: ConnectionInfo, - dataService: DataService - ) => void; - onConnectionFailed?: ( - connectionInfo: ConnectionInfo | null, - error: Error - ) => void; - onConnectionAttemptStarted?: (connectionInfo: ConnectionInfo) => void; -} = {}): { +/** + * @deprecated use connections-store hooks instead + */ +export function useConnections(): { state: State; connect: (connectionInfo: ConnectionInfo) => Promise; - disconnect: (connectionId: string) => Promise; + disconnect: (connectionId: string) => void; createNewConnection: () => void; editConnection: (connectionId: string) => void; - saveEditedConnection: ( - connectionInfo: PartialConnectionInfo - ) => Promise; + saveEditedConnection: (connectionInfo: ConnectionInfo) => Promise; cancelEditConnection: (connectionId: string) => void; duplicateConnection: ( connectionId: string, options?: { autoDuplicate: boolean } - ) => Promise; - toggleConnectionFavoritedStatus: (connectionId: string) => Promise; - removeConnection: (connectionId: string) => Promise; + ) => void; + toggleConnectionFavoritedStatus: (connectionId: string) => void; + removeConnection: (connectionId: string) => void; - removeAllRecentConnections: () => Promise; + removeAllRecentConnections: () => void; showNonGenuineMongoDBWarningModal: (connectionId: string) => void; - - // This state should not be here, but is the only way we can currently check - // when those are loaded (we shouldn't be checking this in tests directly - // either, but this is right now the only way for us to test some methods on - // this hook) - favoriteConnections: ConnectionInfo[]; - recentConnections: ConnectionInfo[]; } { - const track = useTelemetry(); - // TODO(COMPASS-7397): services should not be used directly in render method, - // when this code is refactored to use the hadron plugin interface, storage - // should be handled through the plugin activation lifecycle - const connectionsManager = useConnectionsManagerContext(); - const connectionStorage = useConnectionStorageContext(); - const { - favoriteConnections, - nonFavoriteConnections: recentConnections, - saveConnection, - deleteConnection, - getConnectionInfoById, - filterConnectionInfo, - reduceConnectionInfo, - } = useConnectionRepository(); - const preferences = usePreferencesContext(); - - const reducer = useRef(createConnectionsReducer(preferences)); - const [state, dispatch] = useReducer(reducer.current, { - connectionErrors: {}, - editingConnectionInfo: null, - isEditingConnectionInfoModalOpen: false, - oidcDeviceAuthState: {}, - }); - - const onConnectionAttemptStartedRef = useCurrentRef( - onConnectionAttemptStarted - ); - const onConnectedRef = useCurrentRef(onConnected); - const onConnectionFailedRef = useCurrentRef(onConnectionFailed); - - const { openToast } = useToast('compass-connections'); - const { - openNotifyDeviceAuthModal, - openConnectionStartedToast, - openConnectionSucceededToast, - openConnectionFailedToast, - openMaximumConnectionsReachedToast, - closeConnectionStatusToast, - } = useConnectionStatusNotifications(); - - const saveConnectionInfo = useCallback( - async (connectionInfo: PartialConnectionInfo) => { - try { - const isNewConnection = !getConnectionInfoById(connectionInfo.id); - const savedConnectionInfo = await saveConnection(connectionInfo); - if (isNewConnection) { - trackConnectionCreatedEvent(savedConnectionInfo, track); - } - return savedConnectionInfo; - } catch (err) { - debug( - `error saving connection with id ${connectionInfo.id || ''}: ${ - (err as Error).message - }` - ); - - openToast(`save-connection-error-${connectionInfo.id}`, { - title: 'Error', - variant: 'warning', - description: `An error occurred while saving the connection. ${ - (err as Error).message - }`, - }); - - return null; - } - }, - [openToast, saveConnection, getConnectionInfoById, track] - ); - - const oidcUpdateSecrets = useCallback( - async (connectionInfo: ConnectionInfo, dataService: DataService) => { - try { - if (!preferences.getPreferences().persistOIDCTokens) { - return; - } - - await saveConnectionInfo({ - id: connectionInfo.id, - connectionOptions: await dataService.getUpdatedSecrets(), - }); - } catch (err: any) { - log.warn( - mongoLogId(1_001_000_195), - 'Connection Store', - 'Failed to update connection store with updated secrets', - { err: err?.stack } - ); - } - }, - [preferences, saveConnectionInfo] - ); - - const disconnect = useCallback( - async (connectionId: string) => { - debug('closing connection with connectionId', connectionId); - // In case connection is in progress - closeConnectionStatusToast(connectionId); - log.info( - mongoLogId(1_001_000_313), - 'Connection UI', - 'Initiating disconnect attempt' - ); - try { - await connectionsManager.closeConnection(connectionId); - trackConnectionDisconnectedEvent( - getConnectionInfoById(connectionId), - track - ); - } catch (error) { - log.error( - mongoLogId(1_001_000_314), - 'Connection UI', - 'Disconnect attempt failed', - { - error: (error as Error).message, - } - ); - } - debug('connection closed', connectionId); - }, - [ - closeConnectionStatusToast, - connectionsManager, - getConnectionInfoById, - track, - ] - ); - - const oidcAttemptConnectNotifyDeviceAuth = useCallback( - ( - connectionInfo: ConnectionInfo, - { - verificationUrl, - userCode, - }: { verificationUrl: string; userCode: string }, - signal: AbortSignal - ) => { - // For single connection store the device auth info so that we can append - // it to the "Connecting..." modal - dispatch({ - type: 'oidc-attempt-connect-notify-device-auth', - connectionInfo, - verificationUrl, - userCode, - }); - - openNotifyDeviceAuthModal( - connectionInfo, - verificationUrl, - userCode, - () => { - void disconnect(connectionInfo.id); - }, - signal - ); - }, - [disconnect, openNotifyDeviceAuthModal] - ); - - const createNewConnection = useCallback(() => { - dispatch({ type: 'new-connection' }); - }, []); - - const editConnection = useCallback( - (connectionId: string) => { - const connectionInfo = getConnectionInfoById(connectionId); - if (connectionInfo) { - dispatch({ type: 'edit-connection', connectionInfo }); - } - }, - [getConnectionInfoById] - ); - - const cancelEditConnection = useCallback(() => { - dispatch({ type: 'cancel-edit-connection' }); - }, []); - - const duplicateConnection = useCallback( - async ( - connectionId: string, - { autoDuplicate }: { autoDuplicate: boolean } = { autoDuplicate: false } - ) => { - const connectionInfo = getConnectionInfoById(connectionId); - - if (!connectionInfo) { - return; - } - - function parseFavoriteNameToNameAndCopyCount( - favoriteName: string - ): [string, number] { - const { groups = {} } = - favoriteName.match(/^(?.+?)(\s\((?\d+)\))?$/) ?? {}; - return [ - groups.name ?? favoriteName, - groups.count ? Number(groups.count) : 0, - ]; - } - - const duplicate: ConnectionInfo = { - ...cloneDeep(connectionInfo), - id: new UUID().toString(), - }; - - if (!duplicate.favorite || !duplicate.favorite.name) { - duplicate.favorite = { - ...duplicate.favorite, - name: getConnectionTitle(duplicate), - }; - } - - const [nameWithoutCount, copyCount] = parseFavoriteNameToNameAndCopyCount( - duplicate.favorite.name - ); - - const newCount = reduceConnectionInfo((topCount, connectionInfo) => { - if (connectionInfo.favorite?.name) { - const [name, count] = parseFavoriteNameToNameAndCopyCount( - connectionInfo.favorite.name - ); - if (name === nameWithoutCount && count >= topCount) { - return count + 1; - } - return topCount; - } - return topCount; - }, copyCount + 1); - - duplicate.favorite.name = `${nameWithoutCount} (${newCount})`; - - if (autoDuplicate) { - await saveConnectionInfo(duplicate); - } - - dispatch({ - type: 'duplicate-connection', - connectionInfo: duplicate, - autoDuplicate, - }); - }, - [getConnectionInfoById, reduceConnectionInfo, saveConnectionInfo] - ); - - const removeConnection = useCallback( - async (connectionId: string) => { - const connectionInfo = getConnectionInfoById(connectionId); - if (connectionInfo) { - void disconnect(connectionId); - await deleteConnection(connectionInfo); - trackConnectionRemovedEvent(connectionInfo, track); - dispatch({ type: 'delete-connection', connectionInfo }); - } - }, - [deleteConnection, disconnect, getConnectionInfoById, track] - ); - - const removeAllRecentConnections = useCallback(async () => { - await Promise.all( - filterConnectionInfo((connectionInfo) => { - return ( - !connectionInfo.savedConnectionType || - connectionInfo.savedConnectionType === 'recent' - ); - }).map((connectionInfo) => { - return removeConnection(connectionInfo.id); + const connectionsState = useConnectionsState(); + const state = { + connectionErrors: Object.fromEntries( + Object.entries(connectionsState.connections.byId).map(([k, v]) => { + return [k, v.error ?? null]; }) - ); - }, [filterConnectionInfo, removeConnection]); - - const saveEditedConnection = useCallback( - async (connectionInfo: PartialConnectionInfo) => { - const updatedConnectionInfo = await saveConnectionInfo(connectionInfo); - dispatch({ - type: 'save-edited-connection', - connectionInfo: updatedConnectionInfo, - }); - }, - [saveConnectionInfo] - ); - - const toggleConnectionFavoritedStatus = useCallback( - async (connectionId: string) => { - const connectionInfo = getConnectionInfoById(connectionId); - if (connectionInfo) { - connectionInfo.savedConnectionType = - connectionInfo.savedConnectionType === 'favorite' - ? 'recent' - : 'favorite'; - await saveConnectionInfo(connectionInfo); - } - }, - [getConnectionInfoById, saveConnectionInfo] - ); - - const showNonGenuineMongoDBWarningModal = useCallback( - (connectionId: string) => { - const connectionInfo = getConnectionInfoById(connectionId); - track('Screen', { name: 'non_genuine_mongodb_modal' }, connectionInfo); - void _showNonGenuineMongoDBWarningModal(connectionInfo); - }, - [getConnectionInfoById, track] - ); - - const connect = useCallback( - async ( - connectionInfoOrGetAutoconnectInfo: - | ConnectionInfo - | (() => Promise) - ) => { - const isAutoconnectAttempt = - typeof connectionInfoOrGetAutoconnectInfo === 'function'; - - const deviceAuthAbortController = new AbortController(); - - let connectionInfo: ConnectionInfo | undefined; - - try { - if (isAutoconnectAttempt) { - log.info( - mongoLogId(1_001_000_160), - 'Connection Store', - 'Performing automatic connection attempt' - ); - const autoConnectInfo = await connectionInfoOrGetAutoconnectInfo(); - if (!autoConnectInfo) { - return; - } - connectionInfo = autoConnectInfo; - } else { - connectionInfo = cloneDeep(connectionInfoOrGetAutoconnectInfo); - } - - const { - forceConnectionOptions, - browserCommandForOIDCAuth, - maximumNumberOfActiveConnections, - } = preferences.getPreferences(); - - if ( - typeof maximumNumberOfActiveConnections !== 'undefined' && - connectionsManager.getActiveConnectionsCount() >= - maximumNumberOfActiveConnections - ) { - openMaximumConnectionsReachedToast(maximumNumberOfActiveConnections); - return; - } - - // We use connection storage directly for this check here because - // connection repository state is React based and we have no means of - // making sure that we already loaded connections when doing this check. - // This should not be required after we remove the need to save the - // connection before we actually connect (or at all) for the application - // to function - let existingConnectionInfo: ConnectionInfo | undefined; - try { - existingConnectionInfo = await connectionStorage.load({ - id: connectionInfo.id, - }); - } catch { - // Assume connection doesn't exist yet - } - - // Auto-connect info should never be saved, connection storage has other - // means to returning this info as part of the connections list for now - if (!isAutoconnectAttempt) { - if ( - // TODO(COMPASS-7397): The way the whole connection logic is set up - // right now it is required that we save connection before starting - // the connection process even if we don't need or want to so that - // it can show up in the Compass UI and be picked up from connection - // storage by various providers, this should not be required, but - // we're preserving it for now to avoid even more refactoring - !existingConnectionInfo - ) { - await saveConnectionInfo(connectionInfo); - } - } - - dispatch({ - type: 'attempt-connect', - connectionInfo, - isAutoConnect: isAutoconnectAttempt, - }); - - const connectionId = connectionInfo.id; - - openConnectionStartedToast(connectionInfo, () => { - void disconnect(connectionId); - }); - - onConnectionAttemptStartedRef.current?.(connectionInfo); - - debug('connecting with connectionInfo', connectionInfo); - - log.info( - mongoLogId(1_001_000_004), - 'Connection UI', - 'Initiating connection attempt', - { isAutoconnectAttempt } - ); - - const currentConnectionInfo = connectionInfo; - - const dataService = await connectionsManager.connect(connectionInfo, { - forceConnectionOptions, - browserCommandForOIDCAuth, - onDatabaseSecretsChange: (...args) => { - void oidcUpdateSecrets(...args); - }, - onNotifyOIDCDeviceFlow: (deviceFlowInfo) => { - oidcAttemptConnectNotifyDeviceAuth( - currentConnectionInfo, - deviceFlowInfo, - deviceAuthAbortController.signal - ); - }, - }); - - try { - // Auto-connect info should never be saved - if (!isAutoconnectAttempt) { - // After connection is established we only update lastUsed time and - // maybe an OIDC token if preferences allow - await saveConnectionInfo({ - id: connectionInfo.id, - lastUsed: new Date(), - ...(preferences.getPreferences().persistOIDCTokens - ? { connectionOptions: await dataService.getUpdatedSecrets() } - : {}), - }); - } - } catch (err) { - debug( - 'failed to update connection info after successful connect', - err - ); - } - - dispatch({ type: 'connection-attempt-succeeded', connectionInfo }); - - openConnectionSucceededToast(connectionInfo); - - void onConnectedRef.current?.(connectionInfo, dataService); - - debug( - 'connection attempt succeeded with connection info', - connectionInfo - ); - - if ( - getGenuineMongoDB(connectionInfo.connectionOptions.connectionString) - .isGenuine === false - ) { - void showNonGenuineMongoDBWarningModal(connectionInfo.id); - } - } catch (error) { - const isConnectionCanceledError = isCancelError(error); - - log.error( - mongoLogId(1_001_000_161), - 'Connection Store', - 'Error performing connection attempt', - { - error: (error as Error).message, - isAutoconnectAttempt, - } - ); - - if (!isConnectionCanceledError) { - onConnectionFailedRef.current?.( - connectionInfo ?? null, - error as Error - ); - - openConnectionFailedToast(connectionInfo, error as Error, () => { - if (connectionInfo) { - editConnection(connectionInfo.id); - } - }); - } - - dispatch({ - type: 'connection-attempt-errored', - error: error as Error, - // Autoconnect flow might fail before we can even load connection info - // so connectionInfo might be undefiend at this point. In single - // connection mode this requires some special handling so that we can - // error back to the connection editing form, so we create a new - // connectionInfo if it's undefined - connectionInfo: connectionInfo ?? createNewConnectionInfo(), - isAutoConnect: isAutoconnectAttempt, - }); - } finally { - // Auto close device auth modal - deviceAuthAbortController.abort(); - } - }, - [ - connectionStorage, - connectionsManager, - disconnect, - editConnection, - oidcAttemptConnectNotifyDeviceAuth, - oidcUpdateSecrets, - onConnectedRef, - onConnectionAttemptStartedRef, - onConnectionFailedRef, - openConnectionFailedToast, - openConnectionStartedToast, - openConnectionSucceededToast, - openMaximumConnectionsReachedToast, - preferences, - saveConnectionInfo, - showNonGenuineMongoDBWarningModal, - ] - ); - - const connectRef = useCurrentRef(connect); - - useEffect(() => { - if (connectionStorage.getAutoConnectInfo) { - void connectRef.current( - connectionStorage.getAutoConnectInfo.bind(connectionStorage) - ); - } - }, [connectRef, connectionStorage]); - - const connectWithInflightCheck = useCallback( - async (connectionInfo: ConnectionInfo) => { - const inflightConnect = InFlightConnections.get(connectionInfo.id); - if (inflightConnect) { - return inflightConnect; - } - try { - const connectPromise = connectRef.current(connectionInfo); - InFlightConnections.set(connectionInfo.id, connectPromise); - return await connectPromise; - } finally { - InFlightConnections.delete(connectionInfo.id); - } - }, - [connectRef] - ); - + ), + editingConnectionInfo: + connectionsState.connections.byId[ + connectionsState.editingConnectionInfoId + ].info, + isEditingConnectionInfoModalOpen: + connectionsState.isEditingConnectionInfoModalOpen, + oidcDeviceAuthState: Object.fromEntries( + Object.entries(connectionsState.oidcDeviceAuthInfo).map(([k, v]) => { + return [k, { url: v.verificationUrl, code: v.userCode }]; + }) + ), + }; + const { + connect, + disconnect, + createNewConnection, + editConnection, + saveEditedConnection, + cancelEditConnection, + duplicateConnection, + toggleFavoritedConnectionStatus, + removeConnection, + removeAllRecentConnections, + showNonGenuineMongoDBWarningModal, + } = useConnectionActions(); return { state, - connect: connectWithInflightCheck, + connect, disconnect, createNewConnection, editConnection, saveEditedConnection, cancelEditConnection, duplicateConnection, - toggleConnectionFavoritedStatus, + toggleConnectionFavoritedStatus: toggleFavoritedConnectionStatus, removeConnection, removeAllRecentConnections, showNonGenuineMongoDBWarningModal, - favoriteConnections, - recentConnections, }; } diff --git a/packages/compass-connections/src/stores/store-context.tsx b/packages/compass-connections/src/stores/store-context.tsx new file mode 100644 index 00000000000..ff9b6fa581c --- /dev/null +++ b/packages/compass-connections/src/stores/store-context.tsx @@ -0,0 +1,337 @@ +import React, { createContext, useCallback, useContext, useState } from 'react'; +import type { + MapStateToProps, + ReactReduxContextValue, + TypedUseSelectorHook, +} from 'react-redux'; +import { + connect as reduxConnect, + createStoreHook, + createDispatchHook, + createSelectorHook, +} from 'react-redux'; +import type { + configureStore, + ConnectionId, + ConnectionState, +} from './connections-store-redux'; +import { + cancelEditConnection, + connect as connectionsConnect, + connectionsEventEmitter, + createNewConnection, + disconnect, + duplicateConnection, + editConnection, + getDataServiceForConnection, + removeAllRecentConnections, + removeConnection, + saveEditedConnectionInfo, + showNonGenuineMongoDBWarningModal, + toggleConnectionFavoritedStatus, + importConnections, + refreshConnections, +} from './connections-store-redux'; +import type { Store } from 'redux'; +import { + getConnectionTitle, + type ConnectionInfo, +} from '@mongodb-js/connection-info'; +import { createServiceLocator } from 'hadron-app-registry'; +import { isEqual } from 'lodash'; + +type ConnectionsStore = ReturnType extends Store< + infer S, + infer A +> & { dispatch: infer D } + ? { state: S; actions: A; dispatch: D } + : never; + +export const ConnectionsStoreContext = React.createContext< + ReactReduxContextValue + // @ts-expect-error not possible to correctly pass default value here +>(null); + +/** + * @internal should not be directly exported from this package + */ +export const useStore = createStoreHook( + ConnectionsStoreContext +) as () => ReturnType; + +/** + * @internal should not be directly exported from this package + */ +export const useDispatch = createDispatchHook( + ConnectionsStoreContext +) as () => ConnectionsStore['dispatch']; + +/** + * @internal should not be directly exported from this package + */ +const useSelector: TypedUseSelectorHook = + createSelectorHook(ConnectionsStoreContext); + +export const connect = (( + mapState: MapStateToProps, + mapDispatch = null, + mergeProps = null +) => { + return reduxConnect(mapState, mapDispatch, mergeProps, { + context: ConnectionsStoreContext, + }); +}) as typeof reduxConnect; + +function getConnectionsActions(dispatch: ConnectionsStore['dispatch']) { + return { + connect: (connectionInfo: ConnectionInfo) => { + return dispatch(connectionsConnect(connectionInfo)); + }, + disconnect: (connectionId: ConnectionId) => { + return dispatch(disconnect(connectionId)); + }, + createNewConnection: () => { + return dispatch(createNewConnection()); + }, + editConnection: (connectionId: ConnectionId) => { + return dispatch(editConnection(connectionId)); + }, + duplicateConnection: ( + connectionId: ConnectionId, + options?: { + autoDuplicate: boolean; + } + ) => { + return dispatch(duplicateConnection(connectionId, options)); + }, + saveEditedConnection: (connectionInfo: ConnectionInfo) => { + return dispatch(saveEditedConnectionInfo(connectionInfo)); + }, + cancelEditConnection: (connectionId: ConnectionId) => { + return dispatch(cancelEditConnection(connectionId)); + }, + toggleFavoritedConnectionStatus: (connectionId: ConnectionId) => { + return dispatch(toggleConnectionFavoritedStatus(connectionId)); + }, + removeConnection: (connectionId: ConnectionId) => { + return dispatch(removeConnection(connectionId)); + }, + removeAllRecentConnections: () => { + return dispatch(removeAllRecentConnections()); + }, + showNonGenuineMongoDBWarningModal: (connectionId: ConnectionId) => { + return dispatch(showNonGenuineMongoDBWarningModal(connectionId)); + }, + importConnections: (...args: Parameters) => { + return dispatch(importConnections(...args)); + }, + refreshConnections: () => { + return dispatch(refreshConnections()); + }, + }; +} + +const ConnectionActionsContext = createContext | null>(null); + +/** + * @internal We're using a provider here so that in test environment we can make + * sure we're using the same object reference in all renders and so can safely + * spy on the actions if test requires it. Should not be exported from this + * package + */ +export const ConnectionActionsProvider: React.FunctionComponent = ({ + children, +}) => { + const dispatch = useDispatch(); + const [actions] = useState(() => { + return getConnectionsActions(dispatch); + }); + return ( + + {children} + + ); +}; + +export function useConnectionActions() { + const actions = useContext(ConnectionActionsContext); + if (!actions) { + throw new Error( + "Can't find connection actions in context. Are you using useConnectionActions hook in correct environment?" + ); + } + return actions; +} + +export function useConnections() { + const store = useStore(); + const actions = useConnectionActions(); + const getConnectionById = useCallback( + (connectionId: string): ConnectionState | undefined => { + return store.getState().connections.byId[connectionId]; + }, + [store] + ); + return { + ...actions, + getConnectionById, + getDataServiceForConnection, + on: connectionsEventEmitter.on, + off: connectionsEventEmitter.off, + removeListener: connectionsEventEmitter.removeListener, + }; +} + +export const connectionsLocator = createServiceLocator( + useConnections, + 'connectionsLocator' +); + +function isShallowEqual( + a: Record | null, + b: Record | null +) { + if (a === null || b === null) { + return a === b; + } + const keys = new Set(Object.keys(a).concat(Object.keys(b))); + for (const key of keys) { + if (a[key] !== b[key]) { + return false; + } + } + return true; +} + +/** + * Returns (optionally filtered) list of connection ids. If you need to render a + * list of connections in the app, this hook allows us to subscribe to a list of + * connection items without subscribing to the actual state of those + * connections (subscription to those is usually required on a deeper level of + * the rendering) + */ +export function useConnectionIds( + filter?: (connection: ConnectionState) => boolean +): ConnectionId[] { + return useSelector( + (state) => { + return state.connections.ids.filter((id) => { + return filter?.(state.connections.byId[id]) ?? true; + }); + }, + (a, b) => { + return isEqual(a, b); + } + ); +} + +/** + * Returns (optionally filtered) list of connection state and subscribes to + * changes of all the connection state + * + * @deprecated should be avoided unless your component depends on literally all + * the state of all the connections in the list at once which is very rarely the + * case + */ +export function useConnectionsList( + filter?: (connection: ConnectionState) => boolean +) { + return useSelector( + (state) => { + return state.connections.ids + .filter((id) => { + return filter?.(state.connections.byId[id]) ?? true; + }) + .map((id) => { + return state.connections.byId[id]; + }); + }, + (a, b) => { + if (a.length !== b.length) { + return false; + } + if (a.length === 0) { + return true; + } + return a.every((connA, index) => { + return isShallowEqual(connA, b[index]); + }); + } + ); +} + +/** + * Returns single connection state for a certain connection id and subscribes to + * changes + */ +export function useConnectionForId( + connectionId: ConnectionId +): (ConnectionState & { title: string }) | null { + return useSelector( + (state) => { + const connection = state.connections.byId[connectionId]; + return connection + ? { ...connection, title: getConnectionTitle(connection.info) } + : null; + }, + (a, b) => { + return isShallowEqual(a, b); + } + ); +} + +/** + * Returns only connection info state and a title and subscribes to changes + */ +export function useConnectionInfoForId( + connectionId: ConnectionId +): (ConnectionInfo & { title: string }) | null { + return useSelector( + (state) => { + const connection = state.connections.byId[connectionId]; + return connection + ? { ...connection.info, title: getConnectionTitle(connection.info) } + : null; + }, + (a, b) => { + return isShallowEqual(a, b); + } + ); +} + +/** + * Returns a stable reference for the connection info when you need to access + * the value without triggering the render + */ +export function useConnectionInfoRefForId(connectionId: ConnectionId): { + current: (ConnectionInfo & { title: string }) | null; +} { + const store = useStore(); + const [ref] = useState(() => { + return { + get current() { + const connection = store.getState().connections.byId[connectionId]; + return connection + ? { ...connection.info, title: getConnectionTitle(connection.info) } + : null; + }, + }; + }); + return ref; +} + +/** + * @deprecated exposed for compat with old connections store interface, should + * never be used anywhere else + */ +export function useConnectionsState() { + return useSelector((state) => state, { + // These warnings are very noisy. We know this selector is bad, but there is + // no way to reimplement old connections store without it and we're going to + // remove it soon anyway + stabilityCheck: 'never', + noopCheck: 'never', + }); +} diff --git a/packages/compass-connections/src/test.tsx b/packages/compass-connections/src/test.tsx new file mode 100644 index 00000000000..0bc8b7760cd --- /dev/null +++ b/packages/compass-connections/src/test.tsx @@ -0,0 +1,691 @@ +/** + * TODO: move this to mocha-config-compass package + */ +import { EventEmitter } from 'events'; +import { + createNoopLogger, + LoggerProvider, +} from '@mongodb-js/compass-logging/provider'; +import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import type { ConnectionStorage } from '@mongodb-js/connection-storage/provider'; +import { + ConnectionStorageProvider, + InMemoryConnectionStorage, +} from '@mongodb-js/connection-storage/provider'; +import type { RenderResult } from '@testing-library/react'; +import { + render, + cleanup, + screen, + waitFor, + waitForElementToBeRemoved, + act, + within, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { + ConnectionOptions, + DataService, + InstanceDetails, +} from 'mongodb-data-service'; +import Sinon from 'sinon'; +import React from 'react'; +import type { + AllPreferences, + PreferencesAccess, +} from 'compass-preferences-model'; +import { + PreferencesProvider, + ReadOnlyPreferenceAccess, +} from 'compass-preferences-model/provider'; +import { TelemetryProvider } from '@mongodb-js/compass-telemetry/provider'; +import { CompassComponentsProvider } from '@mongodb-js/compass-components'; +import { + ConnectionInfoProvider, + TEST_CONNECTION_INFO, +} from './connection-info-provider'; +import type { State } from './stores/connections-store-redux'; +import { createDefaultConnectionInfo } from './stores/connections-store-redux'; +import { getDataServiceForConnection } from './stores/connections-store-redux'; +import { useConnectionActions, useStore } from './stores/store-context'; +import CompassConnections, { ConnectFnProvider } from './index'; +import type { HadronPluginComponent, HadronPlugin } from 'hadron-app-registry'; +import AppRegistry, { + AppRegistryProvider, + GlobalAppRegistryProvider, +} from 'hadron-app-registry'; +import { expect } from 'chai'; +import { Provider } from 'react-redux'; + +function wait(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +type ConnectionsOptions = { + /** + * Initial preferences + */ + preferences?: Partial; + /** + * Initial list of connections to be "loaded" to the application + */ + connections?: ConnectionInfo[]; + /** + * Connection function that returns DataService when connecting to a + * connection with the connections store. Second argument is a constructor + * with a bare minimum implementation of DataService required for the + * connections store to function + */ + connectFn?: ( + connectionOptions: ConnectionInfo['connectionOptions'] + ) => Partial | Promise>; +} & Partial< + Omit< + React.ComponentProps, + 'children' | 'preloadConnectionInfos' + > +>; + +class MockDataService + extends EventEmitter + implements + Pick< + DataService, + | 'addReauthenticationHandler' + | 'getCurrentTopologyType' + | 'getUpdatedSecrets' + | 'disconnect' + | 'instance' + > +{ + constructor(private connectionOptions: ConnectionInfo['connectionOptions']) { + super(); + this.setMaxListeners(0); + } + addReauthenticationHandler(): void { + // noop + } + getCurrentTopologyType(): ReturnType { + return 'Unknown'; + } + getLastSeenTopology(): ReturnType { + return { + type: 'Unknown', + servers: new Map(), + setName: null, + maxSetVersion: null, + maxElectionId: null, + stale: false, + compatible: false, + logicalSessionTimeoutMinutes: null, + heartbeatFrequencyMS: 0, + localThresholdMS: 0, + commonWireVersion: 0, + error: null, + hasKnownServers: false, + hasDataBearingServers: false, + toJSON() { + return JSON.parse(JSON.stringify(this)); + }, + }; + } + getUpdatedSecrets(): Promise> { + return Promise.resolve({}); + } + disconnect(): Promise { + return Promise.resolve(); + } + instance(): Promise { + return Promise.resolve({ + auth: { + user: null, + roles: [], + privileges: [], + }, + build: { + isEnterprise: false, + version: '0.0.0', + }, + host: {}, + genuineMongoDB: { + isGenuine: true, + dbType: 'mongodb', + }, + dataLake: { + isDataLake: false, + version: null, + }, + featureCompatibilityVersion: null, + isAtlas: false, + isLocalAtlas: false, + csfleMode: 'unavailable', + }); + } +} + +class InMemoryPreferencesAccess + extends ReadOnlyPreferenceAccess + implements PreferencesAccess +{ + private listeners: Record void)[]> = {}; + constructor(initialPreferences?: Partial) { + super(initialPreferences); + } + async savePreferences(attributes: Partial) { + const initialPreferences = { + ...this.getPreferences(), + }; + await this['_preferences'].savePreferences(attributes); + for (const [key, value] of Object.entries(attributes)) { + if ( + (initialPreferences as any)[key] !== value && + this.listeners[key] && + this.listeners[key].length > 0 + ) { + for (const listener of this.listeners[key]) { + listener(value); + } + } + } + return this.getPreferences(); + } + onPreferenceValueChanged( + key: keyof AllPreferences, + cb: (value: any) => void + ): () => void { + this.listeners[key] ??= []; + this.listeners[key].push(cb); + return () => { + this.listeners[key] = this.listeners[key].filter((fn) => { + return fn !== cb; + }); + }; + } +} + +function createWrapper(options: ConnectionsOptions, container?: HTMLElement) { + const wrapperState = { + globalAppRegistry: new AppRegistry(), + localAppRegistry: new AppRegistry(), + preferences: new InMemoryPreferencesAccess(options.preferences), + track: Sinon.stub(), + logger: createNoopLogger(), + connectionStorage: new InMemoryConnectionStorage( + options.connections + ) as ConnectionStorage, + connectionsStore: { + getState: undefined as unknown as () => State, + actions: {} as ReturnType, + }, + connect: async ({ + connectionOptions, + }: { + connectionOptions: ConnectionInfo['connectionOptions']; + }) => { + if (options.connectFn) { + const dataService = await options.connectFn?.(connectionOptions); + + // Presumably we are dealing with the real data service here based on + // this property being present, do not mess with it and just return it + // straight away. + // + // TODO: This check can probably be more robust, maybe we add some + // special prop for this on DataServiceImpl? + if (Object.prototype.hasOwnProperty.call(dataService, '_id')) { + return dataService as DataService; + } + + return Object.assign( + // Make sure the mock always has the minimum required functions, but + // also allow for them to be overriden + new MockDataService(connectionOptions), + dataService + ) as unknown as DataService; + } else { + return new MockDataService(connectionOptions) as unknown as DataService; + } + }, + getDataServiceForConnection, + }; + const StoreGetter: React.FunctionComponent = ({ children }) => { + const store = useStore(); + const actions = useConnectionActions(); + wrapperState.connectionsStore.getState = store.getState.bind(store); + wrapperState.connectionsStore.actions = actions; + return <>{children}; + }; + const logger = { + createLogger() { + return wrapperState.logger; + }, + }; + const telemetryOptions = { + sendTrack: wrapperState.track, + }; + const wrapper: React.FunctionComponent = ({ children }) => { + return ( + + + + + + + + + { + return Promise.resolve([{}, null] as [any, null]); + }) + } + onAutoconnectInfoRequest={ + options.onAutoconnectInfoRequest + } + preloadStorageConnectionInfos={options.connections} + > + {children} + + + + + + + + + + ); + }; + return { wrapperState, wrapper }; +} + +export type RenderConnectionsOptions< + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +> = { + container?: C; + baseElement?: BE; + wrapper?: React.JSXElementConstructor<{ children?: React.ReactElement }>; +} & ConnectionsOptions; + +function renderWithConnections< + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + ui: React.ReactElement, + { + wrapper: RenderWrapper, + container, + baseElement, + ...connectionsOptions + }: RenderConnectionsOptions = {} +) { + const { wrapper: Wrapper, wrapperState } = createWrapper( + connectionsOptions, + container as HTMLElement + ); + const wrappedWrapper = RenderWrapper + ? function WrappedWrapper({ children }: { children?: React.ReactElement }) { + return ( + + {children} + + ); + } + : Wrapper; + const result = render(ui, { + wrapper: wrappedWrapper, + container, + baseElement, + }); + expect( + (connectionsOptions.connections ?? []).every((info) => { + return !!wrapperState.connectionsStore.getState().connections.byId[ + info.id + ]; + }) + ).to.eq( + true, + 'Expected initial connections to load before rendering rest of the tested UI, but it did not happen' + ); + return { ...wrapperState, result }; +} + +function renderHookWithConnections< + HookProps, + HookResult, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + cb: (props: HookProps) => HookResult, + { + initialProps, + ...options + }: RenderConnectionsOptions & { + initialProps?: HookProps; + } = {} +) { + const result = { current: null } as { current: HookResult }; + function HookResultGetter(props: HookProps) { + result.current = cb(props); + return null; + } + const { result: renderResult, ...rest } = renderWithConnections( + , + options + ); + return { + ...rest, + rerender: (props?: HookProps) => { + return renderResult.rerender( + + ); + }, + result, + }; +} + +function renderPluginComponentWithConnections< + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + ui: React.ReactElement, + Plugin: HadronPluginComponent, + initialProps: T, + options: RenderConnectionsOptions = {} +) { + let plugin; + function ComponentWithProvider() { + plugin = Plugin.useActivate(initialProps); + return ( + + {ui} + + ); + } + const result = renderWithConnections( + , + options + ); + return { + plugin: plugin as unknown as A, + ...result, + }; +} + +function renderPluginHookWithConnections< + HookResult, + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + cb: () => HookResult, + Plugin: HadronPluginComponent, + initialProps: T, + options: RenderConnectionsOptions = {} +) { + const result = { current: null } as { current: HookResult }; + function HookResultGetter() { + result.current = cb(); + return null; + } + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + result: _renderResult, + ...rest + } = renderPluginComponentWithConnections( + , + Plugin, + initialProps, + options + ); + return { ...rest, result }; +} + +/** + * @deprecated instead of testing the store directly, test it through the UI as + * the redux documentation recommends + * @see {@link https://redux.js.org/usage/writing-tests#guiding-principles} + */ +function activatePluginWithConnections< + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + Plugin: HadronPluginComponent, + initialProps: T, + options: RenderConnectionsOptions = {} +) { + const { result, ...rest } = renderHookWithConnections(() => { + return Plugin.useActivate(initialProps); + }, options); + return { plugin: result.current, ...rest }; +} + +async function renderWithActiveConnection< + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + ui: React.ReactElement, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO, + options: RenderConnectionsOptions = {} +) { + function UiWithConnectionInfo() { + return ( + + {ui} + + ); + } + const renderResult = renderWithConnections( + , + { + ...options, + connections: [connectionInfo, ...(options.connections ?? [])], + } + ); + await renderResult.connectionsStore.actions.connect(connectionInfo); + // For ConnectionInfoProvider to render your input, we need to be connected + // successfully + const connectionState = + renderResult.connectionsStore.getState().connections.byId[ + connectionInfo.id + ]; + if (connectionState.status !== 'connected') { + if (connectionState.error) { + connectionState.error.message = + 'Failed to connect when rendering with active connection:\n\n' + + connectionState.error.message; + throw connectionState.error; + } else { + throw new Error( + `Expected to connect when renderering with active connection, instead the connection status is ${connectionState.status}` + ); + } + } + return renderResult; +} + +async function renderHookWithActiveConnection< + HookProps, + HookResult, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + cb: (props: HookProps) => HookResult, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO, + { + initialProps, + ...options + }: RenderConnectionsOptions & { + initialProps?: HookProps; + } = {} +) { + const result = { current: null } as { current: HookResult }; + function HookResultGetter(props: HookProps) { + result.current = cb(props); + return null; + } + const { result: renderResult, ...rest } = await renderWithActiveConnection( + , + connectionInfo, + options + ); + return { + ...rest, + rerender: (props?: HookResult) => { + return renderResult.rerender( + + ); + }, + result, + }; +} + +async function renderPluginComponentWithActiveConnection< + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + ui: React.ReactElement, + Plugin: HadronPluginComponent, + initialProps: T, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO, + options: RenderConnectionsOptions = {} +) { + let plugin; + function ComponentWithProvider() { + plugin = Plugin.useActivate(initialProps); + return ( + + {ui} + + ); + } + const result = await renderWithActiveConnection( + , + connectionInfo, + options + ); + return { + plugin: plugin as unknown as A, + ...result, + }; +} + +export type RenderWithConnectionsResult = ReturnType< + typeof createWrapper +>['wrapperState'] & { result: RenderResult }; + +export type RenderWithConnectionsHookResult< + HookProps = unknown, + HookResult = unknown +> = ReturnType['wrapperState'] & { + result: HookResult; + rerender: (props: HookProps) => void; +}; + +async function renderPluginHookWithActiveConnection< + HookResult, + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + cb: () => HookResult, + Plugin: HadronPluginComponent, + initialProps: T extends Record ? T | undefined : T, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO, + options: RenderConnectionsOptions = {} +) { + const result = { current: null } as { current: HookResult }; + function HookResultGetter() { + result.current = cb(); + return null; + } + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + result: _renderResult, + ...rest + } = await renderPluginComponentWithActiveConnection( + , + Plugin, + initialProps as T, + connectionInfo, + options + ); + return { ...rest, result }; +} + +/** + * @deprecated instead of testing the store directly, test it through the UI as + * the redux documentation recommends + * @see {@link https://redux.js.org/usage/writing-tests#guiding-principles} + */ +async function activatePluginWithActiveConnection< + T, + S extends Record unknown>, + A extends HadronPlugin, + C extends Element | DocumentFragment = HTMLElement, + BE extends Element | DocumentFragment = C +>( + Plugin: HadronPluginComponent, + initialProps: T, + connectionInfo: ConnectionInfo = TEST_CONNECTION_INFO, + options: RenderConnectionsOptions = {} +) { + const { result, ...rest } = await renderHookWithActiveConnection( + () => { + return Plugin.useActivate(initialProps); + }, + connectionInfo, + options + ); + return { plugin: result.current, ...rest }; +} + +export { + // There is never a good reason not to have these wrapper providers when + // rendering something in compass for testing. Using these render methods + // introduces a bit more run time, but most of the code in the application is + // not expecting those to be missing + renderWithConnections as render, + renderHookWithConnections as renderHook, + cleanup, + screen, + wait, + waitFor, + waitForElementToBeRemoved, + renderWithConnections, + renderWithActiveConnection, + renderHookWithConnections, + renderHookWithActiveConnection, + renderPluginComponentWithConnections, + renderPluginComponentWithActiveConnection, + renderPluginHookWithConnections, + renderPluginHookWithActiveConnection, + activatePluginWithConnections, + activatePluginWithActiveConnection, + act, + createDefaultConnectionInfo, + userEvent, + within, +}; diff --git a/packages/compass-connections/test.d.ts b/packages/compass-connections/test.d.ts new file mode 100644 index 00000000000..dc68cf65297 --- /dev/null +++ b/packages/compass-connections/test.d.ts @@ -0,0 +1 @@ +export * from './dist/test.d'; diff --git a/packages/compass-connections/test.js b/packages/compass-connections/test.js new file mode 100644 index 00000000000..2b7f7cb9dd8 --- /dev/null +++ b/packages/compass-connections/test.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = require('./dist/test'); diff --git a/packages/compass-crud/package.json b/packages/compass-crud/package.json index 776600c7cec..ce4c6374320 100644 --- a/packages/compass-crud/package.json +++ b/packages/compass-crud/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "13.38.0", + "version": "13.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,9 +48,9 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -59,42 +59,42 @@ "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "nyc": "^15.1.0", "react-dom": "^17.0.2", "sinon": "^8.1.1", "typescript": "^5.0.4" }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "@mongodb-js/my-queries-storage": "^0.15.0", - "@mongodb-js/reflux-state-mixin": "^1.0.4", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "@mongodb-js/my-queries-storage": "^0.15.1", + "@mongodb-js/reflux-state-mixin": "^1.0.5", "@mongodb-js/shell-bson-parser": "^1.1.0", "ag-grid-community": "^20.2.0", "ag-grid-react": "^20.2.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "hadron-type-checker": "^7.2.2", "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "prop-types": "^15.7.2", diff --git a/packages/compass-e2e-tests/.depcheckrc b/packages/compass-e2e-tests/.depcheckrc index 565004c4a61..6986d6670be 100644 --- a/packages/compass-e2e-tests/.depcheckrc +++ b/packages/compass-e2e-tests/.depcheckrc @@ -4,4 +4,4 @@ ignores: - '@wdio/types' - 'mongodb-compass' - 'ps-list' - - 'mongodb-runner' \ No newline at end of file + - 'mongodb-runner' diff --git a/packages/compass-e2e-tests/helpers/commands/connect-form.ts b/packages/compass-e2e-tests/helpers/commands/connect-form.ts index ce497ff5af1..60cd0784f4a 100644 --- a/packages/compass-e2e-tests/helpers/commands/connect-form.ts +++ b/packages/compass-e2e-tests/helpers/commands/connect-form.ts @@ -644,6 +644,12 @@ export async function setConnectFormState( state.oidcUsername ); } + if (state.oidcUseApplicationProxy === false) { + await browser.expandAccordion(Selectors.ConnectionFormOIDCAdvancedToggle); + await browser.clickParent( + Selectors.ConnectionFormOIDCUseApplicationProxyCheckbox + ); + } } // FLE2 diff --git a/packages/compass-e2e-tests/helpers/commands/disconnect.ts b/packages/compass-e2e-tests/helpers/commands/disconnect.ts index 3b04ebf3699..c9f2d0788fa 100644 --- a/packages/compass-e2e-tests/helpers/commands/disconnect.ts +++ b/packages/compass-e2e-tests/helpers/commands/disconnect.ts @@ -48,11 +48,7 @@ async function resetForDisconnect( closeToasts?: boolean; } = {} ) { - if (await browser.$(Selectors.LGModal).isDisplayed()) { - // close any modals that might be in the way - await browser.clickVisible(Selectors.LGModalClose); - await browser.$(Selectors.LGModal).waitForDisplayed({ reverse: true }); - } + await browser.hideVisibleModal(); // Collapse all the connections so that they will all hopefully fit on screen // and therefore be rendered. diff --git a/packages/compass-e2e-tests/helpers/commands/hide-visible-modal.ts b/packages/compass-e2e-tests/helpers/commands/hide-visible-modal.ts new file mode 100644 index 00000000000..11125cc4e83 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/hide-visible-modal.ts @@ -0,0 +1,24 @@ +import type { CompassBrowser } from '../compass-browser'; +import * as Selectors from '../selectors'; + +import Debug from 'debug'; + +const debug = Debug('compass-e2e-tests'); + +export async function hideVisibleModal(browser: CompassBrowser): Promise { + // If there's some race condition where something else is closing the modal at + // the same time we're trying to close the modal, then make it error out + // quickly so it can be ignored and we move on. + + if (await browser.$(Selectors.LGModal).isDisplayed()) { + // close any modals that might be in the way + const waitOptions = { timeout: 2_000 }; + try { + await browser.clickVisible(Selectors.LGModalClose, waitOptions); + await browser.$(Selectors.LGModal).waitForDisplayed({ reverse: true }); + } catch (err) { + // if the modal disappears by itself in the meantime, that's fine + debug('ignoring', err); + } + } +} diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 45c9d427af0..50e7b830042 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -59,5 +59,6 @@ export * from './create-index'; export * from './drop-index'; export * from './hide-index'; export * from './unhide-index'; +export * from './hide-visible-modal'; export * from './hide-visible-toasts'; export * from './sidebar-collection'; diff --git a/packages/compass-e2e-tests/helpers/commands/remove-connections.ts b/packages/compass-e2e-tests/helpers/commands/remove-connections.ts index a58ff2b7575..74e654e9c2d 100644 --- a/packages/compass-e2e-tests/helpers/commands/remove-connections.ts +++ b/packages/compass-e2e-tests/helpers/commands/remove-connections.ts @@ -3,11 +3,7 @@ import type { CompassBrowser } from '../compass-browser'; import * as Selectors from '../selectors'; async function resetForRemove(browser: CompassBrowser) { - if (await browser.$(Selectors.LGModal).isDisplayed()) { - // close any modals that might be in the way - await browser.clickVisible(Selectors.LGModalClose); - await browser.$(Selectors.LGModal).waitForDisplayed({ reverse: true }); - } + await browser.hideVisibleModal(); // Collapse all the connections so that they will all hopefully fit on screen // and therefore be rendered. diff --git a/packages/compass-e2e-tests/helpers/compass.ts b/packages/compass-e2e-tests/helpers/compass.ts index ba67adc1e4b..450b9185590 100644 --- a/packages/compass-e2e-tests/helpers/compass.ts +++ b/packages/compass-e2e-tests/helpers/compass.ts @@ -46,9 +46,8 @@ let MONGODB_USE_ENTERPRISE = // should we test compass-web (true) or compass electron (false)? export const TEST_COMPASS_WEB = process.argv.includes('--test-compass-web'); -export const TEST_MULTIPLE_CONNECTIONS = process.argv.includes( - '--test-multiple-connections' -); +// multiple connections is now the default when we're not testing compass-web +export const TEST_MULTIPLE_CONNECTIONS = !TEST_COMPASS_WEB; /* A helper so we can easily find all the tests we're skipping in compass-web. @@ -997,20 +996,6 @@ export async function init( ): Promise { name = pathName(name ?? formattedDate()); - // Use the multiple connections feature flag when testing multiple connections - // so that compass starts up with it already enabled. But be careful not to - // override the env var because there are tests that set it. - if ( - TEST_MULTIPLE_CONNECTIONS && - !process.env.COMPASS_GLOBAL_CONFIG_FILE_FOR_TESTING - ) { - process.env.COMPASS_GLOBAL_CONFIG_FILE_FOR_TESTING = path.join( - __dirname, - '..', - 'multiple-connections.yaml' - ); - } - // Unfortunately mocha's type is that this.test inside a test or hook is // optional even though it always exists. So we have a lot of // this.test?.fullTitle() and therefore we hopefully won't end up with a lot diff --git a/packages/compass-e2e-tests/helpers/connect-form-state.ts b/packages/compass-e2e-tests/helpers/connect-form-state.ts index bb7ac9eff32..9f1c087da39 100644 --- a/packages/compass-e2e-tests/helpers/connect-form-state.ts +++ b/packages/compass-e2e-tests/helpers/connect-form-state.ts @@ -41,6 +41,7 @@ export interface ConnectFormState { // - OIDC oidcUsername?: string; // (Principal). + oidcUseApplicationProxy?: boolean; // - AWS IAM awsAccessKeyId?: string; @@ -57,7 +58,7 @@ export interface ConnectFormState { tlsAllowInvalidCertificates?: boolean; // Proxy/SSH - proxyMethod?: 'none' | 'password' | 'identity' | 'socks'; + proxyMethod?: 'none' | 'password' | 'identity' | 'socks' | 'app-proxy'; // FLE2 fleKeyVaultNamespace?: string; diff --git a/packages/compass-e2e-tests/helpers/proxy.ts b/packages/compass-e2e-tests/helpers/proxy.ts new file mode 100644 index 00000000000..ab86ec8e574 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/proxy.ts @@ -0,0 +1,71 @@ +import type { + Server as HTTPServer, + IncomingMessage, + ServerResponse, +} from 'http'; +import { request } from 'http'; +import type { Socket } from 'net'; +import { connect } from 'net'; + +export interface ProxyHandlersResult { + connectRequests: IncomingMessage[]; + httpForwardRequests: IncomingMessage[]; + connections: Socket[]; +} + +export function setupProxyServer(server: HTTPServer): ProxyHandlersResult { + const connectRequests: IncomingMessage[] = []; + const httpForwardRequests: IncomingMessage[] = []; + const connections: Socket[] = []; + + server.on('connect', onconnect); + server.on('request', onrequest); + function onconnect( + this: HTTPServer, + req: IncomingMessage, + socket: Socket, + head: Buffer + ): void { + (req as any).server = this; + let host: string; + let port: string; + if (req.url?.includes(']:')) { + [host, port] = req.url.slice(1).split(']:'); + } else { + [host, port] = (req.url ?? '').split(':'); + } + if (host === 'compass.mongodb.com' || host === 'downloads.mongodb.com') { + // The snippet loader and update notifier can reach out to thes endpoints, + // but we usually do not actually wait for this to happen or not in CI, + // so we're just ignoring these requests here to avoid flaky behavior. + socket.end(); + return; + } + connectRequests.push(req); + socket.unshift(head); + socket.write('HTTP/1.0 200 OK\r\n\r\n'); + const outbound = connect(+port, host); + socket.pipe(outbound).pipe(socket); + // socket.on('data', chk => console.log('[from client] ' + chk.toString())); + // outbound.on('data', chk => console.log('[from server] ' + chk.toString())); + const cleanup = () => { + outbound.destroy(); + socket.destroy(); + }; + outbound.on('error', cleanup); + socket.on('error', cleanup); + connections.push(socket, outbound); + } + function onrequest(req: IncomingMessage, res: ServerResponse) { + httpForwardRequests.push(req); + const proxyReq = request( + req.url!, + { method: req.method, headers: req.headers }, + (proxyRes) => proxyRes.pipe(res) + ); + if (req.method === 'GET') proxyReq.end(); + else req.pipe(proxyReq); + } + + return { connections, connectRequests, httpForwardRequests }; +} diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index e920193e6fe..1fde25106af 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -103,6 +103,10 @@ export const ConnectionFormInputPlainPassword = '[data-testid="connection-plain-password-input"]'; export const ConnectionFormInputOIDCUsername = '[data-testid="connection-oidc-username-input"]'; +export const ConnectionFormOIDCAdvancedToggle = + '[data-testid="oidc-advanced-options"]'; +export const ConnectionFormOIDCUseApplicationProxyCheckbox = + '[data-testid="oidc-use-application-level-proxy"]'; export const ConnectionFormInputAWSAccessKeyId = '[data-testid="connection-form-aws-access-key-id-input"]'; export const ConnectionFormInputAWSSecretAccessKey = @@ -1387,5 +1391,11 @@ export const AtlasLoginStatus = '[data-testid="atlas-login-status"]'; export const AtlasLoginErrorToast = '#atlas-sign-in-error'; export const AgreeAndContinueButton = 'button=Agree and continue'; +// Proxy settings +export const ProxyUrl = + '[data-testid="proxy-settings"] [data-testid="proxy-url"]'; +export const ProxyCustomButton = + '[data-testid="proxy-settings"] [data-testid="custom-radio"]'; + // Close tab confirmation export const ConfirmTabCloseModal = '[data-testid="confirm-tab-close"]'; diff --git a/packages/compass-e2e-tests/index.ts b/packages/compass-e2e-tests/index.ts index 484ebece2ae..82c102442e3 100644 --- a/packages/compass-e2e-tests/index.ts +++ b/packages/compass-e2e-tests/index.ts @@ -25,7 +25,6 @@ const debug = Debug('compass-e2e-tests'); const allowedArgs = [ '--test-compass-web', - '--test-multiple-connections', '--no-compile', '--no-native-modules', '--test-packaged-app', diff --git a/packages/compass-e2e-tests/multiple-connections.yaml b/packages/compass-e2e-tests/multiple-connections.yaml index 3b66fed3d7f..704c8b375db 100644 --- a/packages/compass-e2e-tests/multiple-connections.yaml +++ b/packages/compass-e2e-tests/multiple-connections.yaml @@ -1 +1 @@ -enableNewMultipleConnectionSystem: true +enableMultipleConnectionSystem: true diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index a0dc7d16fef..988599c26c9 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "compass-e2e-tests", - "version": "1.24.0", + "version": "1.25.0", "private": true, "description": "E2E test suite for Compass app that follows smoke tests / feature testing matrix", "scripts": { @@ -34,9 +34,9 @@ }, "devDependencies": { "@electron/rebuild": "^3.6.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/oidc-mock-provider": "^0.9.3", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", @@ -48,21 +48,21 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "clipboardy": "^2.3.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "cross-spawn": "^7.0.3", "debug": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "eslint": "^7.25.0", "fast-glob": "^3.2.7", "glob": "^10.2.5", - "hadron-build": "^25.5.7", + "hadron-build": "^25.5.8", "lodash": "^4.17.21", "mocha": "^10.2.0", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", "mongodb-log-writer": "^1.4.2", - "mongodb-runner": "^5.6.2", + "mongodb-runner": "^5.6.3", "node-fetch": "^2.7.0", "nyc": "^15.1.0", "prettier": "^2.7.1", diff --git a/packages/compass-e2e-tests/tests/collection-import.test.ts b/packages/compass-e2e-tests/tests/collection-import.test.ts index 5ed7bef87be..b536ae31da4 100644 --- a/packages/compass-e2e-tests/tests/collection-import.test.ts +++ b/packages/compass-e2e-tests/tests/collection-import.test.ts @@ -1210,12 +1210,13 @@ describe('Collection import', function () { .$(Selectors.closeToastButton(Selectors.ImportToast)) .waitForDisplayed(); - // Displays first error in the toast and view log. + // Displays first two errors in the toast and view log. + // (It tries to display two, but it also limits the text) const toastText = await toastElement.getText(); expect(toastText).to.include('Import completed 0/3 with errors:'); expect( (toastText.match(/E11000 duplicate key error collection/g) || []).length - ).to.equal(1); + ).to.equal(2); expect(toastText).to.include('VIEW LOG'); const logFilePath = path.resolve( @@ -1231,7 +1232,7 @@ describe('Collection import', function () { const errorCount = ( logFileContent.match(/E11000 duplicate key error collection/g) || [] ).length; - expect(errorCount).to.equal(4); + expect(errorCount).to.equal(3); // Close toast. await browser.clickVisible( diff --git a/packages/compass-e2e-tests/tests/connection-form.test.ts b/packages/compass-e2e-tests/tests/connection-form.test.ts index cee3ac4d020..3c2ca7d7297 100644 --- a/packages/compass-e2e-tests/tests/connection-form.test.ts +++ b/packages/compass-e2e-tests/tests/connection-form.test.ts @@ -671,7 +671,9 @@ describe('Connection form', function () { ); await browser.waitUntil( async () => { - return (await clipboard.read()) === 'mongodb://localhost:27017/'; + return /^mongodb:\/\/localhost:27017\/?$/.test( + await clipboard.read() + ); }, { timeoutMsg: 'Expected copy to clipboard to work' } ); diff --git a/packages/compass-e2e-tests/tests/force-connection-options.test.ts b/packages/compass-e2e-tests/tests/force-connection-options.test.ts index ff462782111..1fcba173446 100644 --- a/packages/compass-e2e-tests/tests/force-connection-options.test.ts +++ b/packages/compass-e2e-tests/tests/force-connection-options.test.ts @@ -47,11 +47,20 @@ describe('forceConnectionOptions', function () { await browser.clickVisible(Selectors.Multiple.SidebarNewConnectionButton); } - const warnings = await browser - .$('[data-testid="connection-warnings-summary"]') - .getText(); - expect(warnings.trim()).to.equal( - 'Some connection options have been overridden through settings: appName' + await browser.waitUntil( + async () => { + const warnings = await browser + .$('[data-testid="connection-warnings-summary"]') + .getText(); + + return ( + warnings.trim() === + 'Some connection options have been overridden through settings: appName' + ); + }, + { + timeoutMsg: 'Expected connection warnings to mention overriden options', + } ); if (TEST_MULTIPLE_CONNECTIONS) { diff --git a/packages/compass-e2e-tests/tests/oidc.test.ts b/packages/compass-e2e-tests/tests/oidc.test.ts index d1db1b0ab0e..e4bfb8251dc 100644 --- a/packages/compass-e2e-tests/tests/oidc.test.ts +++ b/packages/compass-e2e-tests/tests/oidc.test.ts @@ -10,6 +10,7 @@ import { connectionNameFromString, TEST_MULTIPLE_CONNECTIONS, } from '../helpers/compass'; +import { setupProxyServer } from '../helpers/proxy'; import * as Selectors from '../helpers/selectors'; import type { Compass } from '../helpers/compass'; import type { OIDCMockProviderConfig } from '@mongodb-js/oidc-mock-provider'; @@ -18,6 +19,9 @@ import path from 'path'; import os from 'os'; import { promises as fs } from 'fs'; import { once, EventEmitter } from 'events'; +import type { Server as HTTPServer, IncomingMessage } from 'http'; +import { createServer as createHTTPServer } from 'http'; +import type { Socket, AddressInfo } from 'net'; import { expect } from 'chai'; import type { MongoCluster } from '@mongodb-js/compass-test-server'; import { startTestServer } from '@mongodb-js/compass-test-server'; @@ -92,11 +96,6 @@ describe('OIDC integration', function () { return this.skip(); } - // TODO(COMPASS-7966): Enable OIDC tests on 8.0.x when server fix is backported. - if (serverSatisfies('>= 8.0.0-alpha0 <8.1.0-rc0')) { - return this.skip(); - } - { oidcMockProviderEndpointAccesses = {}; oidcMockProviderConfig = { @@ -487,4 +486,72 @@ describe('OIDC integration', function () { expect(oidcMockProviderEndpointAccesses['/authorize']).to.equal(1); }); + + context('when using a proxy', function () { + let httpServer: HTTPServer; + let connectRequests: IncomingMessage[]; + let httpForwardRequests: IncomingMessage[]; + let connections: Socket[]; + + beforeEach(async function () { + await browser.setFeature('proxy', ''); + httpServer = createHTTPServer(); + ({ connectRequests, httpForwardRequests, connections } = + setupProxyServer(httpServer)); + httpServer.listen(0); + await once(httpServer, 'listening'); + }); + + afterEach(async function () { + await browser.setFeature('proxy', ''); + httpServer?.close?.(); + for (const conn of connections) { + if (!conn.destroyed) conn.destroy(); + } + }); + + it('can proxy both HTTP and MongoDB traffic through a proxy', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpServer.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + await browser.connectWithConnectionForm({ + hosts: [hostport], + authMethod: 'MONGODB-OIDC', + connectionName, + oidcUseApplicationProxy: true, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + expect(httpForwardRequests.map((c) => c.url)).to.include( + `${oidcMockProvider.issuer}/.well-known/openid-configuration` + ); + }); + + it('can choose not to forward OIDC HTTP traffic', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpServer.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + await browser.connectWithConnectionForm({ + hosts: [hostport], + authMethod: 'MONGODB-OIDC', + connectionName, + oidcUseApplicationProxy: false, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + expect(httpForwardRequests.map((c) => c.url)).to.be.empty; + }); + }); }); diff --git a/packages/compass-e2e-tests/tests/proxy.test.ts b/packages/compass-e2e-tests/tests/proxy.test.ts index 7a56c6807ef..059e678cf9d 100644 --- a/packages/compass-e2e-tests/tests/proxy.test.ts +++ b/packages/compass-e2e-tests/tests/proxy.test.ts @@ -7,10 +7,12 @@ import { import { expect } from 'chai'; import type { Compass } from '../helpers/compass'; import type { CompassBrowser } from '../helpers/compass-browser'; -import type { Server as HTTPServer } from 'http'; +import type { Server as HTTPServer, IncomingMessage } from 'http'; import { createServer as createHTTPServer } from 'http'; -import type { AddressInfo, Server } from 'net'; +import type { AddressInfo, Server, Socket } from 'net'; import { once } from 'events'; +import { setupProxyServer } from '../helpers/proxy'; +import * as Selectors from '../helpers/selectors'; async function listen(srv: Server): Promise { srv.listen(0); @@ -83,4 +85,45 @@ describe('Proxy support', function () { }); expect(result).to.equal('hello, http://compass.mongodb.com/ (proxy2)'); }); + + context('when connecting to a cluster', function () { + let connectRequests: IncomingMessage[]; + let connections: Socket[]; + + beforeEach(async function () { + compass = await init(this.test?.fullTitle()); + browser = compass.browser; + + await browser.setFeature('proxy', ''); + httpProxyServer1.removeAllListeners('request'); + ({ connectRequests, connections } = setupProxyServer(httpProxyServer1)); + }); + + afterEach(async function () { + await browser?.setFeature('proxy', ''); + for (const conn of connections) { + if (!conn.destroyed) conn.destroy(); + } + }); + + it('can proxy MongoDB traffic through a proxy', async function () { + await browser.openSettingsModal('proxy'); + await browser.clickParent(Selectors.ProxyCustomButton); + await browser.setValueVisible( + Selectors.ProxyUrl, + `http://localhost:${(httpProxyServer1.address() as AddressInfo).port}` + ); + await browser.clickVisible(Selectors.SaveSettingsButton); + + const hostport = '127.0.0.1:27091'; + const connectionName = this.test?.fullTitle() ?? ''; + await browser.connectWithConnectionForm({ + hosts: [hostport], + connectionName, + proxyMethod: 'app-proxy', + }); + + expect(connectRequests.map((c) => c.url)).to.include(hostport); + }); + }); }); diff --git a/packages/compass-e2e-tests/tests/search-indexes.test.ts b/packages/compass-e2e-tests/tests/search-indexes.test.ts index 8a35bcd535b..dbe41df9ada 100644 --- a/packages/compass-e2e-tests/tests/search-indexes.test.ts +++ b/packages/compass-e2e-tests/tests/search-indexes.test.ts @@ -234,7 +234,10 @@ describe('Search Indexes', function () { await browser.dropIndex(indexName); }); - it('renders search indexes tab disabled', async function () { + // TODO(COMPASS-8220): Un-skip this test + (name === 'Atlas Free Cluster' + ? it.skip + : it)('renders search indexes tab disabled', async function () { const searchTab = await browser.$( Selectors.indexesSegmentedTab('search-indexes') ); diff --git a/packages/compass-editor/package.json b/packages/compass-editor/package.json index ea1f8b2f91d..782d9b6f40b 100644 --- a/packages/compass-editor/package.json +++ b/packages/compass-editor/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.29.0", + "version": "0.29.1", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -46,8 +46,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -72,7 +72,7 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.2.0", - "@mongodb-js/compass-components": "^1.29.0", + "@mongodb-js/compass-components": "^1.29.1", "@mongodb-js/mongodb-constants": "^0.10.0", "mongodb-query-parser": "^4.2.0", "polished": "^4.2.2", diff --git a/packages/compass-explain-plan/package.json b/packages/compass-explain-plan/package.json index fa44c1f8e55..976c3c91bf8 100644 --- a/packages/compass-explain-plan/package.json +++ b/packages/compass-explain-plan/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "6.38.0", + "version": "6.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,8 +48,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -58,7 +58,7 @@ "@types/d3-hierarchy": "^3.1.2", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -69,18 +69,18 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/explain-plan-helper": "^1.1.15", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/explain-plan-helper": "^1.2.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", "d3-flextree": "^2.1.2", "d3-hierarchy": "^3.1.2", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", "react": "^17.0.2", diff --git a/packages/compass-export-to-language/package.json b/packages/compass-export-to-language/package.json index eb9feb9b0a2..01b3c097c8d 100644 --- a/packages/compass-export-to-language/package.json +++ b/packages/compass-export-to-language/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "9.14.0", + "version": "9.15.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,27 +48,27 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", "@mongodb-js/shell-bson-parser": "^1.1.0", - "bson-transpilers": "^3.0.6", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "bson-transpilers": "^3.0.7", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@mongodb-js/compass-logging": "^1.4.3", + "@mongodb-js/compass-logging": "^1.4.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "chai": "^4.3.6", diff --git a/packages/compass-export-to-language/src/stores/index.ts b/packages/compass-export-to-language/src/stores/index.ts index a68b227d875..1c3cd57de51 100644 --- a/packages/compass-export-to-language/src/stores/index.ts +++ b/packages/compass-export-to-language/src/stores/index.ts @@ -79,7 +79,9 @@ function getCurrentlyConnectedUri( } if ( - /^mongodb compass/i.exec( + // TODO: we should probably remove the default app name in place that knows + // what is default app name, like data service or compass-connections plugin + /^(mongodb compass|compass web)/i.exec( connectionStringUrl.searchParams.get('appName') || '' ) ) { diff --git a/packages/compass-field-store/package.json b/packages/compass-field-store/package.json index 95e2e286850..78a4ff3323d 100644 --- a/packages/compass-field-store/package.json +++ b/packages/compass-field-store/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "9.13.0", + "version": "9.14.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -49,12 +49,10 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", - "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@types/chai": "^4.2.21", "@types/mocha": "^9.0.0", "@types/sinon-chai": "^3.2.5", @@ -70,13 +68,15 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-connections": "^1.38.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-schema": "^12.2.0", "react": "^17.0.2", "react-redux": "^8.1.3", - "redux": "^4.2.1" + "redux": "^4.2.1", + "redux-thunk": "^2.4.2" }, "is_compass_plugin": true } diff --git a/packages/compass-field-store/src/index.spec.ts b/packages/compass-field-store/src/index.spec.ts index 55f1b1dd389..a407cc958df 100644 --- a/packages/compass-field-store/src/index.spec.ts +++ b/packages/compass-field-store/src/index.spec.ts @@ -1,45 +1,31 @@ -import { renderHook, cleanup } from '@testing-library/react-hooks'; -import { waitFor } from '@testing-library/react'; import FieldStorePlugin from './'; import { useAutocompleteFields } from './'; import { expect } from 'chai'; -import AppRegistry from 'hadron-app-registry'; -import { useConnectionInfoAccess } from '@mongodb-js/compass-connections/provider'; -import { useDispatch } from './stores/context'; -import { createFieldStoreService } from './stores/field-store-service'; - -export const useFieldStoreServiceForTests = () => { - const dispatch = useDispatch(); - const connectionInfoAccess = useConnectionInfoAccess(); - return createFieldStoreService(dispatch, connectionInfoAccess); -}; +import { useFieldStoreService } from './stores/field-store-service'; +import { + renderPluginHookWithActiveConnection, + cleanup, + waitFor, +} from '@mongodb-js/compass-connections/test'; describe('useAutocompleteFields', function () { - let appRegistry: AppRegistry; - let Plugin: ReturnType; - - beforeEach(function () { - appRegistry = new AppRegistry(); - Plugin = FieldStorePlugin.withMockServices({ - globalAppRegistry: appRegistry, - }); - }); - afterEach(cleanup); - it('returns empty list when namespace schema is not available', function () { - const { result } = renderHook(() => useAutocompleteFields('foo.bar'), { - wrapper: Plugin, - }); + it('returns empty list when namespace schema is not available', async function () { + const { result } = await renderPluginHookWithActiveConnection( + () => useAutocompleteFields('foo.bar'), + FieldStorePlugin, + {} + ); expect(result.current).to.deep.eq([]); }); it('updates when fields are added', async function () { - const { result } = renderHook( + const { result } = await renderPluginHookWithActiveConnection( () => { const autoCompleteFields = useAutocompleteFields('foo.bar'); - const fieldStoreService = useFieldStoreServiceForTests(); + const fieldStoreService = useFieldStoreService(); return { getAutoCompleteFields() { return autoCompleteFields; @@ -49,9 +35,8 @@ describe('useAutocompleteFields', function () { }, }; }, - { - wrapper: Plugin, - } + FieldStorePlugin, + {} ); await result.current diff --git a/packages/compass-field-store/src/index.tsx b/packages/compass-field-store/src/index.tsx index c124627367f..cbc8073e6fa 100644 --- a/packages/compass-field-store/src/index.tsx +++ b/packages/compass-field-store/src/index.tsx @@ -2,20 +2,24 @@ import React from 'react'; import { registerHadronPlugin } from 'hadron-app-registry'; import { activatePlugin } from './stores/store'; import { connectionsManagerLocator } from '@mongodb-js/compass-connections/provider'; +import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; + +const FieldStoreComponent: React.FunctionComponent = ({ children }) => { + // FieldStore plugin doesn't render anything, but keeps track of changes to + // the namespace documents and maintains a schema to be used with + // autocompleters + return <>{children}; +}; const FieldStorePlugin = registerHadronPlugin( { name: 'FieldStore', - component({ children }) { - // FieldStore plugin doesn't render anything, but keeps track of changes to - // the namespace documents and maintains a schema to be used with - // autocompleters - return <>{children}; - }, + component: FieldStoreComponent, activate: activatePlugin, }, { connectionsManager: connectionsManagerLocator, + logger: createLoggerLocator('COMPASS-FIELDS-STORE'), } ); diff --git a/packages/compass-field-store/src/modules/index.ts b/packages/compass-field-store/src/modules/index.ts index f2a771c96e7..db11c4c9b48 100644 --- a/packages/compass-field-store/src/modules/index.ts +++ b/packages/compass-field-store/src/modules/index.ts @@ -5,6 +5,8 @@ import { parseSchema } from 'mongodb-schema'; import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; import type { SchemaFieldSubset } from './fields'; import { mergeSchema } from './fields'; +import type { ThunkAction } from 'redux-thunk'; +import type { Logger } from '@mongodb-js/compass-logging/provider'; function isAction( action: Action, @@ -75,17 +77,33 @@ interface DocumentsUpdatedAction { schemaFields: SchemaField[]; } -export const documentsUpdated = async ( +export const documentsUpdated = ( connectionInfoId: ConnectionInfo['id'], namespace: string, documents: Array> -): Promise => { - const { fields } = await parseSchema(documents); - return { - type: DOCUMENTS_UPDATED, - connectionInfoId, - namespace, - schemaFields: fields, +): ThunkAction< + Promise, + ConnectionNamespacesState, + { logger: Logger }, + DocumentsUpdatedAction +> => { + return async (dispatch, _getState, { logger: { mongoLogId, log } }) => { + try { + const { fields } = await parseSchema(documents); + dispatch({ + type: DOCUMENTS_UPDATED, + connectionInfoId, + namespace, + schemaFields: fields, + }); + } catch (err) { + log.warn( + mongoLogId(1_001_000_328), + 'Field Store', + 'Failed to generate schema for documents', + { error: (err as Error).message } + ); + } }; }; diff --git a/packages/compass-field-store/src/stores/context.ts b/packages/compass-field-store/src/stores/context.ts index 49a2ec3784e..fdf4f8e4a86 100644 --- a/packages/compass-field-store/src/stores/context.ts +++ b/packages/compass-field-store/src/stores/context.ts @@ -2,6 +2,7 @@ import React from 'react'; import type { ReactReduxContextValue, TypedUseSelectorHook } from 'react-redux'; import { createSelectorHook, createDispatchHook } from 'react-redux'; import { type ConnectionNamespacesState } from '../modules'; +import type { activatePlugin } from './store'; export const FieldStoreContext = React.createContext< ReactReduxContextValue @@ -10,7 +11,11 @@ export const FieldStoreContext = React.createContext< null ); -export const useDispatch = createDispatchHook(FieldStoreContext); +type Dispatch = ReturnType['store']['dispatch']; + +export const useDispatch = createDispatchHook( + FieldStoreContext +) as () => Dispatch; export const useSelector: TypedUseSelectorHook = createSelectorHook(FieldStoreContext); diff --git a/packages/compass-field-store/src/stores/field-store-service.ts b/packages/compass-field-store/src/stores/field-store-service.ts index fc0c9295047..1a9b915cea6 100644 --- a/packages/compass-field-store/src/stores/field-store-service.ts +++ b/packages/compass-field-store/src/stores/field-store-service.ts @@ -1,7 +1,7 @@ import { type Schema } from 'mongodb-schema'; import { createServiceLocator } from 'hadron-app-registry'; import { - connectionInfoAccessLocator, + useConnectionInfoAccess, type ConnectionInfoAccess, } from '@mongodb-js/compass-connections/provider'; import { useDispatch } from './context'; @@ -15,7 +15,7 @@ export type FieldStoreService = { updateFieldsFromSchema(ns: string, schema: Schema): void; }; -export function createFieldStoreService( +function createFieldStoreService( dispatch: ReturnType, connectionInfoAccess: ConnectionInfoAccess ): FieldStoreService { @@ -24,17 +24,13 @@ export function createFieldStoreService( ns: string, documents: Record[] ) { - try { - dispatch( - await documentsUpdated( - connectionInfoAccess.getCurrentConnectionInfo().id, - ns, - documents - ) - ); - } catch (error) { - // ignore errors - } + await dispatch( + documentsUpdated( + connectionInfoAccess.getCurrentConnectionInfo().id, + ns, + documents + ) + ); }, updateFieldsFromSchema(ns: string, schema: Schema) { dispatch( @@ -48,11 +44,16 @@ export function createFieldStoreService( }; } +/** + * @internal exported for test purposes only + */ +export function useFieldStoreService() { + const dispatch = useDispatch(); + const connectionInfoAccess = useConnectionInfoAccess(); + return createFieldStoreService(dispatch, connectionInfoAccess); +} + export const fieldStoreServiceLocator = createServiceLocator( - function fieldStoreServiceLocator() { - const dispatch = useDispatch(); - const connectionInfoAccess = connectionInfoAccessLocator(); - return createFieldStoreService(dispatch, connectionInfoAccess); - }, + useFieldStoreService, 'fieldStoreServiceLocator' ); diff --git a/packages/compass-field-store/src/stores/store.spec.ts b/packages/compass-field-store/src/stores/store.spec.ts index 17f73776f2a..5d15a5aa637 100644 --- a/packages/compass-field-store/src/stores/store.spec.ts +++ b/packages/compass-field-store/src/stores/store.spec.ts @@ -1,56 +1,42 @@ -import { createActivateHelpers } from 'hadron-app-registry'; import schemaFixture from '../../test/fixtures/array_of_docs.fixture.json'; -import { activatePlugin } from './store'; +import type { activatePlugin } from './store'; import { expect } from 'chai'; import { schemaFieldsToAutocompleteItems } from '../modules/fields'; -import { - type FieldStoreService, - createFieldStoreService, -} from './field-store-service'; import type { Schema } from 'mongodb-schema'; -import type { - ConnectionInfoAccess, - ConnectionInfo, -} from '@mongodb-js/compass-connections/provider'; import { - ConnectionsManager, - ConnectionsManagerEvents, -} from '@mongodb-js/compass-connections/provider'; + activatePluginWithConnections, + cleanup, +} from '@mongodb-js/compass-connections/test'; +import FieldStorePlugin from '..'; +import { documentsUpdated, schemaUpdated } from '../modules'; describe('FieldStore', function () { - let deactivate = () => {}; let store: ReturnType['store']; - let fieldStoreServices: ReturnType; - let mockConnectionsManager: ConnectionsManager; - const connectionInfo: ConnectionInfo = { - id: '1234', - connectionOptions: { - connectionString: 'mongodb://webscales.com:27017', - }, + let connectionsStore: any; + const connectionInfo = { id: '1234' }; + const updateFieldsFromDocuments = ( + ns: string, + docs: any[], + connectionId = connectionInfo.id + ) => { + return store.dispatch(documentsUpdated(connectionId, ns, docs)); }; - const connectionInfoAccess: ConnectionInfoAccess = { - getCurrentConnectionInfo() { - return connectionInfo; - }, + const updateFieldsFromSchema = ( + ns: string, + schema: any, + connectionId = connectionInfo.id + ) => { + return store.dispatch(schemaUpdated(connectionId, ns, schema)); }; beforeEach(function () { - mockConnectionsManager = new ConnectionsManager({ - logger: (() => {}) as any, - }); - ({ store, deactivate } = activatePlugin( - {}, - { connectionsManager: mockConnectionsManager }, - createActivateHelpers() - )); - fieldStoreServices = createFieldStoreService( - store.dispatch.bind(store), - connectionInfoAccess - ); + const result = activatePluginWithConnections(FieldStorePlugin, {}); + store = result.plugin.store; + connectionsStore = result.connectionsStore; }); afterEach(function () { - deactivate(); + cleanup(); }); it('has an initial state', function () { @@ -58,48 +44,19 @@ describe('FieldStore', function () { }); context('when connection is disconnected', function () { - let connectionOneFieldStoreService: FieldStoreService; - let connectionTwoFieldStoreService: FieldStoreService; - const secondConnectionInfo: ConnectionInfo = { - ...connectionInfo, - id: 'QWERTY', - }; beforeEach(async function () { - connectionOneFieldStoreService = createFieldStoreService( - store.dispatch.bind(store), - connectionInfoAccess - ); - connectionTwoFieldStoreService = createFieldStoreService( - store.dispatch.bind(store), - { - getCurrentConnectionInfo() { - return { - ...connectionInfo, - id: 'QWERTY', - }; - }, - } - ); - await connectionOneFieldStoreService.updateFieldsFromDocuments( + await updateFieldsFromDocuments('mflix.movies', [{ name: 'Compass' }]); + await updateFieldsFromDocuments( 'mflix.movies', - [{ name: 'Compass' }] - ); - await connectionTwoFieldStoreService.updateFieldsFromDocuments( - 'mflix.movies', - [{ name: 'Compass' }] + [{ name: 'Compass' }], + 'QWERTY' ); expect(store.getState()).to.have.keys(['1234', 'QWERTY']); }); it('removes the namespaces information for the disconnected connection', function () { - mockConnectionsManager.emit( - ConnectionsManagerEvents.ConnectionDisconnected, - secondConnectionInfo.id - ); + connectionsStore.actions.disconnect('QWERTY'); expect(store.getState()).to.have.keys(['1234']); - mockConnectionsManager.emit( - ConnectionsManagerEvents.ConnectionDisconnected, - connectionInfo.id - ); + connectionsStore.actions.disconnect('1234'); expect(Object.keys(store.getState())).to.be.of.length(0); }); }); @@ -126,10 +83,7 @@ describe('FieldStore', function () { ]; it('on schema store trigger', function () { - fieldStoreServices.updateFieldsFromSchema( - 'test.test', - schemaFixture as Schema - ); + updateFieldsFromSchema('test.test', schemaFixture as Schema); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ '_id', @@ -145,7 +99,7 @@ describe('FieldStore', function () { }); it('on documents-refreshed', async function () { - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -157,7 +111,7 @@ describe('FieldStore', function () { }); it('on document-inserted', async function () { - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -169,7 +123,7 @@ describe('FieldStore', function () { }); it('on documents-paginated', async function () { - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -184,7 +138,7 @@ describe('FieldStore', function () { describe('store process methods', function () { it('samples a single document', async function () { const doc = { harry: 1, potter: true }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -215,7 +169,7 @@ describe('FieldStore', function () { { harry: 1, potter: true }, { ron: 'test', weasley: null }, ]; - await fieldStoreServices.updateFieldsFromDocuments('test.test', docs); + await updateFieldsFromDocuments('test.test', docs); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -261,9 +215,9 @@ describe('FieldStore', function () { it('merges new docs with the existing state', async function () { const doc = { harry: 1, potter: true }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const doc2 = { hermione: 0, granger: false }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc2]); + await updateFieldsFromDocuments('test.test', [doc2]); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -309,11 +263,8 @@ describe('FieldStore', function () { it('merges a schema with the existing state', async function () { const doc = { harry: 1, potter: true }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); - fieldStoreServices.updateFieldsFromSchema( - 'test.test', - schemaFixture as Schema - ); + await updateFieldsFromDocuments('test.test', [doc]); + updateFieldsFromSchema('test.test', schemaFixture as Schema); const state = store.getState()[connectionInfo.id]['test.test']; expect(Object.keys(state.fields)).to.have.all.members([ 'harry', @@ -422,14 +373,14 @@ describe('FieldStore', function () { it('flattens the schema', async function () { const doc = { a: { b: { c: 1 } } }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(state.fields).to.have.all.keys(['a', 'a.b', 'a.b.c']); }); it('maintains list of root fields', async function () { const doc = { a: { b: { c: 1 } }, d: 5, e: { f: 3 } }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(state.topLevelFields).to.have.all.members(['a', 'd', 'e']); }); @@ -437,7 +388,7 @@ describe('FieldStore', function () { describe('multidimensional arrays', function () { it('identifies empty 1d arrays', async function () { const doc = { a: [] }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -452,7 +403,7 @@ describe('FieldStore', function () { it('identifies populated 1d arrays', async function () { const doc = { a: [1, 2, 3] }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -472,7 +423,7 @@ describe('FieldStore', function () { ['2_1', '2_2', '2_3'], ], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -499,7 +450,7 @@ describe('FieldStore', function () { ], ], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -530,11 +481,11 @@ describe('FieldStore', function () { ['2_1', '2_2', '2_3'], ], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc1]); + await updateFieldsFromDocuments('test.test', [doc1]); // Call that matters, the one that should be kept around const doc2 = { a: [1, 2, 3] }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc2]); + await updateFieldsFromDocuments('test.test', [doc2]); expect( store.getState()[connectionInfo.id]['test.test'].fields @@ -545,7 +496,7 @@ describe('FieldStore', function () { describe('mixed nested arrays and subdocuments', function () { it('identifies 1d arrays of subdocuments', async function () { const doc = { a: [{ b: 'foo' }, { b: 'bar' }] }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -571,7 +522,7 @@ describe('FieldStore', function () { [{ b: 'foo' }, { b: 'bar' }], ], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -594,7 +545,7 @@ describe('FieldStore', function () { const doc = { a: [{ b: { c: 'foo' } }, { b: { c: 'bar' } }], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; const expected = { a: { @@ -669,7 +620,7 @@ describe('FieldStore', function () { ], }, }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(state.fields).to.be.deep.equal(expected); }); @@ -701,7 +652,7 @@ describe('FieldStore', function () { const doc = { foo1: [{ age: 10, name: 'bazillion' }], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(state.fields).to.be.deep.equal(expected); }); @@ -730,7 +681,7 @@ describe('FieldStore', function () { const doc = { foo1: [{ age: 10, path: 'bazillion' }], }; - await fieldStoreServices.updateFieldsFromDocuments('test.test', [doc]); + await updateFieldsFromDocuments('test.test', [doc]); const state = store.getState()[connectionInfo.id]['test.test']; expect(state.fields).to.be.deep.equal(expected); }); diff --git a/packages/compass-field-store/src/stores/store.ts b/packages/compass-field-store/src/stores/store.ts index 63fd9e89bf7..432eb3707c2 100644 --- a/packages/compass-field-store/src/stores/store.ts +++ b/packages/compass-field-store/src/stores/store.ts @@ -1,25 +1,26 @@ -import { createStore } from 'redux'; +import { applyMiddleware, createStore } from 'redux'; import reducer, { connectionDisconnected } from '../modules'; import { FieldStoreContext } from './context'; -import { - ConnectionsManagerEvents, - type ConnectionsManager, -} from '@mongodb-js/compass-connections/provider'; +import { type ConnectionsManager } from '@mongodb-js/compass-connections/provider'; import type { ActivateHelpers } from 'hadron-app-registry'; +import thunk from 'redux-thunk'; +import type { Logger } from '@mongodb-js/compass-logging/provider'; export function activatePlugin( _initialProps: unknown, - { connectionsManager }: { connectionsManager: ConnectionsManager }, + { + connectionsManager, + logger, + }: { connectionsManager: ConnectionsManager; logger: Logger }, { on, cleanup }: ActivateHelpers ) { - const store = createStore(reducer); - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionDisconnected, - (connectionInfoId: string) => { - store.dispatch(connectionDisconnected(connectionInfoId)); - } + const store = createStore( + reducer, + applyMiddleware(thunk.withExtraArgument({ logger })) ); + on(connectionsManager, 'disconnected', (connectionInfoId: string) => { + store.dispatch(connectionDisconnected(connectionInfoId)); + }); return { store, deactivate: cleanup, context: FieldStoreContext }; } diff --git a/packages/compass-find-in-page/package.json b/packages/compass-find-in-page/package.json index ce03a097743..e1d95a7a41b 100644 --- a/packages/compass-find-in-page/package.json +++ b/packages/compass-find-in-page/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "4.30.0", + "version": "4.30.1", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,8 +48,8 @@ }, "license": "SSPL", "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -62,7 +62,7 @@ "@types/sinon-chai": "^3.2.5", "chai": "^4.3.4", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -74,9 +74,9 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/compass-components": "^1.29.1", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", diff --git a/packages/compass-generative-ai/package.json b/packages/compass-generative-ai/package.json index 5761d2f0514..b22b96af68a 100644 --- a/packages/compass-generative-ai/package.json +++ b/packages/compass-generative-ai/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.20.0", + "version": "0.21.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -43,7 +43,6 @@ "depcheck": "compass-scripts check-peer-deps && depcheck", "check": "npm run typecheck && npm run lint && npm run depcheck", "check-ci": "npm run check", - "ai-accuracy-tests": "ts-node ./scripts/ai-accuracy-tests/ai-accuracy-tests.ts", "test": "mocha", "test-electron": "xvfb-maybe electron-mocha --no-sandbox", "test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", @@ -53,48 +52,40 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-intercom": "^0.10.0", - "@mongodb-js/compass-logging": "^1.4.3", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-intercom": "^0.11.0", + "@mongodb-js/compass-logging": "^1.4.4", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "mongodb": "^6.8.0", "mongodb-schema": "^12.2.0", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", - "@types/decomment": "^0.9.5", "@types/mocha": "^9.0.0", - "@types/node-fetch": "^2.6.11", "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", "chai": "^4.3.6", - "decomment": "^0.9.5", "depcheck": "^1.4.1", - "digest-fetch": "^2.0.3", - "@mongodb-js/shell-bson-parser": "^1.1.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-runner": "^5.6.2", - "node-fetch": "^2.7.0", "nyc": "^15.1.0", "p-queue": "^7.4.1", "prettier": "^2.7.1", "react-dom": "^17.0.2", "sinon": "^9.2.3", - "ts-node": "^10.9.1", "typescript": "^5.0.4", "xvfb-maybe": "^0.2.1" }, diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-accuracy-tests.ts b/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-accuracy-tests.ts deleted file mode 100644 index 5d35ffe7ecc..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-accuracy-tests.ts +++ /dev/null @@ -1,998 +0,0 @@ -/* eslint-disable no-console */ - -// To run these tests against cloud-dev: -// > ATLAS_PUBLIC_KEY="..." \ -// ATLAS_PRIVATE_KEY="..." \ -// AI_TESTS_ATTEMPTS_PER_TEST=100 \ -// npm run ai-accuracy-tests - -// To run these tests with local mms: -// First create an API key in your Atlas organization with -// the permissions "Organization Member". -// Then using that key run: -// > ATLAS_PUBLIC_KEY="..." \ -// ATLAS_PRIVATE_KEY="..." \ -// AI_TESTS_BACKEND=atlas-local \ -// npm run ai-accuracy-tests - -import { MongoCluster } from 'mongodb-runner'; -import os from 'os'; -import assert from 'assert'; -import ejsonShellParser from '@mongodb-js/shell-bson-parser'; -import { MongoClient } from 'mongodb'; -import { EJSON } from 'bson'; -import type { Document } from 'bson'; -import { getSimplifiedSchema } from 'mongodb-schema'; -import type { SimplifiedSchema } from 'mongodb-schema'; -import path from 'path'; -import util from 'util'; -import { execFile as callbackExecFile } from 'child_process'; -import decomment from 'decomment'; - -import { - validateAIQueryResponse, - validateAIAggregationResponse, -} from '../../src/atlas-ai-service'; -import { loadFixturesToDB } from './fixtures'; -import type { Fixtures } from './fixtures'; -import { AtlasAPI } from './ai-backend'; - -const execFile = util.promisify(callbackExecFile); - -const DEFAULT_ATTEMPTS_PER_TEST = 10; -const DEFAULT_MIN_ACCURACY = 0.8; - -const MAX_TIMEOUTS_PER_TEST = 10; - -// There are a limited amount of resources available both on the Atlas -// and on the ai service side of things, so we want to limit how many -// requests can be happening at a time. -const TESTS_TO_RUN_CONCURRENTLY = 3; - -// To avoid rate limit we also reduce the time between tests running -// when the test returns a result quickly. -const ADD_TIMEOUT_BETWEEN_TESTS_THRESHOLD_MS = 5000; -const TIMEOUT_BETWEEN_TESTS_MS = 3000; - -const monorepoRoot = path.join(__dirname, '..', '..', '..'); -const TEST_RESULTS_DB = 'test_generative_ai_accuracy_evergreen'; -const TEST_RESULTS_COL = 'evergreen_runs'; - -// p-queue has to be dynamically imported as it's ESM only. -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -let PQueue: typeof import('p-queue').default; - -const ATTEMPTS_PER_TEST = process.env.AI_TESTS_ATTEMPTS_PER_TEST - ? +process.env.AI_TESTS_ATTEMPTS_PER_TEST - : DEFAULT_ATTEMPTS_PER_TEST; - -const AI_TESTS_USE_SAMPLE_DOCS = - process.env.AI_TESTS_USE_SAMPLE_DOCS === 'true'; - -type AITestError = Error & { - errorCode?: string; - status?: number; - query?: string; - prompt?: string; - causedBy?: Error; -}; - -type TestResult = { - Type: string; - 'User Input': string; - Namespace: string; - Accuracy: number; - Pass: '✗' | '✓'; - 'Time Elapsed (MS)': number; -}; - -type QueryOptions = { - schema: SimplifiedSchema; - collectionName: string; - databaseName: string; - sampleDocuments: Document[] | undefined; - userInput: string; -}; - -const atlasBackend = new AtlasAPI(); - -function generateFindQuery(options: QueryOptions) { - return atlasBackend.fetchAtlasPrivateApi( - '/ai/api/v1/mql-query?request_id=generative_ai_accuracy_test', - { - method: 'POST', - body: JSON.stringify(options), - } - ); -} - -function generateAggregation(options: QueryOptions) { - return atlasBackend.fetchAtlasPrivateApi( - '/ai/api/v1/mql-aggregation?request_id=generative_ai_accuracy_test', - { - method: 'POST', - body: JSON.stringify(options), - } - ); -} - -const parseShellString = (shellSyntaxString?: string) => { - if (shellSyntaxString === null || shellSyntaxString === undefined) { - return shellSyntaxString; - } - - const parsed = ejsonShellParser(decomment(shellSyntaxString)); - - if (!parsed) { - throw new Error(`Failed to parse shell syntax: \n"${shellSyntaxString}"`); - } - - return parsed; -}; - -let cluster: MongoCluster; -let mongoClient: MongoClient; - -const generateMQL = async ({ - type, - databaseName, - collectionName, - userInput, - includeSampleDocuments, -}: { - type: string; - databaseName: string; - collectionName: string; - userInput: string; - includeSampleDocuments?: boolean; -}) => { - const collection = mongoClient.db(databaseName).collection(collectionName); - const schema = await getSimplifiedSchema(collection.find()); - const sample = await collection.find().limit(2).toArray(); - - if (type === 'aggregation') { - return await generateAggregation({ - schema: schema, - collectionName, - databaseName, - sampleDocuments: - includeSampleDocuments || AI_TESTS_USE_SAMPLE_DOCS ? sample : undefined, - userInput, - }); - } - - return await generateFindQuery({ - schema: schema, - collectionName, - databaseName, - sampleDocuments: - includeSampleDocuments || AI_TESTS_USE_SAMPLE_DOCS ? sample : undefined, - userInput, - }); -}; - -function hasQueryFields(query?: { - filter?: string; - project?: string; - sort?: string; - limit?: string; - skip?: string; -}) { - return ( - query?.filter || - query?.project || - query?.sort || - query?.limit || - query?.skip - ); -} - -type UsageStats = { promptTokens: number; completionTokens: number }; - -type TestOptions = { - type: string; - databaseName: string; - collectionName: string; - includeSampleDocuments?: boolean; - userInput: string; - // When supplied, this overrides the general test accuracy requirement. (0-1) - minAccuracyForTest?: number; - assertResult?: (responseContent: Document[]) => Promise | void; - acceptAggregationResponse?: boolean; -}; - -// eslint-disable-next-line complexity -const runOnce = async ( - { - type, - databaseName, - collectionName, - userInput, - includeSampleDocuments, - assertResult, - acceptAggregationResponse, - }: TestOptions, - usageStats: UsageStats[] -) => { - const response = await generateMQL({ - type, - databaseName, - collectionName, - userInput, - includeSampleDocuments, - }); - - usageStats.push({ promptTokens: 1, completionTokens: 1 }); - - try { - const collection = mongoClient.db(databaseName).collection(collectionName); - - const aggregation = response?.content?.aggregation ?? {}; - const query = response?.content?.query ?? {}; - - if (assertResult) { - let cursor; - - type === 'query' - ? validateAIQueryResponse(response) - : validateAIAggregationResponse(response); - - if ( - type === 'aggregation' || - (type === 'query' && - acceptAggregationResponse && - aggregation.pipeline && - aggregation.pipeline !== '[]' && - // When we don't have a query, we use the aggregation pipeline. - !hasQueryFields(query)) - ) { - cursor = collection.aggregate(parseShellString(aggregation?.pipeline)); - } else { - if (acceptAggregationResponse) { - throw new Error( - 'Expected aggregation response but got query or no aggregation.' - ); - } - - cursor = collection.find(parseShellString(query.filter)); - - if (query.project) { - cursor = cursor.project(parseShellString(query.project)); - } - - if (query.sort) { - cursor = cursor.sort(parseShellString(query.sort)); - } - - if (query.limit) { - cursor = cursor.limit(parseShellString(query.limit)); - } - - if (query.skip) { - cursor = cursor.skip(parseShellString(query.skip)); - } - } - - const result = (await cursor.toArray()).map((doc) => - EJSON.serialize(doc) - ); - - await assertResult(result); - } - } catch (error: unknown) { - const newError: AITestError = new Error('Inaccurate query generated'); - newError.errorCode = 'INACCURATE_QUERY_GENERATED'; - newError.query = (error as Error).message; - newError.prompt = response.prompt; - newError.causedBy = error as Error; - throw error; - } -}; - -const runTest = async (testOptions: TestOptions) => { - const usageStats: UsageStats[] = []; - const attempts = ATTEMPTS_PER_TEST; - let fails = 0; - let timeouts = 0; - let lastTestTimeMS = 0; - let totalTestTimeMS = 0; - - for (let i = 0; i < attempts; i++) { - if (timeouts >= MAX_TIMEOUTS_PER_TEST) { - throw new Error('Too many timeouts'); - } - const startTime = Date.now(); - - if ( - attempts > 0 && - lastTestTimeMS < ADD_TIMEOUT_BETWEEN_TESTS_THRESHOLD_MS - ) { - await new Promise((resolve) => - setTimeout(resolve, TIMEOUT_BETWEEN_TESTS_MS) - ); - } - - const testStartTime = Date.now(); - - try { - console.info('---------------------------------------------------'); - console.info('Running', JSON.stringify(testOptions.userInput)); - console.info('Attempt', i + 1, 'of', attempts, 'Failures:', fails); - await runOnce(testOptions, usageStats); - - console.info('OK'); - totalTestTimeMS += Date.now() - testStartTime; - } catch (e: unknown) { - totalTestTimeMS += Date.now() - testStartTime; - if ((e as AITestError).errorCode === 'GATEWAY_TIMEOUT') { - i--; - timeouts++; - - await new Promise((resolve) => setTimeout(resolve, 1000)); - } else { - console.error(e); - console.info('FAILED'); - fails++; - } - } - lastTestTimeMS = Date.now() - startTime; - } - - const accuracy = (attempts - fails) / attempts; - - return { accuracy, timeouts, totalTestTimeMS, usageStats }; -}; - -let fixtures: Fixtures = {}; - -async function setup() { - // p-queue is ESM-only in recent versions. - PQueue = (await eval(`import('p-queue')`)).default; - - cluster = await MongoCluster.start({ - tmpDir: os.tmpdir(), - topology: 'standalone', - }); - - mongoClient = new MongoClient(cluster.connectionString); - - fixtures = await loadFixturesToDB({ - mongoClient, - }); -} - -async function teardown() { - await mongoClient?.close(); - await cluster?.close(); -} - -const isDeepStrictEqualTo = (expected: unknown) => (actual: unknown) => - assert.deepStrictEqual(actual, expected); - -const isDeepStrictEqualToFixtures = - (db: string, coll: string, comparator: (document: Document) => boolean) => - (actual: unknown) => { - const expected = fixtures[db][coll].filter(comparator); - assert.deepStrictEqual(actual, expected); - }; - -const anyOf = - (assertions: ((result: unknown) => void)[]) => (actual: unknown) => { - const errors: Error[] = []; - for (const assertion of assertions) { - try { - assertion(actual); - } catch (e) { - errors.push(e as Error); - } - } - - if (errors.length === assertions.length) { - throw errors[errors.length - 1]; - } - }; - -/** - * Insert the generative ai results to a db - * so we can track how they perform overtime. - */ -async function pushResultsToDB({ - results, - anyFailed, - runTimeMS, - httpErrors, -}: { - results: TestResult[]; - anyFailed: boolean; - runTimeMS: number; - httpErrors: number; -}) { - const client = new MongoClient( - process.env.AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING || '' - ); - - try { - const database = client.db(TEST_RESULTS_DB); - const collection = database.collection(TEST_RESULTS_COL); - - const gitCommitHash = await execFile('git', ['rev-parse', 'HEAD'], { - cwd: monorepoRoot, - }); - - const doc = { - gitHash: gitCommitHash.stdout.trim(), - completedAt: new Date(), - attemptsPerTest: ATTEMPTS_PER_TEST, - anyFailed, - httpErrors, - totalRunTimeMS: runTimeMS, // Total elapsed time including timeouts to avoid rate limit. - results: results.map((result) => { - const { 'Time Elapsed (MS)': runTimeMS, Pass, ...rest } = result; - return { - runTimeMS, - Pass: Pass === '✓', - ...rest, - }; - }), - }; - - await collection.insertOne(doc); - } finally { - await client.close(); - } -} - -const tests: TestOptions[] = [ - { - type: 'query', - databaseName: 'netflix', - collectionName: 'movies', - userInput: 'find all the movies released in 1983', - assertResult: isDeepStrictEqualToFixtures( - 'netflix', - 'movies', - (doc: Document) => doc._id.$oid === '573b864df29313caabe35593' - ), - }, - { - type: 'query', - databaseName: 'netflix', - collectionName: 'movies', - userInput: - 'find three movies with alien in the title, show earliest movies first, only the _id, title and year', - assertResult: isDeepStrictEqualTo([ - { - _id: { - $oid: '573b864ef29313caabe3907a', - }, - title: "Alien 3: Collector's Edition", - year: '1992', - }, - { - _id: { - $oid: '573b864ef29313caabe39507', - }, - title: 'Alien Chaser', - year: '1996', - }, - { - _id: { - $oid: '573b864df29313caabe36f05', - }, - title: 'Alien Files', - year: '1999', - }, - ]), - }, - { - type: 'query', - databaseName: 'NYC', - collectionName: 'parking_2015', - userInput: - 'find all the violations for the violation code 21 and only return the car plate', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - _id: { - $oid: '5735040085629ed4fa8394a5', - }, - 'Plate ID': 'FPG1269', - }, - { - _id: { - $oid: '5735040085629ed4fa8394bd', - }, - 'Plate ID': 'T645263C', - }, - ]), - isDeepStrictEqualTo([ - { - 'Plate ID': 'FPG1269', - }, - { - 'Plate ID': 'T645263C', - }, - ]), - ]), - }, - { - type: 'query', - databaseName: 'berlin', - collectionName: 'cocktailbars', - userInput: - 'all the bars 10km from the berlin center, hint: use $nearSphere and $geometry', - assertResult: isDeepStrictEqualToFixtures( - 'berlin', - 'cocktailbars', - (doc: Document) => doc._id.$oid === '5ca652bf56618187558b4de3' - ), - }, - { - type: 'query', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: - 'Return all the properties of type "Hotel" and with ratings lte 70', - assertResult: isDeepStrictEqualToFixtures( - 'sample_airbnb', - 'listingsAndReviews', - (doc: Document) => doc._id === '10115921' - ), - }, - { - // Test for how we pass aggregations instead of query properties. - type: 'query', - acceptAggregationResponse: true, - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: - 'what is the bed count that occurs the most? return it in a field called bedCount', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - bedCount: 1, - }, - ]), - isDeepStrictEqualTo([ - { - _id: 1, - bedCount: 1, - }, - ]), - isDeepStrictEqualTo([ - { - _id: null, - bedCount: 1, - }, - ]), - ]), - }, - { - type: 'query', - acceptAggregationResponse: true, - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - includeSampleDocuments: true, - userInput: - 'whats the total number of reviews across all listings? return it in a field called totalReviewsOverall', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - totalReviewsOverall: 319, - }, - ]), - isDeepStrictEqualTo([ - { - _id: null, - totalReviewsOverall: 319, - }, - ]), - ]), - }, - { - type: 'query', - acceptAggregationResponse: true, - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - // This currently fails with our method of formatting arrays with documents in our prompt, - // at least with gpt-3.5-turbo. So we set the min accuracy to 0. - minAccuracyForTest: 0, - userInput: - 'which host id has the most reviews across all listings? return it in a field called hostId', - assertResult: isDeepStrictEqualTo([ - { - hostId: '16187044', - }, - ]), - }, - { - // We pass the current date to the prompt, as the training data isn't always - // up to date. This test ensures we use that data. - type: 'query', - databaseName: 'UFO', - collectionName: 'sightings', - includeSampleDocuments: true, - userInput: - 'Give me all of the documents of sightings that happened last year, no _id', - assertResult: isDeepStrictEqualTo([ - { - description: 'Flying Saucer in the sky, numerous reports.', - where: 'Oklahoma', - // Last year. - year: `${new Date().getFullYear() - 1}`, - }, - ]), - }, - - { - type: 'query', - databaseName: 'delimiters', - collectionName: 'filter', - userInput: 'get all docs where filter is true', - assertResult: isDeepStrictEqualTo([ - { - _id: '1', - filter: true, - }, - ]), - }, - { - type: 'query', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: - 'give me just the price and the first 3 amenities of the listing has "Step-free access" in its amenities.', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - _id: '10108388', - price: { - $numberDecimal: '185.00', - }, - amenities: ['TV', 'Wifi', 'Air conditioning'], - }, - ]), - isDeepStrictEqualTo([ - { - price: { - $numberDecimal: '185.00', - }, - amenities: ['TV', 'Wifi', 'Air conditioning'], - }, - ]), - ]), - }, - { - // Tests that sample documents work, as the field values are relevant - // for building the correct query. - type: 'query', - databaseName: 'NYC', - collectionName: 'parking_2015', - userInput: 'The Plate IDs of Acura vehicles registered in New York', - includeSampleDocuments: true, - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - _id: { - $oid: '5735040085629ed4fa839504', - }, - 'Plate ID': 'DRW5164', - }, - ]), - isDeepStrictEqualTo([ - { - 'Plate ID': 'DRW5164', - }, - ]), - ]), - }, - { - type: 'aggregation', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: - '¿Qué alojamiento tiene el precio más bajo? devolver el número en un campo llamado "precio"', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - precio: { - $numberDecimal: '40.00', - }, - }, - ]), - isDeepStrictEqualTo([ - { - _id: '10117617', - precio: { - $numberDecimal: '40.00', - }, - }, - ]), - isDeepStrictEqualTo([ - { - precio: 40, - }, - ]), - ]), - }, - { - type: 'aggregation', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: - 'give only me the cancellation policy and host url of the most expensive listing', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - cancellation_policy: 'moderate', - host: { - host_url: 'https://www.airbnb.com/users/show/51471538', - }, - }, - ]), - isDeepStrictEqualTo([ - { - cancellation_policy: 'moderate', - host_url: 'https://www.airbnb.com/users/show/51471538', - }, - ]), - ]), - }, - { - type: 'aggregation', - databaseName: 'netflix', - collectionName: 'movies', - userInput: 'find all the movies released in 1983', - assertResult: isDeepStrictEqualTo([ - { - _id: { - $oid: '573b864df29313caabe35593', - }, - title: 'Smokey and the Bandit Part 3', - year: '1983', - id: '168', - }, - ]), - }, - { - type: 'aggregation', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - // Test $unwind with array of documents. - // This currently fails a good amount with gpt-3.5-turbo. So we set the min accuracy to 0. - minAccuracyForTest: 0, - userInput: - 'build an array called reviewComments of all of the review comments by reviewer id 72064521.', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - reviewComments: [ - 'Our stay was fantastic. Mehmet was was excellent with communication and made us feel at home. His place is centrally located and the cafe downstairs as a nice welcoming vibe. Would recommend to stay here on a trip to Istanbul.', - ], - }, - ]), - isDeepStrictEqualTo([ - { - _id: null, - reviewComments: [ - 'Our stay was fantastic. Mehmet was was excellent with communication and made us feel at home. His place is centrally located and the cafe downstairs as a nice welcoming vibe. Would recommend to stay here on a trip to Istanbul.', - ], - }, - ]), - ]), - }, - { - type: 'aggregation', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - userInput: 'which listing has the most amenities? return only the _id', - assertResult: isDeepStrictEqualTo([ - { - _id: '10108388', - }, - ]), - }, - { - type: 'aggregation', - databaseName: 'netflix', - collectionName: 'movies', - // TODO(COMPASS-7763): GPT-4 generates better results for this input. - // When we've swapped over we can increase the accuracy for this test. - // For now it will be giving low accuracy. - minAccuracyForTest: 0.4, - userInput: - 'What are the 5 most frequent words used in movie titles in the 1980s and 1990s combined? Sorted first by frequency count then alphabetically. output fields count and word', - assertResult: isDeepStrictEqualTo([ - { - count: 3, - word: 'Alien', - }, - { - count: 2, - word: 'The', - }, - { - count: 1, - word: '3', - }, - { - count: 1, - word: '3:', - }, - { - count: 1, - word: 'A', - }, - ]), - }, - { - type: 'aggregation', - databaseName: 'sample_airbnb', - collectionName: 'listingsAndReviews', - // TODO(COMPASS-7763): GPT-4 generates better results for this input. - // When we've swapped over we can increase the accuracy for this test. - // For now it will be giving low accuracy. gpt-3.5-turbo usually tries to - // use $expr in a $project stage which is not valid syntax. - minAccuracyForTest: 0, - userInput: - 'what percentage of listings have a "Washer" in their amenities? Only consider listings with more than 2 beds. Return is as a string named "washerPercentage" like "75%", rounded to the nearest whole number.', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - _id: null, - washerPercentage: '67%', - }, - ]), - isDeepStrictEqualTo([ - { - washerPercentage: '67%', - }, - ]), - ]), - }, - - { - type: 'query', - databaseName: 'NYC', - collectionName: 'parking_2015', - // TODO(COMPASS-7763): GPT-4 generates better results for this input. - // When we've swapped over we can increase the accuracy for this test. - // For now it will be giving low accuracy. - minAccuracyForTest: 0.5, - userInput: - 'Write a query that does the following: "find all of the parking incidents that occurred on an ave (match all ways to write ave). Give me an array of all of the plate ids involved, in an object with their summons number and vehicle make and body type. Put the vehicle make and body type into lower case. No _id, sorted by the summons number lowest first.', - assertResult: anyOf([ - isDeepStrictEqualTo([ - { - 'Summons Number': { - $numberLong: '7093881087', - }, - 'Plate ID': 'FPG1269', - 'Vehicle Make': 'gmc', - 'Vehicle Body Type': 'subn', - }, - { - 'Summons Number': { - $numberLong: '7623830399', - }, - 'Plate ID': 'T645263C', - 'Vehicle Make': 'chevr', - 'Vehicle Body Type': 'subn', - }, - { - 'Summons Number': { - $numberLong: '7721537642', - }, - 'Plate ID': 'GMX1207', - 'Vehicle Make': 'honda', - 'Vehicle Body Type': '4dsd', - }, - { - 'Summons Number': { - $numberLong: '7784786281', - }, - 'Plate ID': 'DRW5164', - 'Vehicle Make': 'acura', - 'Vehicle Body Type': '4dsd', - }, - ]), - - isDeepStrictEqualTo([ - { - 'Summons Number': 7093881087, - 'Plate ID': 'FPG1269', - 'Vehicle Make': 'gmc', - 'Vehicle Body Type': 'subn', - }, - { - 'Summons Number': 7623830399, - 'Plate ID': 'T645263C', - 'Vehicle Make': 'chevr', - 'Vehicle Body Type': 'subn', - }, - { - 'Summons Number': 7721537642, - 'Plate ID': 'GMX1207', - 'Vehicle Make': 'honda', - 'Vehicle Body Type': '4dsd', - }, - { - 'Summons Number': 7784786281, - 'Plate ID': 'DRW5164', - 'Vehicle Make': 'acura', - 'Vehicle Body Type': '4dsd', - }, - ]), - ]), - }, -]; -async function main() { - try { - await setup(); - const results: TestResult[] = []; - - const startTime = Date.now(); - let anyFailed = false; - - const testPromiseQueue = new PQueue({ - concurrency: TESTS_TO_RUN_CONCURRENTLY, - }); - - tests.map((test) => - testPromiseQueue.add(async () => { - const { - accuracy, - totalTestTimeMS, - // usageStats - } = await runTest(test); - const minAccuracy = DEFAULT_MIN_ACCURACY; - const failed = accuracy < (test.minAccuracyForTest ?? minAccuracy); - - results.push({ - Type: test.type.slice(0, 1).toUpperCase(), - 'User Input': test.userInput.slice(0, 50), - Namespace: `${test.databaseName}.${test.collectionName}`, - Accuracy: accuracy, - 'Time Elapsed (MS)': totalTestTimeMS, - // 'Prompt Tokens': usageStats[0]?.promptTokens, - // 'Completion Tokens': usageStats[0]?.completionTokens, - Pass: failed ? '✗' : '✓', - }); - - anyFailed = anyFailed || failed; - }) - ); - - await testPromiseQueue.onIdle(); - - console.table(results, [ - 'Type', - 'User Input', - 'Namespace', - 'Accuracy', - 'Time Elapsed (MS)', - // 'Prompt Tokens', - // 'Completion Tokens', - 'Pass', - ]); - - if (process.env.AI_ACCURACY_RESULTS_MONGODB_CONNECTION_STRING) { - await pushResultsToDB({ - results, - anyFailed, - httpErrors: atlasBackend.httpErrors, - runTimeMS: Date.now() - startTime, - }); - } - - console.log('\nTotal HTTP errors received', atlasBackend.httpErrors); - - if (anyFailed) { - process.exit(1); - } - } finally { - await teardown(); - } -} - -void main(); diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-backend.ts b/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-backend.ts deleted file mode 100644 index e35923d3d31..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/ai-backend.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable no-console */ - -import DigestClient from 'digest-fetch'; -import nodeFetch from 'node-fetch'; - -const BACKEND = process.env.AI_TESTS_BACKEND || 'atlas-dev'; - -if (!['atlas-dev', 'atlas-local', 'compass'].includes(BACKEND)) { - throw new Error('Unknown backend'); -} - -const fetch = (() => { - if (BACKEND === 'atlas-dev' || BACKEND === 'atlas-local') { - const ATLAS_PUBLIC_KEY = process.env.ATLAS_PUBLIC_KEY; - const ATLAS_PRIVATE_KEY = process.env.ATLAS_PRIVATE_KEY; - - if (!(ATLAS_PUBLIC_KEY || ATLAS_PRIVATE_KEY)) { - throw new Error('ATLAS_PUBLIC_KEY and ATLAS_PRIVATE_KEY are required.'); - } - - const client = new DigestClient(ATLAS_PUBLIC_KEY, ATLAS_PRIVATE_KEY, { - algorithm: 'MD5', - }); - - return client.fetch.bind(client); - } - - return nodeFetch; -})() as typeof nodeFetch; - -const backendBaseUrl = - process.env.AI_TESTS_BACKEND_URL || - (BACKEND === 'atlas-dev' - ? 'https://cloud-dev.mongodb.com/api/private' - : BACKEND === 'atlas-local' - ? 'http://localhost:8080/api/private' - : 'http://localhost:8080'); - -type AITestError = Error & { - errorCode?: string; - status?: number; - query?: string; - prompt?: string; - causedBy?: Error; -}; - -export class AtlasAPI { - httpErrors = 0; - - async fetchAtlasPrivateApi( - urlPath: string, - init: Partial[1]> = {} - ) { - const url = `${backendBaseUrl}${ - urlPath.startsWith('/') ? urlPath : `/${urlPath}` - }`; - - const res = await fetch(url, { - ...init, - headers: { - ...init.headers, - 'Content-Type': 'application/json', - 'User-Agent': 'Compass AI Accuracy tests', - }, - }); - const data = await res.json(); - if (res.ok && data) { - console.info(data); - return data; - } - - const errorCode = data?.errorCode || '-'; - - const error: AITestError = new Error( - `Request failed: ${res.status} - ${res.statusText}: ${errorCode}` - ); - - error.status = res.status; - error.errorCode = errorCode; - - this.httpErrors++; - - throw error; - } -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures.ts b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures.ts deleted file mode 100644 index 1622f07f4d8..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { promises as fs } from 'fs'; -import { EJSON } from 'bson'; -import type { Document } from 'bson'; -import path from 'path'; -import type { MongoClient } from 'mongodb'; - -export type Fixtures = { - [dbName: string]: { - [colName: string]: Document /* Extended JSON javascript object. */; - }; -}; - -function getDynamicDateFixture(): { - db: string; - coll: string; - documents: Document[]; -} { - return { - db: 'UFO', - coll: 'sightings', - documents: [ - { - description: 'Flying Saucer in the sky, numerous reports.', - where: 'Oklahoma', - // Last year. - year: `${new Date().getFullYear() - 1}`, - }, - { - description: 'Alien spaceship.', - where: 'Tennessee', - year: `2005`, - }, - { - description: - 'Portal in the sky created by moving object, possibly just northern lights.', - where: 'Alaska', - year: `2020`, - }, - { - description: 'Floating pineapple, likely northern lights.', - where: 'Alaska', - year: `2021`, - }, - { - description: - 'Someone flying on a broomstick, sighters reported "It looks like Harry Potter".', - where: 'New York', - year: `2022`, - }, - ], - }; -} - -const dynamicFixtures: { - db: string; - coll: string; - documents: Document[]; -}[] = [getDynamicDateFixture()]; - -export async function loadFixturesToDB({ - mongoClient, -}: { - mongoClient: MongoClient; -}): Promise { - const fixtureFiles = ( - await fs.readdir(path.join(__dirname, 'fixtures'), 'utf-8') - ).filter((f) => f.endsWith('.json')); - - const fixtures: Fixtures = {}; - - // Load the static json fixtures. - for (const fixture of fixtureFiles) { - const fileContent = await fs.readFile( - path.join(__dirname, 'fixtures', fixture), - 'utf-8' - ); - - const [db, coll] = fixture.split('.'); - - const ejson = EJSON.parse(fileContent); - - fixtures[db] = { [coll]: EJSON.serialize(ejson.data) }; - await mongoClient.db(db).collection(coll).insertMany(ejson.data); - - if (ejson.indexes) { - for (const index of ejson.indexes) { - await mongoClient.db(db).collection(coll).createIndex(index); - } - } - } - - // Load dynamic fixtures. - for (const { db, coll, documents } of dynamicFixtures) { - fixtures[db] = { [coll]: documents }; - await mongoClient.db(db).collection(coll).insertMany(documents); - } - - return fixtures; -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/NYC.parking_2015.json b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/NYC.parking_2015.json deleted file mode 100644 index a0cee80c5b0..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/NYC.parking_2015.json +++ /dev/null @@ -1,504 +0,0 @@ -{ - "data": [ - { - "_id": { - "$oid": "5735040085629ed4fa839473" - }, - "Summons Number": { - "$numberLong": "7721537642" - }, - "Plate ID": "GMX1207", - "Registration State": "NY", - "Plate Type": "PAS", - "Issue Date": "09/18/2014", - "Violation Code": 38, - "Vehicle Body Type": "4DSD", - "Vehicle Make": "HONDA", - "Issuing Agency": "T", - "Street Code1": 8790, - "Street Code2": 17990, - "Street Code3": 18090, - "Vehicle Expiration Date": "01/01/20160202 12:00:00 PM", - "Violation Location": 115, - "Violation Precinct": 115, - "Issuer Precinct": 115, - "Issuer Code": 358644, - "Issuer Command": "T401", - "Issuer Squad": "R", - "Violation Time": "0433P", - "Time First Observed": "", - "Violation County": "Q", - "Violation In Front Of Or Opposite": "F", - "House Number": "88-22", - "Street Name": "37th Ave", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "h1", - "Violation Legal Code": "", - "Days Parking In Effect": "Y", - "From Hours In Effect": "0830A", - "To Hours In Effect": "0700P", - "Vehicle Color": "BK", - "Unregistered Vehicle?": "", - "Vehicle Year": 2013, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "16 4", - "Violation Description": "38-Failure to Display Muni Rec", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa839474" - }, - "Summons Number": { - "$numberLong": "7899927729" - }, - "Plate ID": "63543JM", - "Registration State": "NY", - "Plate Type": "COM", - "Issue Date": "01/22/2015", - "Violation Code": 14, - "Vehicle Body Type": "VAN", - "Vehicle Make": "GMC", - "Issuing Agency": "T", - "Street Code1": 34890, - "Street Code2": 10410, - "Street Code3": 10510, - "Vehicle Expiration Date": "01/01/88888888 12:00:00 PM", - "Violation Location": 18, - "Violation Precinct": 18, - "Issuer Precinct": 18, - "Issuer Code": 353508, - "Issuer Command": "T106", - "Issuer Squad": "D", - "Violation Time": "0940A", - "Time First Observed": "", - "Violation County": "NY", - "Violation In Front Of Or Opposite": "F", - "House Number": 5, - "Street Name": "W 56th St", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "c", - "Violation Legal Code": "", - "Days Parking In Effect": "YYYYYYY", - "From Hours In Effect": "", - "To Hours In Effect": "", - "Vehicle Color": "BROWN", - "Unregistered Vehicle?": "", - "Vehicle Year": 1990, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "18 6", - "Violation Description": "14-No Standing", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa839475" - }, - "Summons Number": { - "$numberLong": "7899927729" - }, - "Plate ID": "63543JM", - "Registration State": "NY", - "Plate Type": "COM", - "Issue Date": "01/22/2015", - "Violation Code": 14, - "Vehicle Body Type": "VAN", - "Vehicle Make": "GMC", - "Issuing Agency": "T", - "Street Code1": 34890, - "Street Code2": 10410, - "Street Code3": 10510, - "Vehicle Expiration Date": "01/01/88888888 12:00:00 PM", - "Violation Location": 18, - "Violation Precinct": 18, - "Issuer Precinct": 18, - "Issuer Code": 353508, - "Issuer Command": "T106", - "Issuer Squad": "D", - "Violation Time": "0940A", - "Time First Observed": "", - "Violation County": "NY", - "Violation In Front Of Or Opposite": "F", - "House Number": 5, - "Street Name": "W 56th St", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "c", - "Violation Legal Code": "", - "Days Parking In Effect": "YYYYYYY", - "From Hours In Effect": "", - "To Hours In Effect": "", - "Vehicle Color": "BROWN", - "Unregistered Vehicle?": "", - "Vehicle Year": 1990, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "18 6", - "Violation Description": "14-No Standing", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa83948c" - }, - "Summons Number": { - "$numberLong": "7845993839" - }, - "Plate ID": "61362MC", - "Registration State": "NY", - "Plate Type": "COM", - "Issue Date": "05/12/2015", - "Violation Code": 38, - "Vehicle Body Type": "VAN", - "Vehicle Make": "FORD", - "Issuing Agency": "T", - "Street Code1": 17490, - "Street Code2": 25390, - "Street Code3": 27800, - "Vehicle Expiration Date": "01/01/20161130 12:00:00 PM", - "Violation Location": 13, - "Violation Precinct": 13, - "Issuer Precinct": 13, - "Issuer Code": 355675, - "Issuer Command": "T102", - "Issuer Squad": "F", - "Violation Time": "1144A", - "Time First Observed": "", - "Violation County": "NY", - "Violation In Front Of Or Opposite": "O", - "House Number": 55, - "Street Name": "E 25th St", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "h1", - "Violation Legal Code": "", - "Days Parking In Effect": "Y", - "From Hours In Effect": "0800A", - "To Hours In Effect": "0700P", - "Vehicle Color": "WH", - "Unregistered Vehicle?": "", - "Vehicle Year": 2013, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "09 6", - "Violation Description": "38-Failure to Display Muni Rec", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa8394a5" - }, - "Summons Number": { - "$numberLong": "7093881087" - }, - "Plate ID": "FPG1269", - "Registration State": "NY", - "Plate Type": "PAS", - "Issue Date": "07/12/2014", - "Violation Code": 21, - "Vehicle Body Type": "SUBN", - "Vehicle Make": "GMC", - "Issuing Agency": "T", - "Street Code1": 31190, - "Street Code2": 36670, - "Street Code3": 36690, - "Vehicle Expiration Date": "01/01/20150721 12:00:00 PM", - "Violation Location": 30, - "Violation Precinct": 30, - "Issuer Precinct": 30, - "Issuer Code": 346020, - "Issuer Command": "T103", - "Issuer Squad": "O", - "Violation Time": "0825A", - "Time First Observed": "", - "Violation County": "NY", - "Violation In Front Of Or Opposite": "F", - "House Number": 715, - "Street Name": "St Nicholas Ave", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "d1", - "Violation Legal Code": "", - "Days Parking In Effect": "Y", - "From Hours In Effect": "0800A", - "To Hours In Effect": "0830A", - "Vehicle Color": "BK", - "Unregistered Vehicle?": "", - "Vehicle Year": 2013, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "CC3", - "Violation Description": "21-No Parking (street clean)", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa8394bd" - }, - "Summons Number": { - "$numberLong": "7623830399" - }, - "Plate ID": "T645263C", - "Registration State": "NY", - "Plate Type": "OMT", - "Issue Date": "08/06/2014", - "Violation Code": 21, - "Vehicle Body Type": "SUBN", - "Vehicle Make": "CHEVR", - "Issuing Agency": "T", - "Street Code1": 67690, - "Street Code2": 37290, - "Street Code3": 54390, - "Vehicle Expiration Date": "01/01/20141231 12:00:00 PM", - "Violation Location": 110, - "Violation Precinct": 110, - "Issuer Precinct": 110, - "Issuer Code": 341274, - "Issuer Command": "T401", - "Issuer Squad": "L", - "Violation Time": "0738A", - "Time First Observed": "", - "Violation County": "Q", - "Violation In Front Of Or Opposite": "F", - "House Number": "85-23", - "Street Name": "Whitney Ave", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "d1", - "Violation Legal Code": "", - "Days Parking In Effect": "Y", - "From Hours In Effect": "0730A", - "To Hours In Effect": "0800A", - "Vehicle Color": "BK", - "Unregistered Vehicle?": "", - "Vehicle Year": 2011, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "L 41", - "Violation Description": "21-No Parking (street clean)", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa8394cd" - }, - "Summons Number": { - "$numberLong": "7804240508" - }, - "Plate ID": "GEN6160", - "Registration State": "NY", - "Plate Type": "PAS", - "Issue Date": "06/10/2015", - "Violation Code": 71, - "Vehicle Body Type": "2DSD", - "Vehicle Make": "BMW", - "Issuing Agency": "T", - "Street Code1": 10880, - "Street Code2": 16280, - "Street Code3": 16330, - "Vehicle Expiration Date": "01/01/20160617 12:00:00 PM", - "Violation Location": 62, - "Violation Precinct": 62, - "Issuer Precinct": 62, - "Issuer Code": 361853, - "Issuer Command": "T302", - "Issuer Squad": "R", - "Violation Time": "0217P", - "Time First Observed": "", - "Violation County": "K", - "Violation In Front Of Or Opposite": "F", - "House Number": 1732, - "Street Name": "86th St", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "j6", - "Violation Legal Code": "", - "Days Parking In Effect": "YYYYYYY", - "From Hours In Effect": "", - "To Hours In Effect": "", - "Vehicle Color": "WH", - "Unregistered Vehicle?": "", - "Vehicle Year": 2009, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "06 3", - "Violation Description": "71A-Insp Sticker Expired (NYS)", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa8394e9" - }, - "Summons Number": { - "$numberLong": "7873289114" - }, - "Plate ID": "ZHH71H", - "Registration State": "NJ", - "Plate Type": "PAS", - "Issue Date": "02/17/2015", - "Violation Code": 40, - "Vehicle Body Type": "4DSD", - "Vehicle Make": "TOYOT", - "Issuing Agency": "T", - "Street Code1": 17110, - "Street Code2": 22170, - "Street Code3": 10110, - "Vehicle Expiration Date": "01/01/88888888 12:00:00 PM", - "Violation Location": 9, - "Violation Precinct": 9, - "Issuer Precinct": 9, - "Issuer Code": 333032, - "Issuer Command": "T501", - "Issuer Squad": "J", - "Violation Time": "0425P", - "Time First Observed": "", - "Violation County": "NY", - "Violation In Front Of Or Opposite": "F", - "House Number": 225, - "Street Name": "E 6th St", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "e2", - "Violation Legal Code": "", - "Days Parking In Effect": "YYYYYYY", - "From Hours In Effect": "", - "To Hours In Effect": "", - "Vehicle Color": "BLUE", - "Unregistered Vehicle?": "", - "Vehicle Year": 0, - "Meter Number": "", - "Feet From Curb": 5, - "Violation Post Code": "CC1", - "Violation Description": "40-Fire Hydrant", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa8394ec" - }, - "Summons Number": { - "$numberLong": "7280701784" - }, - "Plate ID": "BHU2569", - "Registration State": "NY", - "Plate Type": "PAS", - "Issue Date": "10/03/2014", - "Violation Code": 14, - "Vehicle Body Type": "SUBN", - "Vehicle Make": "PLYMO", - "Issuing Agency": "T", - "Street Code1": 23230, - "Street Code2": 83330, - "Street Code3": 40330, - "Vehicle Expiration Date": "01/01/20160423 12:00:00 PM", - "Violation Location": 83, - "Violation Precinct": 83, - "Issuer Precinct": 83, - "Issuer Code": 354093, - "Issuer Command": "T301", - "Issuer Squad": "O", - "Violation Time": "0427P", - "Time First Observed": "", - "Violation County": "K", - "Violation In Front Of Or Opposite": "O", - "House Number": 785, - "Street Name": "Broadway", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "c", - "Violation Legal Code": "", - "Days Parking In Effect": "YYYYYYY", - "From Hours In Effect": "0400P", - "To Hours In Effect": "0700P", - "Vehicle Color": "OTHER", - "Unregistered Vehicle?": "", - "Vehicle Year": 1998, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "C 31", - "Violation Description": "14-No Standing", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - }, - { - "_id": { - "$oid": "5735040085629ed4fa839504" - }, - "Summons Number": { - "$numberLong": "7784786281" - }, - "Plate ID": "DRW5164", - "Registration State": "NY", - "Plate Type": "PAS", - "Issue Date": "02/11/2015", - "Violation Code": 38, - "Vehicle Body Type": "4DSD", - "Vehicle Make": "ACURA", - "Issuing Agency": "T", - "Street Code1": 62590, - "Street Code2": 10440, - "Street Code3": 7490, - "Vehicle Expiration Date": "01/01/20160509 12:00:00 PM", - "Violation Location": 108, - "Violation Precinct": 108, - "Issuer Precinct": 108, - "Issuer Code": 358867, - "Issuer Command": "T401", - "Issuer Squad": "N", - "Violation Time": "0151P", - "Time First Observed": "", - "Violation County": "Q", - "Violation In Front Of Or Opposite": "F", - "House Number": "26-16", - "Street Name": "Skillman Ave", - "Intersecting Street": "", - "Date First Observed": "01/05/0001 12:00:00 PM", - "Law Section": 408, - "Sub Division": "h1", - "Violation Legal Code": "", - "Days Parking In Effect": "Y", - "From Hours In Effect": "0700A", - "To Hours In Effect": "0700P", - "Vehicle Color": "WH", - "Unregistered Vehicle?": "", - "Vehicle Year": 2006, - "Meter Number": "", - "Feet From Curb": 0, - "Violation Post Code": "W 41", - "Violation Description": "38-Failure to Display Muni Rec", - "No Standing or Stopping Violation": "", - "Hydrant Violation": "", - "Double Parking Violation": "" - } - ] -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/berlin.cocktailbars.json b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/berlin.cocktailbars.json deleted file mode 100644 index 799e34e59bd..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/berlin.cocktailbars.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "indexes": [{ "koordinaten": "2dsphere" }], - "data": [ - { - "_id": { - "$oid": "5ca652bf56618187558b4de3" - }, - "name": "Bar Zentral", - "strasse": "Lotte-Lenya-Bogen", - "hausnummer": 551, - "plz": 10623, - "webseite": "barzentralde", - "koordinaten": [13.404954, 52.520008] - }, - { - "_id": { - "$oid": "5ca6544a97aed3878f9b090f" - }, - "name": "Hefner Bar", - "strasse": "Kantstr.", - "hausnummer": 146, - "plz": 10623, - "koordinaten": [13.3213093, 42.5055506] - }, - { - "_id": { - "$oid": "5ca654ec97aed3878f9b0910" - }, - "name": "Bar am Steinplatz", - "strasse": "Steinplatz", - "hausnummer": 4, - "plz": 10623, - "webseite": "barsteinplatz.com", - "koordinaten": [13.3241804, 42.5081672] - }, - { - "_id": { - "$oid": "5ca6559e97aed3878f9b0911" - }, - "name": "Rum Trader", - "strasse": "Fasanenstr.", - "hausnummer": 40, - "plz": 10719, - "koordinaten": [13.3244667, 42.4984012] - }, - { - "_id": { - "$oid": "5ca655f597aed3878f9b0912" - }, - "name": "Stairs", - "strasse": "Uhlandstr.", - "hausnummer": 133, - "plz": 10717, - "webseite": "stairsbar-berlin.com", - "koordinaten": [13.3215159, 42.49256] - }, - { - "_id": { - "$oid": "5ca656a697aed3878f9b0913" - }, - "name": "Green Door", - "strasse": "Winterfeldtstr.", - "hausnummer": 50, - "plz": 10781, - "webseite": "greendoor.de", - "koordinaten": [13.3507105, 42.4970952] - }, - { - "_id": { - "$oid": "5ca6570597aed3878f9b0914" - }, - "name": "Mister Hu", - "strasse": "Goltzstr.", - "hausnummer": 39, - "plz": 10781, - "webseite": "misterhu.de", - "koordinaten": [13.3511185, 42.4927243] - }, - { - "_id": { - "$oid": "5ca6576f97aed3878f9b0915" - }, - "name": "Salut!", - "strasse": "Goltzstr.", - "hausnummer": 7, - "plz": 10781, - "webseite": "salut-berlin.de", - "koordinaten": [13.3513021, 42.4911044] - }, - { - "_id": { - "$oid": "5ca6581197aed3878f9b0916" - }, - "name": "Lebensstern", - "strasse": "Kurfürstenstr.", - "hausnummer": 58, - "plz": 10785, - "webseite": "lebens-stern.de", - "koordinaten": [13.3524999, 42.502059] - }, - { - "_id": { - "$oid": "5ca6588397aed3878f9b0917" - }, - "name": "Victoria Bar", - "strasse": "Potsdamer Str.", - "hausnummer": 102, - "plz": 10785, - "webseite": "victoriabar.de", - "koordinaten": [13.3616635, 42.5014176] - } - ] -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/delimiters.filter.json b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/delimiters.filter.json deleted file mode 100644 index 8a761e22c3c..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/delimiters.filter.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "data": [ - { - "_id": "1", - "filter": true - }, - { - "_id": "2", - "filter": false - } - ] -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/netflix.movies.json b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/netflix.movies.json deleted file mode 100644 index b0cac8f87b4..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/netflix.movies.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "data": [ - { - "_id": { - "$oid": "573b864df29313caabe354fc" - }, - "title": "Nature: Antarctica", - "year": "1982", - "id": "14" - }, - { - "_id": { - "$oid": "573b864df29313caabe3550d" - }, - "title": "Ferngully 2: The Magical Rescue", - "year": "2000", - "id": "35" - }, - { - "_id": { - "$oid": "573b864df29313caabe35513" - }, - "title": "Love Reinvented", - "year": "2000", - "id": "39" - }, - { - "_id": { - "$oid": "573b864df29313caabe3555b" - }, - "title": "The Eye of Vichy", - "year": "1993", - "id": "112" - }, - { - "_id": { - "$oid": "573b864df29313caabe3557a" - }, - "title": "The Tricky Master", - "year": "2000", - "id": "142" - }, - { - "_id": { - "$oid": "573b864df29313caabe35581" - }, - "title": "A Little Princess", - "year": "1995", - "id": "152" - }, - { - "_id": { - "$oid": "573b864df29313caabe35593" - }, - "title": "Smokey and the Bandit Part 3", - "year": "1983", - "id": "168" - }, - { - "_id": { - "$oid": "573b864df29313caabe355a8" - }, - "title": "Airplane II: The Sequel", - "year": "1982", - "id": "189" - }, - { - "_id": { - "$oid": "573b864df29313caabe355f1" - }, - "title": "The Big Clock", - "year": "1948", - "id": "261" - }, - { - "_id": { - "$oid": "573b864df29313caabe355fc" - }, - "title": "Female Yakuza Tale", - "year": "1973", - "id": "272" - }, - { - "_id": { - "$oid": "573b864df29313caabe3695c" - }, - "title": "Alien 3000", - "year": "2004", - "id": "5233" - }, - { - "_id": { - "$oid": "573b864ef29313caabe3907a" - }, - "title": "Alien 3: Collector's Edition", - "year": "1992", - "id": "15246" - }, - { - "_id": { - "$oid": "573b864df29313caabe37495" - }, - "title": "Alien Apocalypse", - "year": "2005", - "id": "8106" - }, - { - "_id": { - "$oid": "573b864ef29313caabe39507" - }, - "title": "Alien Chaser", - "year": "1996", - "id": "16413" - }, - { - "_id": { - "$oid": "573b864df29313caabe36f05" - }, - "title": "Alien Files", - "year": "1999", - "id": "6682" - } - ] -} diff --git a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/sample_airbnb.listingsAndReviews.json b/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/sample_airbnb.listingsAndReviews.json deleted file mode 100644 index eed826b2a7f..00000000000 --- a/packages/compass-generative-ai/scripts/ai-accuracy-tests/fixtures/sample_airbnb.listingsAndReviews.json +++ /dev/null @@ -1,4539 +0,0 @@ -{ - "data": [ - { - "_id": "10117617", - "listing_url": "https://www.airbnb.com/rooms/10117617", - "name": "A Casa Alegre é um apartamento T1.", - "summary": "Para 2 pessoas. Vista de mar a 150 mts. Prédio com 2 elevadores. Tem: - quarto com roupeiro e cama de casal (colchão magnetizado); - cozinha: placa de discos, exaustor, frigorifico, micro-ondas e torradeira; casa de banho completa; - sala e varanda.", - "space": "Foi renovado há menos de um ano. É um apartamento acolhedor e luminoso. Tem uma sala aprazível e extensível à varanda (15 m2), com uma rede brasileira para descansar e apreciar a vista do mar. A praia, a dois minutos de distancia, tem bandeira azul. Disponibiliza-se roupa de cama (lençóis, almofadas e cobertores), de banho (toalhas) e utensílios de cozinha (pratos, panelas, copos e talheres). Disponibiliza-se também sabonete e gel. A Casa Alegre está localizada numa avenida tranquila, com estacionamento à porta e em zona de comércio local (restaurantes, farmácias, cabeleireiros, mercearias, bancos, lavandaria e mercado). Está perto do centro de Vila do Conde e da Póvoa do Varzim, cidades ligadas por uma ciclovia marítima. Os percursos podem ser feitos facilmente a pé ou de bicicleta.", - "description": "Para 2 pessoas. Vista de mar a 150 mts. Prédio com 2 elevadores. Tem: - quarto com roupeiro e cama de casal (colchão magnetizado); - cozinha: placa de discos, exaustor, frigorifico, micro-ondas e torradeira; casa de banho completa; - sala e varanda. Foi renovado há menos de um ano. É um apartamento acolhedor e luminoso. Tem uma sala aprazível e extensível à varanda (15 m2), com uma rede brasileira para descansar e apreciar a vista do mar. A praia, a dois minutos de distancia, tem bandeira azul. Disponibiliza-se roupa de cama (lençóis, almofadas e cobertores), de banho (toalhas) e utensílios de cozinha (pratos, panelas, copos e talheres). Disponibiliza-se também sabonete e gel. A Casa Alegre está localizada numa avenida tranquila, com estacionamento à porta e em zona de comércio local (restaurantes, farmácias, cabeleireiros, mercearias, bancos, lavandaria e mercado). Está perto do centro de Vila do Conde e da Póvoa do Varzim, cidades ligadas por uma ciclovia marítima. Os percursos po", - "neighborhood_overview": "Vila do Conde, além de ser um centro de veraneio (praia e piscinas), é também um ativo centro cultural e turístico, devido aos monumentos que a povoam (Fortaleza de S. João, Igreja e Convento de Santa Clara, Igreja da Misericórdia e a zona histórica…), à biblioteca pública, à Casa Museu José Régio….", - "notes": "", - "transit": "Tem transportes públicos à porta (para a Póvoa e para o Metro que faz ligação ao Aeroporto Sá Carneiro e à cidade do Porto). Disponibilizo agenda com os números de telefones uteis. Há estacionamento gratuito nas proximidades do alojamento.", - "access": "Os hospedes podem aceder e usufruir do quarto, casa de banho, cozinha, sala e varanda.", - "interaction": "Terá à sua disposição um prospeto turístico da vila.", - "house_rules": "Espero que tratem a minha casa bem, respeitando os objetos que ela contem e dos quais vão usufruir. Não fumar. Não são permitidas festas. Não se aceitam animais. Não é permitido fazer barulho depois das 23 horas e ate às 8 da manha.", - "property_type": "Apartment", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "7", - "maximum_nights": "180", - "cancellation_policy": "moderate", - "last_scraped": { - "$date": "2019-02-16T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-02-16T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-04-19T04:00:00.000Z" - }, - "last_review": { - "$date": "2017-08-27T04:00:00.000Z" - }, - "accommodates": 2, - "bedrooms": 1, - "beds": 1, - "number_of_reviews": 12, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Kitchen", - "Elevator", - "Buzzer/wireless intercom", - "Heating", - "Family/kid friendly", - "Washer", - "First aid kit", - "Safety card", - "Fire extinguisher", - "Essentials", - "Shampoo", - "Hangers", - "Iron", - "Laptop friendly workspace", - "translation missing: en.hosting_amenity_49", - "Bathtub", - "Beachfront" - ], - "price": { - "$numberDecimal": "40.00" - }, - "security_deposit": { - "$numberDecimal": "250.00" - }, - "cleaning_fee": { - "$numberDecimal": "15.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "2" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/8845f3f6-9775-4c14-9486-fe0997611bda.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "51920973", - "host_url": "https://www.airbnb.com/users/show/51920973", - "host_name": "Manuela", - "host_location": "Porto, Porto District, Portugal", - "host_about": "Sou uma pessoa que gosta de viajar, conhecer museus, visitar exposições e cinema.\r\nTambém gosto de passear pelas zonas históricas das cidades e contemplar as suas edificações.", - "host_response_time": "within a day", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/bb526001-78b2-472d-9663-c3d02a27f4ce.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/bb526001-78b2-472d-9663-c3d02a27f4ce.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 1, - "host_total_listings_count": 1, - "host_verifications": [ - "email", - "phone", - "reviews", - "jumio", - "government_id" - ] - }, - "address": { - "street": "Vila do Conde, Porto, Portugal", - "suburb": "", - "government_area": "Vila do Conde", - "market": "Porto", - "country": "Portugal", - "country_code": "PT", - "location": { - "type": "Point", - "coordinates": [-8.75383, 41.3596], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 0, - "availability_60": 0, - "availability_90": 0, - "availability_365": 46 - }, - "review_scores": { - "review_scores_accuracy": 10, - "review_scores_cleanliness": 10, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 9, - "review_scores_value": 10, - "review_scores_rating": 96 - }, - "reviews": [ - { - "_id": "70611181", - "date": { - "$date": "2016-04-19T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "40950935", - "reviewer_name": "Sylvie", - "comments": "The host canceled this reservation 13 days before arrival. This is an automated posting." - }, - { - "_id": "87394500", - "date": { - "$date": "2016-07-19T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "15965521", - "reviewer_name": "Jan", - "comments": "Manuela was a great host! She and her son even picked us up from the airport. Her son travelled with us in the subway to bring us to the apartment. This was really welcoming. He showed us the apartment, which was perfectly clean and felt right away as a comfortable home. \r\nIn the fridge Manuala left us milk and her husband picked out an outstanding local port! \r\nThe ocean is less than a two minut walk and there are some great (fish) restaurants around the corner. \r\nThank you for everything Manuela, when ever we come back to Portugal we will make sure to visit your lovely apartment!" - }, - { - "_id": "90522099", - "date": { - "$date": "2016-08-01T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "84206379", - "reviewer_name": "Sílvia", - "comments": "Bom dia.... adorei o apartamento,o sítio... tudo foi bom.. a simpatia as comodidades tudo otimo... pretendo voltar a repetir. .. Obrigada por estes dias que foram maravilhosos.... atenciosamente. .. \nAté a próxima. ..." - }, - { - "_id": "96121734", - "date": { - "$date": "2016-08-21T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "64731723", - "reviewer_name": "José", - "comments": "Manuela a été très accueillante, très attentionnée. Le logement répondait à toutes nos attentes. Très bien situé avec une belle vue sur la mer, commerces et restaurants à proximité. Nous le recommandons." - }, - { - "_id": "103255360", - "date": { - "$date": "2016-09-21T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "43365931", - "reviewer_name": "Olivier", - "comments": "Très bon accueil de Manuela.Elle est venue nous chercher ,avec son mari, à l'aéroport.L'appartement est semblable aux photos et description du site,il est rustique mais très propre.Balcon avec vue sur la mer dommage qu'il ne soit pas ensoleillé.Bouteilles de vin et porto en guise de bienvenue.Manuela a également jouer le rôle de guide pendant une après-midi dans la très jolie ville de Porto, nous la remercions encore pour ces explications éducatives et sa disponibilité." - }, - { - "_id": "109748885", - "date": { - "$date": "2016-10-23T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "98907235", - "reviewer_name": "Fernando", - "comments": "Um muito obrigado a Sra Manuela pois nos recebeu com muita simpatia, estava tudo dentro das expectativas recomendo vivamente pois a paisagem é magnifica obrigado mais uma vez." - }, - { - "_id": "148849322", - "date": { - "$date": "2017-05-01T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "40974626", - "reviewer_name": "Jari", - "comments": "Hieno huoneisto lähellä merta. Kauppa ja leipomo vastapäätä. Porton metroon pääsi kätevästi vartin kävelyllä.\nJa vieressä meri!" - }, - { - "_id": "153581708", - "date": { - "$date": "2017-05-21T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "126499821", - "reviewer_name": "Vanessa", - "comments": "Apartamento bem localizado, funcional e limpo, tudo 5 estrelas. O único aspecto a melhor é a agua quente, que se escasseava rapidamente, mas nada de problemático. Realço a simpatia e disponibilidade da Manuela, que teve a amabilidade de nos deixar fazer o check-out mais tarde que o suposto. Recomendo vivamente! " - }, - { - "_id": "172479312", - "date": { - "$date": "2017-07-21T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "115494032", - "reviewer_name": "Sylvie", - "comments": "Appartement propre, tres agréable et bien situé, Manuela est très accueillante.\nSylvie, Oscar" - }, - { - "_id": "176204969", - "date": { - "$date": "2017-07-30T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "141726771", - "reviewer_name": "Sérgio", - "comments": "Recomendo vivamente este espaço tudo cinco estrelas." - }, - { - "_id": "178230560", - "date": { - "$date": "2017-08-05T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "123494194", - "reviewer_name": "Gabriela", - "comments": "Àppartement très propre fonctionnel. Bonne situation avec les commerces nécessaires pour le quotidient.\nMerci à Manuela qui est super gentille et arrangeante.." - }, - { - "_id": "187527297", - "date": { - "$date": "2017-08-27T04:00:00.000Z" - }, - "listing_id": "10117617", - "reviewer_id": "61905315", - "reviewer_name": "Mati", - "comments": "Manuela ha sido una anfitriona encantadora, nos recibió en su casa de maravilla. El piso tiene una ubicación ideal para disfrutar de vila do conde, de la playa y de oporto... Una ciudad impresionante, que conocimos gracias a las recomendaciones de manuela! Tanto guille como yo quedamos encantados y esperamos volver algún día! Gracias por todo" - } - ] - }, - { - "_id": "10108388", - "listing_url": "https://www.airbnb.com/rooms/10108388", - "name": "Sydney Hyde Park City Apartment (checkin from 6am)", - "summary": "Our city apartment is a bright, comfortable 1 bedroom with 24hr front-desk access. It is conveniently located directly across from Hyde Park and within walking distance of delightful cafes, restaurants, parks, major city attractions and public transport, including Museum Station which has a direct connection to Sydney Airport.", - "space": "SPACE Comfortable 1 bedroom which has a queen-sized bed, is air conditioned and has a self-contained kitchen, including: fridge, dishwasher, oven, cooktop, microwave.", - "description": "Our city apartment is a bright, comfortable 1 bedroom with 24hr front-desk access. It is conveniently located directly across from Hyde Park and within walking distance of delightful cafes, restaurants, parks, major city attractions and public transport, including Museum Station which has a direct connection to Sydney Airport. SPACE Comfortable 1 bedroom which has a queen-sized bed, is air conditioned and has a self-contained kitchen, including: fridge, dishwasher, oven, cooktop, microwave. SECURITY Secure 24hour front-desk access with full access to the rooftop area which has stunning views of Hydepark and the city, and includes: pool, sauna and gym. Laundry facilities, which are located on the mezzanine level and has coin operated washing machines and dryers. MY AVAILABILITY I am easily contactable at anytime. If I am unable to welcome you personally I will leave the keys to the apartment and any further information marked for your attention at the front desk. AREA Located withi", - "neighborhood_overview": "AREA Located within a vibrant and contemporary part of the city that is seeped in rich history and within walking distance of many attractions including: Hyde Park, St Mary's Cathedral, NSW Art Gallery, The Domaine, The Sydney Museum and less than a 30 minute walk to The Sydney Opera House, Sydney Harbour Bridge, Darling Harbour, Chinese Gardens, Wildlife Parks and Aquarium. SYDNEY PRIVÉ If you wish discover why Sydney is such an extraordinary place then I would highly recommend Sydney Privé, which is an exclusive concierge service that specialises in providing personalised tours and luxury experiences for the most discerning traveller. To truly experience Sydney, one must explore the local hideouts, and there is no better place to do this than in Darlinghurst. Darlinghurst is the heartbeat of the city! Checkout our airbnb Guidebook for some of our favourite hideouts. The Guidebook will provide you with website links and actual location.", - "notes": "IMPORTANT: Our apartment is privately owned and serviced. It is not part of the hotel that is operated from within the building. Internet: Our internet connection is wifi and dedicated to our apartment. So there is no sharing with other guests and no need to pay additional fees for internet usage.", - "transit": "TRANSPORT Public transport: Easily accessible with buses, trains and taxi's. Train Station: Museum Station is 250meters from the apartment, so that approximately 3 minute walk to the station. Airport Museum Station has a direct connection to/from the Sydney International and Domestic airports. The train takes approximately 15 minutes to/from the Sydney airports.", - "access": "SECURITY Secure 24hour front-desk access with full access to the rooftop area which has stunning views of Hydepark and the city, and includes: pool, sauna and gym. Laundry facilities, which are located on the mezzanine level and has coin operated washing machines and dryers.", - "interaction": "MY AVAILABILITY I am easily contactable at anytime. If I am unable to welcome you personally I will leave the keys to the apartment and any further information marked for your attention at the front desk.", - "house_rules": "GENERAL HOUSE RULES: That guests have good behaviour, are responsible for any damage caused and leave the apartment clean and tidy. That the apartment is only used for 2 guests and not to be used for parties or events. That guests are mindful of our neighbours, and adhere to safety and security rules of the building and staff. Check-in : 2PM Check-out : 10AM EARLY CHECK-IN/CHECK-OUT: If would like to change your check-in/check-out time, please let us know this information when making your booking. We will make every effort to accommodate your request, however this may not always be possible as we have the cleaners, fresh linen drops and maintenance scheduled. LOST KEYS AND SECURITY TAG: In the unlikely event that the keys and security-tag are lost or stolen, for security reasons we will need to replace the lock, keys and security-tag. For this reason there is an additional charge of $450. If you are in this unusual situation please contact me immediately so that I can ensure you", - "property_type": "Apartment", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "2", - "maximum_nights": "30", - "cancellation_policy": "moderate", - "last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-06-30T04:00:00.000Z" - }, - "last_review": { - "$date": "2019-03-06T05:00:00.000Z" - }, - "accommodates": 2, - "bedrooms": 1, - "beds": 1, - "number_of_reviews": 109, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Wifi", - "Air conditioning", - "Pool", - "Kitchen", - "Gym", - "Elevator", - "Heating", - "Washer", - "Dryer", - "Smoke detector", - "Carbon monoxide detector", - "First aid kit", - "Fire extinguisher", - "Essentials", - "Shampoo", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace", - "translation missing: en.hosting_amenity_49", - "translation missing: en.hosting_amenity_50", - "Self check-in", - "Building staff", - "Private living room", - "Hot water", - "Bed linens", - "Extra pillows and blankets", - "Microwave", - "Coffee maker", - "Refrigerator", - "Dishwasher", - "Dishes and silverware", - "Cooking basics", - "Oven", - "Stove", - "Patio or balcony", - "Cleaning before checkout", - "Step-free access", - "Flat path to front door", - "Well-lit path to entrance", - "Step-free access" - ], - "price": { - "$numberDecimal": "185.00" - }, - "security_deposit": { - "$numberDecimal": "800.00" - }, - "cleaning_fee": { - "$numberDecimal": "120.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/a2e7de4a-6349-4515-acd3-c788d6f2abcf.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "16187044", - "host_url": "https://www.airbnb.com/users/show/16187044", - "host_name": "Desireé", - "host_location": "Australia", - "host_about": "At the centre of my life is my beautiful family...home is wherever my family is.\r\n\r\nI enjoy filling my life with positive experiences and love to travel and experience new cultures, to meet new people, to read and just enjoy the beauty and wonders of the 'littlest' things in the world around me and my family. \r\n", - "host_response_time": "within an hour", - "host_thumbnail_url": "https://a0.muscache.com/im/users/16187044/profile_pic/1402737505/original.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/users/16187044/profile_pic/1402737505/original.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Darlinghurst", - "host_response_rate": 100, - "host_is_superhost": true, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 1, - "host_total_listings_count": 1, - "host_verifications": [ - "email", - "phone", - "reviews", - "jumio", - "government_id" - ] - }, - "address": { - "street": "Darlinghurst, NSW, Australia", - "suburb": "Darlinghurst", - "government_area": "Sydney", - "market": "Sydney", - "country": "Australia", - "country_code": "AU", - "location": { - "type": "Point", - "coordinates": [151.21346, -33.87603], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 5, - "availability_60": 16, - "availability_90": 35, - "availability_365": 265 - }, - "review_scores": { - "review_scores_accuracy": 10, - "review_scores_cleanliness": 10, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 10, - "review_scores_value": 10, - "review_scores_rating": 100 - }, - "reviews": [ - { - "_id": "82917761", - "date": { - "$date": "2016-06-30T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "27775510", - "reviewer_name": "Matt", - "comments": "Friendly, helpful and welcoming hosts. Beautiful and comfortable apartment. Located opposite beautiful Hyde Park and close to Sydney's main attractions and public transport. We enjoyed having Sydney on our doorstep and exploring the city. Can't wait to visit again! Thank you Desiree and Julian. " - }, - { - "_id": "96384851", - "date": { - "$date": "2016-08-22T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3577571", - "reviewer_name": "Chris", - "comments": "Desiree's Airbnb was perfectly appointed. The space is incredibly clean and welcoming. She had everything I needed for work and relaxation. She also provided helpful instructions and was easy to contact for whatever I needed. The location is great - coffee, grocery, dry cleaning, bars, shopping all within minutes. Love this location!" - }, - { - "_id": "98208829", - "date": { - "$date": "2016-08-29T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "64310199", - "reviewer_name": "Scott", - "comments": "The place is perfect for that single or couple who is on a getaway. Great location. Not to mention your Hostess is impeccable." - }, - { - "_id": "98931907", - "date": { - "$date": "2016-09-02T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "26417940", - "reviewer_name": "Jon", - "comments": "Desireé the host was very friendly and replied quickly to any questions. The apartment was a little noisy from the road outside but it wasn't a big issue. It has obviously been recently renovated and is very clean. There was tea/coffee and milk which was a convenient touch. I would recommend this Airbnb." - }, - { - "_id": "100923916", - "date": { - "$date": "2016-09-11T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "52384943", - "reviewer_name": "Hye Ryoung", - "comments": "If you are looking for room with have the best location and the most comfortable room condition, Desireé's room is just what you looking for. It's exactly same as the pictures, super clean, large enough for 1 or 2 person and you need only 3 mins walk from the Museum Station. It's also close to the mart, convenience store and easy to take a taxi. What I love the most is that it's just near the Hyde Park. It means that you could walk there every morning and night with beautiful view. And comfortable bed, clean and large shower booth, big screen TV with Samsung Smart-hub, big dinning table... everything in room was ready for your perfect trip. In addition, Desireé is really kind, good-hearted host. I couldn't meet her but she always answer kindly to my messages and give help everytime :D Don't miss this room!! " - }, - { - "_id": "101528687", - "date": { - "$date": "2016-09-13T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "13933157", - "reviewer_name": "Anne", - "comments": "Desirée is an excellent host. Quick to respond to questions and most helpful. The accommodation itself is spacious and exceedingly clean/tidy. The location is excellent - near to the Museum train station (direct connection to airport in only 15min) and a very pleasant 15min stroll, via Hyde Park, to Martin's place. I would definitely recommend the accommodation to both business travelers and tourists." - }, - { - "_id": "102302564", - "date": { - "$date": "2016-09-17T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "94739343", - "reviewer_name": "Gabriella", - "comments": "A lovely, clean and spacious apartment right on Hyde Park. We loved the apartment and the little touches - a hand written note and fresh flowers. Thanks for having us, Desiree" - }, - { - "_id": "103475158", - "date": { - "$date": "2016-09-22T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "2091404", - "reviewer_name": "Janine", - "comments": "My daughter and I came to Sydney for her medical school interview. Desiree was an outstanding host - providing detailed instructions on every stage of our stay, including our last day where she kindly offered for us to check out later than the arranged time. The apartment was immaculate and really did provide a home away from home. You could seriously just turn up at his place as everything is provided for the guest's use. Desiree thought of all the little special touches - from fresh fruit and sparkling water to chocolates and flowers. The location is fantastic - walking distance to all that Sydney has to offer. Her guide book and restaurant recommendations were wonderful. Highly recommended - better than any hotel stay! We will be back again! Many thanks Desiree" - }, - { - "_id": "104987053", - "date": { - "$date": "2016-09-29T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "33068288", - "reviewer_name": "Shane", - "comments": "Wow my best Airbnb experience by far. Great location very clean and comfortable. Desireé is a wonderful host and her apartment is just fantastic." - }, - { - "_id": "106143437", - "date": { - "$date": "2016-10-04T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "65679946", - "reviewer_name": "Mai", - "comments": "Really lovely apartment in a really convenient location. The apartment was really well equipped and beautifully maintained. Probably one of the best Airbnb locations we have ever stayed in…thank you so much for everything Desiree!!" - }, - { - "_id": "106930818", - "date": { - "$date": "2016-10-08T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "24385505", - "reviewer_name": "Alex", - "comments": "Absolutely divine! Lovely spotless apartment in a fabulous location." - }, - { - "_id": "107989861", - "date": { - "$date": "2016-10-13T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "25722890", - "reviewer_name": "Ruby", - "comments": "Everything is perfect! Location is great. House was very clean and tidy. Desireé is a very kind, helpful and responsible. Our communication are very smooth, no problem at all. It must be one of my favourite airbnb stay! Thank you for the lovely flowers and chocolates. Highly recommend to stay in Desireé 's place for your next accommodation! " - }, - { - "_id": "109494400", - "date": { - "$date": "2016-10-21T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "25179507", - "reviewer_name": "Stephen", - "comments": "Hi\nWe had such a great time staying with Desiree. The flat is perfect and centrally located. We couldn't recommend a better host.\nSteve and Lucy " - }, - { - "_id": "109997821", - "date": { - "$date": "2016-10-23T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "38919640", - "reviewer_name": "Alfie", - "comments": "What an amazing apartment! I would highly recommend this apartment! I stayed here with my partner and we had plenty of space. It was in a perfect location right across from Hyde park. I did not use the pool but the apartment does have a roof top heated pool which looked great. Apartment was clean and the bed was comfy. Desireé was a wonderful host and had some lovely touches such as stocking the fridge with beers and provided snacks, fruit and milk etc. the bathroom was also stocked with products and didn't even need to use my own shampoo etc. We also were allowed an early check in and a late check out which was just amazing. I will be staying here again on my next visit. " - }, - { - "_id": "111970224", - "date": { - "$date": "2016-11-03T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "26941228", - "reviewer_name": "Abi", - "comments": "This was a sensational experience from start to finish. Desireé was exceptionally communicative and helpful, across arrival instructions and helpful hints, to being extremely flexible with my check in and departure times from the property. She was wonderfully generous with items left in the fridge for me to help myself to, and even some lovely fresh flowers for me on arrival. It's a gorgeous apartment right in the hub of the city BUT with the bedroom double glazed I had some of the best nights sleeps here I've had in ages. Terrific stay. Best part? Uber wide screen Tv and free NETFLIX! I loved this apartment. Don't hesitate to book." - }, - { - "_id": "113807436", - "date": { - "$date": "2016-11-13T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "13556935", - "reviewer_name": "Bobbie", - "comments": "Desiree certainly knows how to make her guests feel welcome. Fresh flowers and chocolates on arrival is an example. Her attention to detail was very much appreciated and one of my best airbnb experiences to date. She was also quick to respond to all queries. The apartment is as shown in the photos and immaculately clean. Dont hesitate to stay here!" - }, - { - "_id": "114350117", - "date": { - "$date": "2016-11-17T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "13715525", - "reviewer_name": "Mariano", - "comments": "Todo ha sido perfecto! La comodidad del apartamento es aún mejor en vivo que en las fotos. Los detalles y amabilidad de Desiree son grandiosos. Sin duda aconsejo por ubicación y comodidades este apartamento para conocer la preciosa Sydney es perfecto!" - }, - { - "_id": "115329842", - "date": { - "$date": "2016-11-23T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "93269084", - "reviewer_name": "Hilda", - "comments": "Our host Desiree is fantastic she had everything ready for us. It was so easy we just turned the key and everything was there. She kept in contact with me to make sure everything was ok which was very thoughtful. There was fresh roses in the apartment really lovely touch also lots of groceries like tea bags, coffee, milk etc. which was great after a long flight. There are even bath robes and lovely fluffy towels The apartment is beautiful very luxurious, we loved it. It's spotlessly clean, great TV, WIFI, everything you need and more. It's a great location. Very easy to walk around from and also Museum station is right on the door step. It was our first visit to Sydney and should we go back I would love to stay here again. I can't thank Desiree enough. We were absolutely thrilled with it. Truly 5 star." - }, - { - "_id": "115751437", - "date": { - "$date": "2016-11-26T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "64381954", - "reviewer_name": "Reed", - "comments": "A lovely, comfortable, well decorated apartment in a great location. Very friendly host. I can't wait to stay again. " - }, - { - "_id": "117101900", - "date": { - "$date": "2016-12-04T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "90320689", - "reviewer_name": "Andrew", - "comments": "Where do we begin? Desireé is a wonderfully thoughtful host that made our stay so special. The apartment looks exactly as pictured - modern, comfortable and clean. The location is fabulous and central to everything! This apartment does not disappoint! Our only regret is that we didn't stay for longer. We will certainly be back the next time we are in Sydney! Thank you Desireé!" - }, - { - "_id": "120347989", - "date": { - "$date": "2016-12-09T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3950204", - "reviewer_name": "Judith", - "comments": "My parents loved Desirée's flat. She had left a hand written note and ferrero rocher as well as beautiful, fresh flowers to make it a special welcome. A perfectly stocked kitchen and tasty tea made it really homely. It was also spotless clean and the African styled furnishings gave it a memorable touch. The house has friendly reception staff and they were helpful with giving directions. The location is sublime as it overlooks Hyde Park which is really close to both parts of the city centre. We highly recommend the flat and would happily stay again in the future. Thanks!" - }, - { - "_id": "121158621", - "date": { - "$date": "2016-12-14T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "13736624", - "reviewer_name": "Laura", - "comments": "My stay at Desireé's apartment was one of my best experiences yet! She's a phenomenal host and the apartment has everything a traveler could need. It's really a luxury stay. Loved it!!" - }, - { - "_id": "125435935", - "date": { - "$date": "2017-01-05T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "5524726", - "reviewer_name": "Vanessa", - "comments": "Desiree was a fantastic host with a lovely apartment in a great location in Sydney! \n" - }, - { - "_id": "126703197", - "date": { - "$date": "2017-01-13T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "52608629", - "reviewer_name": "Christopher", - "comments": "Centrally located, comfortable, 4 star quality, beautifully decorated apartment with everything you could need including a lovely view of Hyde Park. We thoroughly enjoyed our stay and are already looking forward to staying here again. Desireé was helpful and responsive. We miss it already. " - }, - { - "_id": "127752669", - "date": { - "$date": "2017-01-20T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "27669736", - "reviewer_name": "Aika", - "comments": "Desiree's place was beyond our expectations!! The interior of the home was very clean and modern. The location was perfect for exploring the city, and communication with Desiree was always very clear with quick response. If we ever go back to Sydney we will definitely book with her again!! " - }, - { - "_id": "129263649", - "date": { - "$date": "2017-01-29T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "43886564", - "reviewer_name": "Clara", - "comments": "The apartment is really well located, a stunning view from the rooftop pool on the Anzac Memorial and the Sydney eye tower. Desireé is helpful and kind! \nA really good address!" - }, - { - "_id": "129841221", - "date": { - "$date": "2017-02-02T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "111460786", - "reviewer_name": "Katalin", - "comments": "Loved staying at Your place, great location, 20 min walk to Opera House, close to great shopping and dining area...Thank You for the chocolate and the flowers!!!" - }, - { - "_id": "130599209", - "date": { - "$date": "2017-02-06T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "102765936", - "reviewer_name": "Deborah", - "comments": "Loved this apartment. It is located across from beautiful Hyde Park. You can easily walk to the botanical gardens, many museums and art galleries, the opera house and much more. The apartment itself is beautiful, nicely furnished, has all the amenities you would want including coffee, tea, milk, cookies, fruit and more. There is a convenience store right next to it. Desiree could not have been warmer or more helpful. She was available any time we had a question. We already asked to book another stay and we will recommend her place to friends." - }, - { - "_id": "131930618", - "date": { - "$date": "2017-02-14T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "80245144", - "reviewer_name": "Marie", - "comments": "This is a very nice condo, location was awesome. We walked everywhere and went to some of the restaurants Desiree recommended, we were not disappointed. Had a great stay and most certainly would love to do it again. " - }, - { - "_id": "132947447", - "date": { - "$date": "2017-02-19T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "44434344", - "reviewer_name": "Mike", - "comments": "The apartment was perfectly located and very lovely. It's just as nice as the pics, and Desiree was the perfect host. The neighborhood is wonderful, and the guidebook Desiree provided was very useful. Definitely go to Messina at least once for gelato (we went twice). We also loved the ARTERY gallery, and bought some nice gifts for friends back home and even a cool painting that I could carry on to the plane. Overall a great experience and a perfect spot to end our trip to Australia in style." - }, - { - "_id": "134556522", - "date": { - "$date": "2017-02-27T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "45516629", - "reviewer_name": "Ines", - "comments": "The appartment is very confortable, very clean and new. It is like it's shown no pictures. Desiree was really helpful and left us the basic stuff for everyday needs, such us coffee, tea, sugar and bathroom supplies. The location is convenient, easy to get from the airport and with lots of buses stops, in a nice area. We had a great time! " - }, - { - "_id": "135708219", - "date": { - "$date": "2017-03-05T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "15755469", - "reviewer_name": "Arbel", - "comments": "This is best place to stay in Sydney! Just few steps to the Hyde Park. Right in the central area which is easy to travel,exploring and food hunting.\nWhen we arrived this beautiful apartment. Everything you see make you feel like home. Because we were there for the Sydney gay and lesbian mardi gras. You can watch the parade at the balcony (Best view ever).\nAnd we thank you for the host making our trip in Sydney unforgettable." - }, - { - "_id": "136291613", - "date": { - "$date": "2017-03-09T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "103339406", - "reviewer_name": "Mirko", - "comments": "Best apartment ever, super central in the heart of Sydney, 100% recommended, check it out!" - }, - { - "_id": "136642373", - "date": { - "$date": "2017-03-11T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "92727298", - "reviewer_name": "Sarah", - "comments": "My husband and I had a wonderful time staying in Desireé's beautiful apartment. Would definitely stay again." - }, - { - "_id": "138848087", - "date": { - "$date": "2017-03-21T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "18197139", - "reviewer_name": "Richard", - "comments": "This is a lovely apartment, in a hotel/serviced apartment building. For once the pictures really don't do it justice. The hotel rooms get marked down for being dated in reviews on hotel sites, but this apartment certainly isn't. It's centrally located: it has Darlinghurst's little strip of restaurants just around the corner, and central Sydney equally close in the other direction. I was here for a week in March and it (unusually) rained every day, so the 60\" UHD Samsung smart TV with Netflix and Stan (Aussie streaming), as well as Apple TV, was particularly welcome, as was the small gym on the roof. The pool was less useful, but would be great normally. Desireé is a very friendly host too. Highly recommended." - }, - { - "_id": "139320556", - "date": { - "$date": "2017-03-24T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3774958", - "reviewer_name": "Ishara", - "comments": "Desireé’s apartment is the perfect place to explore Sydney city from. Perfectly central to all city sights. The apartment was super clean and neat. It hosts beautiful city views, a quick 5 minute walk to Museum rail station, and a hop to great restaurants and nightlife. The apartment is tastefully decorated with art and books from adventurous travels; furnished and accessorized with absolutely everything that I could require, plus more. I was impressed at the Desireé's thoughtfulness in accommodating her guests needs. Such things as a kind welcome note. Petite chocolates, mini fresh milk packs in the fridge, expresso machine and every conceivable toiletry item one could need inside a bathroom cabinet. Discreetly rolled up extension cords so you never have to look for one to reach the wall socket to charge your digital devices. The bed was comfortable, where sprigs of fragrant lavender greeted us laid upon freshly rolled up towels. We loved our stay and highly recommend Desireé as a host. She communicated well, was always polite and prompt with messaging all required information upon booking. There's no doubt why Desireé has been voted as a super host. You will be delighted." - }, - { - "_id": "142140419", - "date": { - "$date": "2017-04-06T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "119185319", - "reviewer_name": "Annamarie", - "comments": "This is a wonderful place to stay comfortable and clean easy access and very close to Museum Station and beautiful Hyde Park. The host is marvelous and provided us with so many wonderful amenities. We would definitely stay here again ." - }, - { - "_id": "144173346", - "date": { - "$date": "2017-04-15T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "8036583", - "reviewer_name": "Corey", - "comments": "Desireé is a wonderful host. She's easy to contact and responds promptly when you send her an email. Her place is very comfortable and clean. She let us arrive early and we had no problem staying an extra night. We really appreciated that!! Transport to the city and surrounding areas are very close to her place. Great place to walk around lots to see and do. I would definitely stay again :)" - }, - { - "_id": "145595722", - "date": { - "$date": "2017-04-19T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "76881567", - "reviewer_name": "Fay", - "comments": "Lovely, tastefully designed and very clean apartment in a great location. We had high expectations having read the rave reviews and it certainly did not disappoint. " - }, - { - "_id": "147074725", - "date": { - "$date": "2017-04-24T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "105278846", - "reviewer_name": "Helen", - "comments": "Desiree was the perfect host. The apartment is just as it is down on the airbnb site. Desiree answered our many queries and requests super-quickly and she is very approachable and friendly. The location is really close to all the CBD attractions and public transport. Desiree added so many extras like a bottle of wine and chocolates to help us celebrate our milestone anniversary. We would highly recommend this listing. Thanks Desiree!\n" - }, - { - "_id": "149358540", - "date": { - "$date": "2017-05-03T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "6025551", - "reviewer_name": "Robert", - "comments": "This apartment is a very nice good place to stay, its in a secure building, The apartment is spotless clean and very well maintained, very comfortable, kitchen is well prepared with stove, oven, fridge, microwave, coffeemaker, watercooker, dishwasser, enough pots and pans etc. Livingroom has a comfortable sofa, salon table, mega big TV with many channels, dining table with 6 chairs, extra lounger, two small balconies, one which has 2 chairs and a table. Bedroom with large bed, desk and big closet, bathroom with sink, shower and toilet. The host had many soaps, shampoos, drinks, coffee, wine and bottled water available for us to use, which was very kind and nice ( Im not sure if this is always there) The host replied always quick to any questions we had. We stayed 8 nights and we will return! Thanks Desiree and Julian!" - }, - { - "_id": "151255323", - "date": { - "$date": "2017-05-12T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "747245", - "reviewer_name": "Lisa", - "comments": "Fabulous apartment in the centre of Sydney, we'll appointed and great facilities, pool, gym and pay laundry downstairs. Apartment is just like the photos. Communication with Desiree was fabulous and nothing was a problem. Nice personal touches when we arrived. A great place to stay " - }, - { - "_id": "152506693", - "date": { - "$date": "2017-05-16T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "149324", - "reviewer_name": "Valerie", - "comments": "Fantastic! Desireé made the entire stay flawless - sent detailed arrival info days before, had keys waiting for us at the front desk, a bottle of wine waiting upon arrival, and a spotless apartment with all possible amenities. Great home base for a vacation. Public transit just across the street, restaurants are within 10 minutes walk, and a taxi stand is available right next door. Highly recommend. " - }, - { - "_id": "155259011", - "date": { - "$date": "2017-05-28T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "49564841", - "reviewer_name": "Dale", - "comments": "Very tidy, modern one bed apartment opposite Hyde Park. The location was superb for transportation, local cafes, local cafes and restaurants. Great Wifi, safe and secure. The host Desiree is very helpful and has a lot of extra bits and pieces to make stay more comfortable. Would stay again. " - }, - { - "_id": "158253735", - "date": { - "$date": "2017-06-05T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "42066890", - "reviewer_name": "Jessica", - "comments": "We had a wonderful stay in Desiree's apartment. Great location, spotlessly clean and very homely. Would definitely stay again." - }, - { - "_id": "159856310", - "date": { - "$date": "2017-06-11T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "52471153", - "reviewer_name": "Kathy", - "comments": "If you are heading to Sydney and want a FANTASTIC place that is central and within walking distance to everywhere, especially Darling Harbour, then please stay at Desiree's apartment. There is everything you could want close by, especially restaurants and public transport, the airport train is just a few meters away. The apartment has everything you could possibly need and more. Desiree's interest and love of people shines through in everything she does; she goes way beyond being a superhost. Thank you Desiree for making our stay so special and we can't wait to come back, hopefully when the weather is a little kinder." - }, - { - "_id": "162441686", - "date": { - "$date": "2017-06-21T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "128392930", - "reviewer_name": "Mara", - "comments": "Great location! Everything is in walking distance and the supermarket is just around the corner. The hosts are always available and very helpful. The apartment is very clean and safe. Would definitely stay here again!" - }, - { - "_id": "164591689", - "date": { - "$date": "2017-06-28T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "28341665", - "reviewer_name": "Amy", - "comments": "Perfect central apartment, walkable to everything you need and very close to public transportation options as well. Desireé was an absolutely fantastic host, stocking the kitchen with the necessities and even a bottle of wine! She communicated everything up front very clearly and had great recommendations in her guidebook. We were very comfortable in the apartment for our stay and would highly recommend to anyone looking to explore the Sydney area! " - }, - { - "_id": "166900440", - "date": { - "$date": "2017-07-05T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "69981299", - "reviewer_name": "Thao", - "comments": "Desiree and Julian were wonderful hosts. The listing was beautifully decorated. Would absolutely love to stay here again during our next trip to Sydney." - }, - { - "_id": "168686088", - "date": { - "$date": "2017-07-10T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "23929627", - "reviewer_name": "Angus", - "comments": "This is a lovely apartment for a couple. Well fitted out, a few essentials for eating in and decorated with a very tasteful African theme. During the cool winter evenings, the reverse cycle heaters were fantastic. Traffic noise was well muted by the double glass in the bedroom. Desireé was very communicative through our stay and had provided lots of info about Sydney and what to do, where to eat, etc. " - }, - { - "_id": "170556011", - "date": { - "$date": "2017-07-16T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "9235106", - "reviewer_name": "Ric", - "comments": "This is a little treasure in the best part of Sydney! Desiree was a great communicator, checking in with us all through our visit to make sure all was good - and it was great! Lovely sized apartment, beautifully appointed and fully of little things to make our stay memorable. " - }, - { - "_id": "189500335", - "date": { - "$date": "2017-09-02T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "68882132", - "reviewer_name": "Graeme", - "comments": "We spent a very comfortable 8 nights in Desiree's apartment. It is just as it looks in the pictures. It is smartly decorated with comfortable furniture and lovely modern kitchen and bathroom. There is a guest laundry in the building with coin operated machines and you can do a big wash and dry for $4 each. The apartment is bright and airy and has a nice busy city outlook which we enjoyed. You can certainly hear the buzz of the city around you but you are right in the CBD so that is to be expected and enjoyed. The location is fantastic. You can walk to just about everything in the city centre and trains and buses are only metres away if you want to get around more quickly or go further afield. Very easy access from the airport by train into Museum station. The host, Desiree, was a pleasure to deal with, excellent communication all the way through. This is a wonderful little home away from home putting you right into centre of the Sydney action." - }, - { - "_id": "192708777", - "date": { - "$date": "2017-09-11T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "23683916", - "reviewer_name": "Mario", - "comments": "Eine sehr Zentrum nah gelegene Wohnung. Absolut empfehlenswert um Sydney zu entdecken. Man kann optimal zu Fuß in die Innenstadt und läuft nur kurz durch den Hyde Park. Zum Flughafen sind es auch nur 15 Minuten." - }, - { - "_id": "193259899", - "date": { - "$date": "2017-09-13T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "13933157", - "reviewer_name": "Anne", - "comments": "Enjoyable repeat stay." - }, - { - "_id": "195805380", - "date": { - "$date": "2017-09-21T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "28422683", - "reviewer_name": "Shiya", - "comments": "We had a great time here. The host is so generous and thoughtful. One of the best places I've stayed! Highly recommend!!!" - }, - { - "_id": "197225880", - "date": { - "$date": "2017-09-25T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "6394358", - "reviewer_name": "Nolan", - "comments": "Desirees place was awesome. The room was very clean, internet speed was sufficient, and all the goodies that she provides in the room ( extra toiletries, snacks, and even wine in the kitchen and refrigerator were a great bonus. The location of the condo was very convenient to get around Sydney. Lots of restaurants and train stations nearby !!! Desiree was also very quick at responding to our needs and questions . Overall, 5 out of 5 stars for us !!!" - }, - { - "_id": "199412050", - "date": { - "$date": "2017-10-01T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "74924314", - "reviewer_name": "Ivy", - "comments": "The apartment has great convenience to everything. It suits for tourist. Desiree is helpful and warm hearted." - }, - { - "_id": "203026080", - "date": { - "$date": "2017-10-13T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "45829604", - "reviewer_name": "Karen", - "comments": "Fab location near Museum train station. Apartment sparkling clean and so many extras e.g butter, beer, wine, bottled water, cereal etc that were much appreciated. Would definitely recommend this apartment." - }, - { - "_id": "206530009", - "date": { - "$date": "2017-10-26T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "76637502", - "reviewer_name": "Janet", - "comments": "Desiree is an amazing host and had the perfect space for us! It started from wonderful communication to easy early check in. It continued with an immaculate unit that provided us an oasis from our sight seeing. We use Airbnb quite a bit and especially enjoy being able to have a kitchen area for a quick breakfast or meal and morning coffee. The unit was close to grocery, public transport and amenities were as if I had lived there. We were there to celebrate my spouse's 50th birthday and were pleasantly surprised by the nice and thoughtful birthday card. Thank you again and we had a blast!" - }, - { - "_id": "207795654", - "date": { - "$date": "2017-10-30T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3838105", - "reviewer_name": "Benedicte Et Remi", - "comments": "The apartment is perfectly located, easy access and walking distance to all attractions. Spotless clean, well appointed and really comfy, the seperate bedroom was great especialy when you are traveling with a baby !\nThanks again for our great stay :)" - }, - { - "_id": "209222000", - "date": { - "$date": "2017-11-04T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "59949023", - "reviewer_name": "Sue", - "comments": "A super place to stay - well appointed, beautifully furnished with great facilities within the building. Going to Sydney contact Desiree.\n\nSue Hillman" - }, - { - "_id": "212046808", - "date": { - "$date": "2017-11-15T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "2325365", - "reviewer_name": "Riett", - "comments": "The apartment was absolutely as expected. Clean and modern. Lots of great restaurants and shopping close by." - }, - { - "_id": "212654397", - "date": { - "$date": "2017-11-18T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "28203124", - "reviewer_name": "Sofia", - "comments": "Beautiful apartment, clean, modern, nicely decorated. Centrally located, walking distance from everything we wanted to see. Two balconies with awesome views of Hyde Park and Oxford St. We really felt like we were living the Sydney high life. Plenty of towels, bedding, etc, there was even a bunch of shampoo/conditioner/soap to use and coffee, juice and chocolate for us as well. Desiree’s apartment really felt like a home away from home, will definitely visit again next time we’re in Sydney!" - }, - { - "_id": "213745185", - "date": { - "$date": "2017-11-22T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "5628912", - "reviewer_name": "Sheila", - "comments": "Desireé’s place is extremely well located, superbly comfortable and sparklingly clean. We would love to stay here on every future visit to Sydney." - }, - { - "_id": "217552448", - "date": { - "$date": "2017-12-09T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "7764765", - "reviewer_name": "Rachael", - "comments": "This space is great. The size is wonderful for a couple, anything you could need was available. Desireé was so easy to work with. We asked for special accommodations for checking in and out and Desireé was swift in reply as well as considerate in communications! The space is even nicer than it appears in the photos and the location is very convenient to so many attractions." - }, - { - "_id": "219571237", - "date": { - "$date": "2017-12-18T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "138194849", - "reviewer_name": "Tom", - "comments": "This is an amazing place! LOVED the amenities of coffee, milk, wine bottle, chocolate bar , great TV, lots and lots of towels, and wonderful location. We didn't use the rooftop pool, but we should have (busy visiting daughter). Could not have been happier with this choice. Thank you Desiree." - }, - { - "_id": "223852780", - "date": { - "$date": "2018-01-01T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "25827160", - "reviewer_name": "Mildred", - "comments": "Super great stay here!" - }, - { - "_id": "226285311", - "date": { - "$date": "2018-01-09T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "18997240", - "reviewer_name": "Colleen", - "comments": "This is the perfect location. Just a block from a great transit connection but walkable to the main sites too. \n\nIt’s easy to get anywhere. We took the bus to Coogee and Bondi beaches, to Sydney Fish Market, to Opera House and ferry hub of Circular Quay for the ferry to Manly. You are also on subway routes too!\n\nThe home is quiet even though the street and park are well used. It feels super safe to walk in.\n\nThere are great grocery and restaurant options right here...and we were hooked on Greenhouse Coffee just a few blocks down to start our day. We enjoyed the Sydney Festival and walked to their events and free activities. \n\nThe home had lots of lovely touches and thoughtful additions in the bath, the kitchen and a nice collection of books and info." - }, - { - "_id": "229094652", - "date": { - "$date": "2018-01-21T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "5827353", - "reviewer_name": "Zsófi", - "comments": "Very nice, stylish place, close to everything, in quiet environment. Check in was easy." - }, - { - "_id": "230992031", - "date": { - "$date": "2018-01-29T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "4833356", - "reviewer_name": "Stephen", - "comments": "Stylish decor—thoughtful touches—great communication—\\0perfect location!" - }, - { - "_id": "232715698", - "date": { - "$date": "2018-02-06T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "12104740", - "reviewer_name": "Patrizia", - "comments": "This was our first experience with Airbnb and we have been very lucky to choose Desiree as a host. The apartment is lovely, very clean, confortable and well appointed. The size is perfect for two people and the location is very convenient, in front of Hide Park, easy to be reached by the airport via irport likn train that stops just few steps away. All the main city attractions are at walking distance. Desiree is very kind and supporting. You can easily reach her at any time via e-mail and she answers in a few minutes. Highly recommended!" - }, - { - "_id": "234475159", - "date": { - "$date": "2018-02-12T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "1964328", - "reviewer_name": "Tom", - "comments": "Great flat in the heart of Sydney. Photos and description exactly as advertised, and host communication was second-to-none! Would recommend if you want to be able to walk anywhere centrally" - }, - { - "_id": "236285249", - "date": { - "$date": "2018-02-18T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "156098679", - "reviewer_name": "Fiona", - "comments": "This place is really good!! It has convenient location, basically you can get everything you need within 10 minuets-walk. The hosts are super kind and thoughtful. Highly recommend!" - }, - { - "_id": "237409675", - "date": { - "$date": "2018-02-23T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "111305679", - "reviewer_name": "Ben", - "comments": "Amazing location, loved the area and easy walk to all major sites. Very nice unit and amazing value." - }, - { - "_id": "240485706", - "date": { - "$date": "2018-03-04T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "165017028", - "reviewer_name": "Mark", - "comments": "Desiree earns a big tick for hospitality and thoughtfulness. The apartment was exactly as described and couldn't have better suited a weekend Mardi Gras getaway.\n\nWe'd definitely recommend this place to others and can't rate Desiree's communication and responsiveness highly enough. The apartment was spotless, had all the mod cons you could wish for, was convenient to transport and perfectly located with awesome views." - }, - { - "_id": "241889288", - "date": { - "$date": "2018-03-10T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "5485604", - "reviewer_name": "Amy", - "comments": "Great communication and thoughtful touches! Will definitely stay together" - }, - { - "_id": "255531356", - "date": { - "$date": "2018-04-21T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3972289", - "reviewer_name": "Vivian", - "comments": "Amazing location, the apartment was exactly how it described. Desiree was very responsive and check in was easy. Highly recommended!" - }, - { - "_id": "260153531", - "date": { - "$date": "2018-05-03T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "43200108", - "reviewer_name": "Miguel", - "comments": "Beautiful view, gorgeous apartment, and perfect location. Would definitely stay here again the next time I’m in Sydney! Thanks for the wonderful stay, Desireé!" - }, - { - "_id": "264818731", - "date": { - "$date": "2018-05-14T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "6904660", - "reviewer_name": "Josee-Lina", - "comments": "You will feel at home at Desireé's apartment. Beautifully designed, it is luminous, cosy and perfectly situated in front of Hyde Park; the Museum Station is just a few steps away and many bus stops are just downstairs. It' s easy to go anywhere you wish, thanks to the perfect location.\n\nAnd Desireé is a real superhost: Quick to answer my inquiries, she gave me valuable advice during my stay.\n\n I recommend strongly this apartment: You will enjoy even more your stay in Sydney (and won't want to leave...). Many thanks Desireé!" - }, - { - "_id": "266303914", - "date": { - "$date": "2018-05-20T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "34136908", - "reviewer_name": "Katherine", - "comments": "Desireé and Julian’s apartment was a perfect place to explore Sydney from - and delicious Darlinghurst restaurants nearby. They had thought of everything, and we really appreciated the homely touches. The bed is very comfy, there is plenty of wardrobe space, and a comfy couch when a night in is in order. Despite the location right near Hyde Park, it is very quiet and we were not bothered by any noise during our stay. Desireé and Julian were lovely hosts and we appreciated the clear directions and easy check-in and check-out. We would love to stay here again when we’re in Sydney again!" - }, - { - "_id": "269768479", - "date": { - "$date": "2018-05-28T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "19652047", - "reviewer_name": "Tiffany", - "comments": "I have stayed at Desiree’s place twice now as I travel for work and it’s one of my favourite Sydney accomodation venues. It’s is absolutely spotless, secure, quiet and the most convenient location. Desiree is an excellent and attentive host - this place deserves every great review it gets and more" - }, - { - "_id": "277654395", - "date": { - "$date": "2018-06-17T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "5104154", - "reviewer_name": "Juliet", - "comments": "Desiree's place was very comfortable & was perfectly located for my trip to Sydney. I came for the Sydney Film Festival, and the apartment was well located on Hyde Par, so I could walk to and from the venue. The studio was very comfortable and well stocked too - the little things such as tea, milk, spreads for breakfast etc were thoughtful. Picking up and dropping off the keys were a breeze and whilst I did not meet my host, she was very responsive and helpful. I can highly recommend this place for a city based jaunt. Thanks again Desiree" - }, - { - "_id": "281469121", - "date": { - "$date": "2018-06-25T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "34792630", - "reviewer_name": "Rebecca", - "comments": "Really lovely apartment in a fantastic location. The double glazing in the bedroom and super comfy bed meant we slept like a log, and the rest of the apartment is stylish and very comfortable. As per previous reviews, Desiree's thoughtful touches, excellent communication and brilliant local recommendations really shines and elevates the whole experience to something very special. We stayed with our toddler, and the building/apartment has excellent pram accessibility. " - }, - { - "_id": "288493445", - "date": { - "$date": "2018-07-10T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "11760311", - "reviewer_name": "João", - "comments": "Great apartment in an excellent locations, close to public transportation and countless cafes and dinner spots. Desiree was always super quick to reply and available at all times to meet our requests. We would definitely stay here again if returning to Sydney!" - }, - { - "_id": "295146430", - "date": { - "$date": "2018-07-23T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "170487787", - "reviewer_name": "Terry", - "comments": "A great convenient location to stay for visitors to Sydney with a great host." - }, - { - "_id": "299898709", - "date": { - "$date": "2018-08-01T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "96752801", - "reviewer_name": "Ross", - "comments": "Great location and a very nicely presented apartment." - }, - { - "_id": "305125435", - "date": { - "$date": "2018-08-10T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "155037158", - "reviewer_name": "Bronwyn", - "comments": "Such a great apartment, super host and fantastic holiday!!! Keeps in contact and replies to messages super fast" - }, - { - "_id": "312136096", - "date": { - "$date": "2018-08-23T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "8905686", - "reviewer_name": "Sean", - "comments": "Desireé’s apartment is great! It's clean, well lit, comfortable, and located in a great part of Sydney. Desireé is a great host and an excellent communicator. I would love to stay here again! " - }, - { - "_id": "313702518", - "date": { - "$date": "2018-08-26T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "11826533", - "reviewer_name": "Zunilka", - "comments": "Do yourself a favour, if this place is free, book it!! Stunning. Thanks so much" - }, - { - "_id": "319246488", - "date": { - "$date": "2018-09-06T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "67048231", - "reviewer_name": "Helen", - "comments": "Wonderful base for my visiting parents. Desireé was so helpful and conscientious and I can’t recommend her apartment enough." - }, - { - "_id": "324819945", - "date": { - "$date": "2018-09-18T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "67048231", - "reviewer_name": "Helen", - "comments": "Desireé was the perfect host for my parents on their return to Sydney after staying in her apartment the previous week. She was fast to respond to queries and the apartment had lots of thoughtful touches and was in a great location. Definitely would use this when they visit again." - }, - { - "_id": "329849308", - "date": { - "$date": "2018-09-29T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "2141875", - "reviewer_name": "Adam", - "comments": "Fantastic unit, very central. Lots of amenities." - }, - { - "_id": "333104176", - "date": { - "$date": "2018-10-06T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "112825219", - "reviewer_name": "Sean", - "comments": "Location, location, location!\n\nIn the heart of the city, close to transport, close to cafes and restaurants and walking distance to CBD.\n\nHotels are good, Airbnb’s are great!" - }, - { - "_id": "335315057", - "date": { - "$date": "2018-10-11T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "25379624", - "reviewer_name": "Karyn", - "comments": "This apartment is a wonderful place to stay in Sydney. It is very stylish and comfortable. The location is near transport, shops, cafes and restaurants. Having access to a kitchen also gave me the option of cooking a light meal if I didn't feel like going out." - }, - { - "_id": "339731424", - "date": { - "$date": "2018-10-22T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "75059289", - "reviewer_name": "Pam", - "comments": "Desiree’s apartment was wonderful. Beautifully furnished, sparkling clean, fabulous location and many thoughtful touches. The location is amazing and i would stay there any time I am in Sydney. Thank you for sharing your home Desireé and your thoughtfulness!" - }, - { - "_id": "341036128", - "date": { - "$date": "2018-10-25T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "22035157", - "reviewer_name": "Stéphanie", - "comments": "Very good location. Incredible host, Desiree helped us out with everything and responded really fast. Everything we needed was at the appartment." - }, - { - "_id": "342941547", - "date": { - "$date": "2018-10-30T04:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "172543899", - "reviewer_name": "Judy", - "comments": "Beautiful rental, perfect location to attractions, busses, restaurants and so much more. Owners go above and beyond." - }, - { - "_id": "346489742", - "date": { - "$date": "2018-11-09T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "2314881", - "reviewer_name": "Lisa", - "comments": "A wonderfully stylish and authentic apartment. Perfect location. Lots of lovely books to read and thoughtful touches. The hosts are fantastic. I would hope to stay there if ever we are in Sydney again." - }, - { - "_id": "349271873", - "date": { - "$date": "2018-11-17T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "19535282", - "reviewer_name": "Nevet", - "comments": "we had such a great stay. the location is super, the host is flexible and accomodating and the appartment is sooooo nice plus, plus ,plus" - }, - { - "_id": "350933933", - "date": { - "$date": "2018-11-20T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "3700308", - "reviewer_name": "Kyle", - "comments": "This flat was absolutely perfect! It was located right across from Hyde Park with a beautiful view. It was very central to everything we wanted to do in Sydney. She also left us chocolate, coffee, milk and other food for us which was so lovely to come to. We traveled very far and were immediately impressed by the lovely accommodation and personal touch of Desiree’s flat. I would highly recommend this apartment if coming to Sydney for a city break!" - }, - { - "_id": "352172186", - "date": { - "$date": "2018-11-24T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "33014705", - "reviewer_name": "Aleks", - "comments": "Wonderfully appointed apartment. Centrally located and a lovely host" - }, - { - "_id": "353839476", - "date": { - "$date": "2018-11-29T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "53907804", - "reviewer_name": "Anne", - "comments": "Desiree’s place was absolutely perfect for our time spent in Sydney. Amazing location and everything you could need! Highly recommend staying here while in Sydney!" - }, - { - "_id": "357829570", - "date": { - "$date": "2018-12-11T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "71873083", - "reviewer_name": "은혜", - "comments": "위치가 매우 좋았고, 청결하고 집도 넓고 쾌적했어요! 그리고 호스트가 매우매우 친절했습니다" - }, - { - "_id": "400388868", - "date": { - "$date": "2019-01-12T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "21864680", - "reviewer_name": "Mary", - "comments": "Desireé apartment is so centrally - ideal location! \nThe apartment is a real home from home & has everything you need.\nDesireé is a super host, very easy to checkin & out. \nWe loved our stay here & didn’t want to leave. :)" - }, - { - "_id": "403458039", - "date": { - "$date": "2019-01-20T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "176191120", - "reviewer_name": "David", - "comments": "My wife and I had a lovely time staying at Desiree’s apartment. Was very close to public transportation to get around the city. Apartment was very clean and comfortable for our stay. Desireé was very prompt in any communication before and during our stay. Highly recommend." - }, - { - "_id": "405933232", - "date": { - "$date": "2019-01-28T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "17795180", - "reviewer_name": "Matthew", - "comments": "This AirBnB is not to be missed! Home was very clean and there were thoughtful touches upon check in. I arrived very late at night and the check in process was quick and easy. The host had amazing and quick responses during my stay - great communication. The apartment has 2 balconies which allow loads of sunlight into the space when the curtains are pulled back. Great location for Ubers, Taxis, walks in the park. I stayed for business and I walked to the CBD within 15 minutes - easy. I will definitely stay again!" - }, - { - "_id": "408870752", - "date": { - "$date": "2019-02-06T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "15652289", - "reviewer_name": "Dave", - "comments": "The moment you enter this Airbnb you realize this is a host who cares. Chocolate on the coffee table, cold water in the fridge, cereal on the shelf, and roses in the bedroom greeted us. Nine floors up you can hear the street noise but the bedroom is surprisingly quiet. The location is near the metro and walkable to the waterfront. This home and its location were perfect for our 6 night stay. Would happily return on our next trip to Sydney." - }, - { - "_id": "415147375", - "date": { - "$date": "2019-02-22T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "10933634", - "reviewer_name": "Joyce", - "comments": "This city sized apartment was wonderful. Very clean, well stocked with most everything you need. Located around grocery stores, pharmacies, shops, restaurants and bars. Great location for exploring Sydney, buses and trains within 5 minutes, walking distance to circular quay. Street noise was remarkably low in the bedroom. The only shortcoming was the internet connection, very spotty if more than one device was was connected. We would definitely stay here upon our return to Darlinghurst." - }, - { - "_id": "420508856", - "date": { - "$date": "2019-03-06T05:00:00.000Z" - }, - "listing_id": "10108388", - "reviewer_id": "35883709", - "reviewer_name": "Joe", - "comments": "I highly recommend Desiree's place to anyone visiting Sydney. It's convenient, clean, and very comfortable. Desiree was an outstanding host. She was communicative, helpful, and went above and beyond. I hope to return." - } - ] - }, - { - "_id": "10057826", - "listing_url": "https://www.airbnb.com/rooms/10057826", - "name": "Deluxe Loft Suite", - "summary": "Loft Suite Deluxe @ Henry Norman Hotel Located in Greenpoint, Brooklyn and housed in a converted 19th-century warehouse, the Henry Norman Hotel features modern lofts and suites with hardwood floors, antiques and contemporary art.", - "space": "This loft unit features a kitchenette and is fully equipped with everything you need for your stay. The Loft Suite Deluxe has an alcove bedroom area with a Queen size bed and a comfortable pull-out sofa in the living room area, which in total can easily sleep up to 4 guests. The loft is equipped with 42\" LCD HD TVs in both the living and bedroom areas with premium cable channels (HBO, Showtime & more). There are hardwood floors throughout and a full bathroom with shower/tub combination. Sleeps up to 4 guests.", - "description": "Loft Suite Deluxe @ Henry Norman Hotel Located in Greenpoint, Brooklyn and housed in a converted 19th-century warehouse, the Henry Norman Hotel features modern lofts and suites with hardwood floors, antiques and contemporary art. This loft unit features a kitchenette and is fully equipped with everything you need for your stay. The Loft Suite Deluxe has an alcove bedroom area with a Queen size bed and a comfortable pull-out sofa in the living room area, which in total can easily sleep up to 4 guests. The loft is equipped with 42\" LCD HD TVs in both the living and bedroom areas with premium cable channels (HBO, Showtime & more). There are hardwood floors throughout and a full bathroom with shower/tub combination. Sleeps up to 4 guests. Guest will have access to common terraces, lounge area, business center with 2 iMac computers, fitness center with dry sauna and steam shower, and laundry room. Greenpoint is an artsy Brooklyn neighborhood filled with great restaurants, cafes, and shops.", - "neighborhood_overview": "Greenpoint is an artsy Brooklyn neighborhood filled with great restaurants, cafes, and shops.", - "notes": "", - "transit": "", - "access": "Guest will have access to common terraces, lounge area, business center with 2 iMac computers, fitness center with dry sauna and steam shower, and laundry room.", - "interaction": "", - "house_rules": "Guest must leave a copy of credit card with front desk for any incidentals/ damages with a copy of valid ID. There are no additional charges than what has been paid in advance.", - "property_type": "Apartment", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "3", - "maximum_nights": "1125", - "cancellation_policy": "strict_14_with_grace_period", - "last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-01-03T05:00:00.000Z" - }, - "last_review": { - "$date": "2018-02-18T05:00:00.000Z" - }, - "accommodates": 4, - "bedrooms": 0, - "beds": 2, - "number_of_reviews": 5, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Cable TV", - "Internet", - "Wifi", - "Air conditioning", - "Kitchen", - "Doorman", - "Gym", - "Elevator", - "Heating", - "Family/kid friendly", - "Washer", - "Dryer", - "Smoke detector", - "Carbon monoxide detector", - "First aid kit", - "Fire extinguisher", - "Essentials", - "Shampoo", - "24-hour check-in", - "Hangers", - "Hair dryer", - "Iron" - ], - "price": { - "$numberDecimal": "205.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/40ace1e3-4917-46e5-994f-30a5965f5159.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "47554473", - "host_url": "https://www.airbnb.com/users/show/47554473", - "host_name": "Mae", - "host_location": "US", - "host_about": "", - "host_response_time": "within a few hours", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/c680ce22-d6ec-4b00-8ef3-b5b7fc0d76f2.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/c680ce22-d6ec-4b00-8ef3-b5b7fc0d76f2.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Greenpoint", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": false, - "host_listings_count": 13, - "host_total_listings_count": 13, - "host_verifications": [ - "email", - "phone", - "google", - "reviews", - "jumio", - "government_id" - ] - }, - "address": { - "street": "Brooklyn, NY, United States", - "suburb": "Greenpoint", - "government_area": "Greenpoint", - "market": "New York", - "country": "United States", - "country_code": "US", - "location": { - "type": "Point", - "coordinates": [-73.94472, 40.72778], - "is_location_exact": true - } - }, - "availability": { - "availability_30": 30, - "availability_60": 31, - "availability_90": 31, - "availability_365": 243 - }, - "review_scores": { - "review_scores_accuracy": 9, - "review_scores_cleanliness": 10, - "review_scores_checkin": 10, - "review_scores_communication": 8, - "review_scores_location": 9, - "review_scores_value": 9, - "review_scores_rating": 88 - }, - "reviews": [ - { - "_id": "58665374", - "date": { - "$date": "2016-01-03T05:00:00.000Z" - }, - "listing_id": "10057826", - "reviewer_id": "22162519", - "reviewer_name": "Alex", - "comments": "I could not have found a better place to stay in Brooklyn. I have nothing but positive things to say about this place. Definitely recommend staying here and would stay again." - }, - { - "_id": "88420503", - "date": { - "$date": "2016-07-24T04:00:00.000Z" - }, - "listing_id": "10057826", - "reviewer_id": "20282871", - "reviewer_name": "Dina", - "comments": "My husband, son and I were fortunate enough to have discovered the Henry Norman Hotel for our recent stay in Greenpoint Brooklyn. Even though we booked our room through Airbnb, we were welcome to all the amenities the hotel had to offer. We were pleased from our arrival at the hotel, where the exterior was as pretty as the interior and greeted by Robert whose very pleasant demeanor was refreshing. Each of the staff at the hotel were very professional and accommodating but Robert stood out from the rest. I was unfortunate enough to have left my phone at a nearby shop after returning via the hotel shuttle, when notifying the front desk to see if I had left my phone in the back seat, Robert quickly responded with his assistance in searching for the item, then driving us immediately to the last location while he assured us he would be outside waiting so we would not have to walk back. I did in fact find my phone at that location (thanks to a good Samaritan) and Robert refused to take compensation for his kindness. The only thing I would do differently on our next stay is to spend the extra for the room with the patio, as the view from the hotel is quite nice and the breezes up there were wonderful. We not only will be back to visit the lovely hotel but will gladly recommend it to all who might be vising the Greenpoint area. " - }, - { - "_id": "92234102", - "date": { - "$date": "2016-08-08T04:00:00.000Z" - }, - "listing_id": "10057826", - "reviewer_id": "19466953", - "reviewer_name": "Rosana", - "comments": "Great place!!" - }, - { - "_id": "207168650", - "date": { - "$date": "2017-10-28T04:00:00.000Z" - }, - "listing_id": "10057826", - "reviewer_id": "95604783", - "reviewer_name": "Marie", - "comments": "I am disappointed of my trip in the hotel. The room is well&clean but the staff not helpful. Indeed, because of a flight schedule change, I asked the Airbnb host and the welcome desk of the hotel how I could manage my last night cancellation to be refund. I did exactly what they asked me to do and have been informed two days after that they refused my request of refunding... Despite the policy rules, I found it very dishonest and not correct at all as I have asked them before doing the cancellation on Airbnb and the flight confirmation.\nMoreover, I felt insulted when they made me left the welcome desk to go to the basement in order to let other clients of the hotel believe everything is perfect (I was calm and just trying to solve the issue).\nSo to conclude, the room ok but bad experience in this Airbnb that is an hotel!" - }, - { - "_id": "236131644", - "date": { - "$date": "2018-02-18T05:00:00.000Z" - }, - "listing_id": "10057826", - "reviewer_id": "11623469", - "reviewer_name": "Adam", - "comments": "we rented the suite at the henry norman hotel, and it was lovely" - } - ] - }, - { - "_id": "10133350", - "listing_url": "https://www.airbnb.com/rooms/10133350", - "name": "2 bedroom Upper east side", - "summary": "Near 70th and 1st. A very nice 6th floor walk-up with true 2 bedrooms and one bath. Full Kitchen. One bedroom has a queen and the other a double. Living room has a couch you could sleep on too. New powerful air conditioner cools whole apt. Great area with restaurants, shopping and walk to Central Park Please contact us first if reserving on same day, we might need time to arrange for keys. Holiday weekends we prefer 3 night stays. Thank you!", - "space": "The space is one block away from the new second avenue subway... The Q line which gets you into times sqaure/heralds square in ten minutes.", - "description": "Near 70th and 1st. A very nice 6th floor walk-up with true 2 bedrooms and one bath. Full Kitchen. One bedroom has a queen and the other a double. Living room has a couch you could sleep on too. New powerful air conditioner cools whole apt. Great area with restaurants, shopping and walk to Central Park Please contact us first if reserving on same day, we might need time to arrange for keys. Holiday weekends we prefer 3 night stays. Thank you! The space is one block away from the new second avenue subway... The Q line which gets you into times sqaure/heralds square in ten minutes. We are able to be reached during your stay. Walk to Central Park or Madison ave shops. Plenty of restaurants, pizza and great bagel shop.", - "neighborhood_overview": "Walk to Central Park or Madison ave shops. Plenty of restaurants, pizza and great bagel shop.", - "notes": "", - "transit": "", - "access": "", - "interaction": "We are able to be reached during your stay.", - "house_rules": "", - "property_type": "Apartment", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "2", - "maximum_nights": "7", - "cancellation_policy": "strict_14_with_grace_period", - "last_scraped": { - "$date": "2019-03-06T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-03-06T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-05-28T04:00:00.000Z" - }, - "last_review": { - "$date": "2017-08-19T04:00:00.000Z" - }, - "accommodates": 5, - "bedrooms": 2, - "beds": 2, - "number_of_reviews": 9, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Cable TV", - "Internet", - "Wifi", - "Air conditioning", - "Kitchen", - "Pets allowed", - "Pets live on this property", - "Buzzer/wireless intercom", - "Heating", - "Family/kid friendly", - "Essentials", - "Shampoo", - "Hair dryer", - "Iron", - "Laptop friendly workspace" - ], - "price": { - "$numberDecimal": "275.00" - }, - "security_deposit": { - "$numberDecimal": "0.00" - }, - "cleaning_fee": { - "$numberDecimal": "35.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/d9886a79-0633-4ab4-b03a-7686bab13d71.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "52004369", - "host_url": "https://www.airbnb.com/users/show/52004369", - "host_name": "Chelsea", - "host_location": "Sea Cliff, New York, United States", - "host_about": "", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/4d361f57-f65e-4885-b934-0e92eebf288d.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/4d361f57-f65e-4885-b934-0e92eebf288d.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "", - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 2, - "host_total_listings_count": 2, - "host_verifications": [ - "email", - "phone", - "reviews", - "jumio", - "offline_government_id", - "government_id" - ] - }, - "address": { - "street": "New York, NY, United States", - "suburb": "Upper East Side", - "government_area": "Upper East Side", - "market": "New York", - "country": "United States", - "country_code": "US", - "location": { - "type": "Point", - "coordinates": [-73.95854, 40.7664], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 0, - "availability_60": 0, - "availability_90": 0, - "availability_365": 0 - }, - "review_scores": { - "review_scores_accuracy": 9, - "review_scores_cleanliness": 8, - "review_scores_checkin": 10, - "review_scores_communication": 9, - "review_scores_location": 9, - "review_scores_value": 9, - "review_scores_rating": 90 - }, - "reviews": [ - { - "_id": "76578167", - "date": { - "$date": "2016-05-28T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "49207437", - "reviewer_name": "Danielle", - "comments": "The host canceled this reservation the day before arrival. This is an automated posting." - }, - { - "_id": "93702337", - "date": { - "$date": "2016-08-13T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "9967305", - "reviewer_name": "Kelly", - "comments": "Beautiful home. Quiet. clean. amazing time." - }, - { - "_id": "105658320", - "date": { - "$date": "2016-10-02T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "96994191", - "reviewer_name": "William", - "comments": "We had an excellent experience at Chelsea's apartment. The host was always available to answer any questions we had about the apartment or the surrounding area and made some great recommendations for restaurants in the area. We had no issues with the apartment or the amenities." - }, - { - "_id": "106856500", - "date": { - "$date": "2016-10-08T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "33610061", - "reviewer_name": "Harry", - "comments": "Chelsea was in constant contact with me since we booked. We needed a place to stay inside Manhattan and this place was perfect. You cant beat the location of this apartment anywhere in the city. Just enough distance away from the tourists, and easy walk to subway, bars, and great restaurants. Will definitely book this place again. " - }, - { - "_id": "107654987", - "date": { - "$date": "2016-10-11T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "56867841", - "reviewer_name": "Kayla", - "comments": "Chelsea was a great host! We arrived a little later than expected but had no trouble arranging to pick up keys. The apartment is in a nice location. It is far enough out to be relaxing but still close enough to go to Time Square and many other attractions. The apartment was nice and a great fit for my family of 3. Would recommend staying here." - }, - { - "_id": "115711765", - "date": { - "$date": "2016-11-26T05:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "102526576", - "reviewer_name": "Kristin", - "comments": "Chelsea provided amazing communication, was super friendly and very accomodating with check-in and check-out! She was just a text away at any time & checked in to be sure we got in/out with ease. That made our stay very pleasant. The location is in walking distance from Rockefeller, Central Park & public transport is easily accessible too. We walked with a stroller & 3 young kids to both those places more than once and anything we needed was right around the block. Thanks so much Chelsea!" - }, - { - "_id": "123736997", - "date": { - "$date": "2016-12-30T05:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "44715535", - "reviewer_name": "Alex", - "comments": "Chelsea was the best and so understanding. The apartment was fantastic. Everything I expected and more. Easy access to the subway and a great easy walk to Central Park. Highly recommended. Again Chelsea is simply amazing and super great." - }, - { - "_id": "158224614", - "date": { - "$date": "2017-06-05T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "107443841", - "reviewer_name": "Mateus", - "comments": "It's a nice place, near of subway stations and restaurants. Chelsea is an amazing person, gave us a lot of tips and was so gentle. Our biggest problems was: 6th floor walk (brazilians buy a lot of things in USA, that's why it was a thuf mission go up carrying everything lol), the small bathroom (but with the time you fits with it) and the dust (one of us have allergic rhinitis and sneezed a lot). But it was a great stay, we certainly return if we can." - }, - { - "_id": "184374356", - "date": { - "$date": "2017-08-19T04:00:00.000Z" - }, - "listing_id": "10133350", - "reviewer_id": "125308610", - "reviewer_name": "Francesco", - "comments": "Lo sconsigliamo vivamente. Abbiamo avuto una pessima esperienza. Mia moglie, io e i tre bambini non abbiamo potuto fermarci neanche un minuto per le pessime condizioni in cui abbiamo trovato l'appartamento. Non corrisponde alle foto, perchè le stanze da letto sono molto più piccole di quello che appaiono, piene di suppellettili polverose e in disordine. Il sofa bed è praticamente un divano sporco dove abbiamo dovuto chiedere lenzuola e ci è stato dato un solo lenzuolo. Il bagno è una latrina minuscola tanto è vero che non appare nelle foto pubblicate. Non abbiamo potuto dormire neanche una notte perché non c'è neanche un cassetto o un armadio vuoto in cui poter stipare i propri effetti. Abbiamo pagato per tre notti oltre 800 euro, non abbiamo passato neanche venti minuti in quella casa, abbiamo dovuto trovare all'1 di notte un'altra sistemazione a New YorK - per cinque persone, compresi due bambini minori - e Chelsea non ci ha risarcito neanche un euro. Veramente un comportamento deprecabile da parte sua." - } - ] - }, - { - "_id": "10133554", - "listing_url": "https://www.airbnb.com/rooms/10133554", - "name": "Double and triple rooms Blue mosque", - "summary": "", - "space": "We are on the central city Blue mosque 5 minutes Hagia sopiha 4 minutes Topkapi palace and archeology museums 5 minutes Grand bazaar 10 minutes", - "description": "We are on the central city Blue mosque 5 minutes Hagia sopiha 4 minutes Topkapi palace and archeology museums 5 minutes Grand bazaar 10 minutes We have bathroom,wc,İnternet in rooms and under rooms we have cafe bar restaurant and market anything you need we can help you 24 hours Hospitality Cankurtaran mahallesi akbıyık caddesi no 22 Sultanahmet Fatih İstanbul Tram 5 minutes metro 10 minutes Bus 10 minutes", - "neighborhood_overview": "Cankurtaran mahallesi akbıyık caddesi no 22 Sultanahmet Fatih İstanbul", - "notes": "", - "transit": "Tram 5 minutes metro 10 minutes Bus 10 minutes", - "access": "We have bathroom,wc,İnternet in rooms and under rooms we have cafe bar restaurant and market anything you need we can help you 24 hours", - "interaction": "Hospitality", - "house_rules": "", - "property_type": "Bed and breakfast", - "room_type": "Private room", - "bed_type": "Real Bed", - "minimum_nights": "1", - "maximum_nights": "1125", - "cancellation_policy": "moderate", - "last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "first_review": { - "$date": "2017-05-04T04:00:00.000Z" - }, - "last_review": { - "$date": "2018-05-07T04:00:00.000Z" - }, - "accommodates": 3, - "bedrooms": 1, - "beds": 2, - "number_of_reviews": 29, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "Internet", - "Wifi", - "Air conditioning", - "Free parking on premises", - "Smoking allowed", - "Heating", - "Family/kid friendly", - "Suitable for events", - "Washer", - "Dryer", - "Fire extinguisher", - "Essentials", - "Shampoo", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace", - "Self check-in", - "Building staff" - ], - "price": { - "$numberDecimal": "121.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/68de30b5-ece5-42ab-8152-c1834d5e25fd.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "52004703", - "host_url": "https://www.airbnb.com/users/show/52004703", - "host_name": "Mehmet Emin", - "host_location": "Istanbul, İstanbul, Turkey", - "host_about": "", - "host_response_time": "within a few hours", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/user/4cb6be34-659b-42cc-a93d-77a5d3501e7a.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/user/4cb6be34-659b-42cc-a93d-77a5d3501e7a.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 2, - "host_total_listings_count": 2, - "host_verifications": [ - "email", - "phone", - "facebook", - "reviews", - "jumio", - "offline_government_id", - "government_id" - ] - }, - "address": { - "street": "Fatih , İstanbul, Turkey", - "suburb": "Fatih", - "government_area": "Fatih", - "market": "Istanbul", - "country": "Turkey", - "country_code": "TR", - "location": { - "type": "Point", - "coordinates": [28.98009, 41.0062], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 30, - "availability_60": 60, - "availability_90": 90, - "availability_365": 365 - }, - "review_scores": { - "review_scores_accuracy": 9, - "review_scores_cleanliness": 9, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 10, - "review_scores_value": 9, - "review_scores_rating": 92 - }, - "reviews": [ - { - "_id": "149469150", - "date": { - "$date": "2017-05-04T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "12428834", - "reviewer_name": "Tara", - "comments": "AMAZING time. Great price, clean room. The staff were the best!!" - }, - { - "_id": "150131865", - "date": { - "$date": "2017-05-07T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "12428834", - "reviewer_name": "Tara", - "comments": "Great location, amazing food and vibe downstairs. We ate downstairs every night & enjoyed their wonderful company. Super friendly staff and help you any chance they can. \nThe room is on top of the bar so at night it is a little noisy but we were okay with it. \nReally nice part of town. 2 minute walk to Hagia Sophia, the palace, museums, and other food options. \nThe room was just like the photos! Clean and modern. " - }, - { - "_id": "151850175", - "date": { - "$date": "2017-05-14T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "39119353", - "reviewer_name": "Matt", - "comments": "Mehmet was a fantastic host. Very accommodating and always immediately communicated. His space is in a great part of the city, and it was clean, accessible and comfortable. " - }, - { - "_id": "157981834", - "date": { - "$date": "2017-06-05T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "47190229", - "reviewer_name": "Former Member", - "comments": "Low price and still super close to Hagia Sophia and many other places. Mehmet and his colleagues are really nice and do their best to help you feel at home. The cafe on the ground floor is really cool and you keep meeting interesting people there. \n100% recommended if you plan to explore Jerusalem most of the day and look for a cheap place to sleep. " - }, - { - "_id": "162998934", - "date": { - "$date": "2017-06-23T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "135955722", - "reviewer_name": "Syafiq", - "comments": "Mehmet and all the staff here are soooo nice. They made me feel like im at home and treated me like a family. Highly recommended. Nice surrounding. Good food at the cafe down here. They made me wanna come and stay here again and i definitely will." - }, - { - "_id": "165255742", - "date": { - "$date": "2017-06-30T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "41117815", - "reviewer_name": "Stefan", - "comments": "We would give it 3.5 rating based on the fact that the bed was exceptional and the location was optimal. Bathroom was very dirty with a broken toilet seat that was never fixed ...eventhough the staff was politely reminded each of the seven nights we stayed...holiday or no holiday...how can you be running a place like that? AC wasnt functioning, however, it was fixed by the end of second night and a fan provided as a replacement. In the end, it was exceptional as a budget place. I would highly suggest that the management take cleaning and basic hygeine into serious consideration (mold on the bathroom wall) the place could earn a 4 star experience with ease. " - }, - { - "_id": "168732288", - "date": { - "$date": "2017-07-10T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "18106672", - "reviewer_name": "Yeşim", - "comments": "Muhteşem konum!" - }, - { - "_id": "169048910", - "date": { - "$date": "2017-07-11T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "131433329", - "reviewer_name": "Sebastian", - "comments": "It was an affordable stay. They have great shuttle options to the airport. Pick up shuttles from the airport are expensive but to the airport are very affordable" - }, - { - "_id": "170357685", - "date": { - "$date": "2017-07-15T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "43626981", - "reviewer_name": "Hiro", - "comments": "Location is great! Ambiance and the people are perfect. The room I stayed is clean and perfect; no complains about that. My only concern is the accuracy of the photo in the listing, the photo is just a \"model\" interior of what it's gonna look like, not the real room you're gonna stay. Since I was traveling alone, I was given a room enough for a single traveler. The room in the listing is probably the room beside mine. Anyway it is not a big deal. I would still definitely stay here in the future. Thanks Mehmet!" - }, - { - "_id": "177536085", - "date": { - "$date": "2017-08-03T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "37488325", - "reviewer_name": "学丹", - "comments": "位置很好,房东很友好,如果是追求位置的话,可以选择这一家,有一点就是洗手间设施不是很好,楼下是酒吧可能会有点吵闹,总的来说很开心。蓝色清真寺和索非亚大教堂可以步行去。" - }, - { - "_id": "178362175", - "date": { - "$date": "2017-08-05T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "30846126", - "reviewer_name": "Alma", - "comments": "The apartment is in a very central and nice location and is right over a bar (which you get tea for free in as a guest and food at a 10% discount) but so it can be a little noisy at night. The people were super nice and helped us with anything we needed. We only had a problem with the fact that there was no real curtain in the room and so you could literally see in the room from the street which sucked a little and there was mold in the bathroom. but otherwise it's a great place to stay for a few nights!" - }, - { - "_id": "180317672", - "date": { - "$date": "2017-08-10T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "62494735", - "reviewer_name": "Dylan", - "comments": "Mehmet is very nice ! The room is perfect, very near in Topkapi, Saint Sophia, Bleu Mosque...\nHe helps for the advices and all !\n\nI advice you Mehmet :)" - }, - { - "_id": "184129350", - "date": { - "$date": "2017-08-19T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "55039470", - "reviewer_name": "Serena", - "comments": "我很喜歡~" - }, - { - "_id": "185928853", - "date": { - "$date": "2017-08-23T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "72064521", - "reviewer_name": "Adam", - "comments": "Our stay was fantastic. Mehmet was was excellent with communication and made us feel at home. His place is centrally located and the cafe downstairs as a nice welcoming vibe. Would recommend to stay here on a trip to Istanbul." - }, - { - "_id": "186718124", - "date": { - "$date": "2017-08-25T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "123844092", - "reviewer_name": "Yury", - "comments": "The apartment is not corresponds to a photo on the site." - }, - { - "_id": "189223420", - "date": { - "$date": "2017-09-01T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "77317123", - "reviewer_name": "Ǝbo", - "comments": "Good value, good location - recommended for short stay - good for single travellers or couples." - }, - { - "_id": "192245881", - "date": { - "$date": "2017-09-10T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "76832413", - "reviewer_name": "Judi", - "comments": "Nice location,friendly welcoming even when we arrived very late.\n\nThumbs up for me" - }, - { - "_id": "192781055", - "date": { - "$date": "2017-09-11T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "7444804", - "reviewer_name": "Jurgis", - "comments": "Very sincere, friendly and caring people run this place. Definitely recommended!" - }, - { - "_id": "196094134", - "date": { - "$date": "2017-09-22T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "1500684", - "reviewer_name": "Bassam", - "comments": "Mehmet was great. I was only in and out for a night. But it's close to all the things I wanted to be close to.\n\nPerfect for a couple or single folks. I wouldn't recommend my room for kids. The stairs are a bit steep if you have children and the area can get a bit loud at night. But it was exactly what I needed for my short stay. Many of the people that were staying there were there for a few weeks. Mehmet seems to be an impeccable host and takes care of any concerns you may have.\n\nI would definitely stay again if I came alone or with a friend." - }, - { - "_id": "197646866", - "date": { - "$date": "2017-09-26T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "74478502", - "reviewer_name": "Jesu", - "comments": "Great room.. and awesome host" - }, - { - "_id": "201339702", - "date": { - "$date": "2017-10-08T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "46864896", - "reviewer_name": "Chirihan", - "comments": "Mehmet is a very good host. We had a wonderful time during our stay. There is a restaurant on the ground floor very cosy and practical. The apartment was clean although the water wasn't hot enough to shower. The neighbourhood has many restaurants which make it a bit noisy at night.The best thing about it is that it is very near to the main historical monuments." - }, - { - "_id": "210779482", - "date": { - "$date": "2017-11-11T05:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "157267090", - "reviewer_name": "Amar", - "comments": "It was good" - }, - { - "_id": "212488240", - "date": { - "$date": "2017-11-18T05:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "158612749", - "reviewer_name": "Enes", - "comments": "Very convenient location, great host - hospitality was good. would definitely recommend." - }, - { - "_id": "215182451", - "date": { - "$date": "2017-11-28T05:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "22085638", - "reviewer_name": "Roger", - "comments": "I could not check in because, much to my surprise, I was not allowed to leave the Istanbul airport during my overnight in Istanbul. I immediately emailed Mehmet Emin my situation. He was within his rights to refuse to refund my payment. I had hoped you would refund since my inability to check in was due to circumstances out of my control." - }, - { - "_id": "217169176", - "date": { - "$date": "2017-12-08T05:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "158851113", - "reviewer_name": "Jae Hoon", - "comments": "Best host ever!" - }, - { - "_id": "244652310", - "date": { - "$date": "2018-03-19T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "105509679", - "reviewer_name": "Juan Carlos", - "comments": "The apartment was spacious and acceptably clean. The location is perfect, near to a lot of sites of interest. Hagia Sofia is like 5 min walking. The place is also in a restorant and bar zone, actually it is above a restorant, which is good because you have food and drink at hand, but it can be a little noisy at night. Mehmet was a great host, he's very friendly and accesible as well as the people at the restaurant below. 100% recomendable." - }, - { - "_id": "249540910", - "date": { - "$date": "2018-04-02T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "51106035", - "reviewer_name": "Faris", - "comments": "Mehmet is extremely accommodating and very polite." - }, - { - "_id": "259629452", - "date": { - "$date": "2018-05-01T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "50848584", - "reviewer_name": "Filip", - "comments": "Good place on great location." - }, - { - "_id": "261857290", - "date": { - "$date": "2018-05-07T04:00:00.000Z" - }, - "listing_id": "10133554", - "reviewer_id": "34395507", - "reviewer_name": "Cindy", - "comments": "Location is wonderful, especially for a first-timer in Istanbul. I went with my boyfriend who had been to the city before, and my first visit was fantastic in the area of Mehmit's place. Can walk to everything. Good food and drinks right downstairs, and staff and friends of the Siva Restaurant are always available. It's very lively at night, so it's a great place to stay if you're social and stay up late. Mehmit's place is the whole package and experience. Well worth it and a good price." - } - ] - }, - { - "_id": "10115921", - "listing_url": "https://www.airbnb.com/rooms/10115921", - "name": "GOLF ROYAL RESİDENCE TAXİM(1+1):3", - "summary": "our place situated at the middle of beautiful places such as nişantaşi street just 5 min walking and osmanbey these streets are full of markets,resturants also we are 1.2 kilo metere far from taksim square and istiklal street one of the most famous", - "space": "", - "description": "our place situated at the middle of beautiful places such as nişantaşi street just 5 min walking and osmanbey these streets are full of markets,resturants also we are 1.2 kilo metere far from taksim square and istiklal street one of the most famous", - "neighborhood_overview": "", - "notes": "", - "transit": "", - "access": "", - "interaction": "", - "house_rules": "", - "property_type": "Hotel", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "1", - "maximum_nights": "1125", - "cancellation_policy": "strict_14_with_grace_period", - "last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-02-01T05:00:00.000Z" - }, - "last_review": { - "$date": "2017-08-07T04:00:00.000Z" - }, - "accommodates": 4, - "bedrooms": 2, - "beds": 4, - "number_of_reviews": 3, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Cable TV", - "Internet", - "Wifi", - "Air conditioning", - "Wheelchair accessible", - "Kitchen", - "Paid parking off premises", - "Smoking allowed", - "Doorman", - "Elevator", - "Buzzer/wireless intercom", - "Heating", - "Family/kid friendly", - "Suitable for events", - "Dryer", - "Smoke detector", - "Carbon monoxide detector", - "First aid kit", - "Safety card", - "Fire extinguisher", - "Essentials", - "Shampoo", - "24-hour check-in", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace", - "Self check-in", - "Building staff", - "Crib", - "Hot water", - "Luggage dropoff allowed", - "Long term stays allowed" - ], - "price": { - "$numberDecimal": "838.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/fbdaf067-9682-48a6-9838-f51589d4791a.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "51471538", - "host_url": "https://www.airbnb.com/users/show/51471538", - "host_name": "Ahmet", - "host_location": "Istanbul, İstanbul, Turkey", - "host_about": "", - "host_response_time": "within an hour", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/user/d8c830d0-16da-455c-818a-790864132e0a.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/user/d8c830d0-16da-455c-818a-790864132e0a.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Şişli", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": false, - "host_listings_count": 16, - "host_total_listings_count": 16, - "host_verifications": ["email", "phone", "reviews"] - }, - "address": { - "street": "Şişli, İstanbul, Turkey", - "suburb": "Şişli", - "government_area": "Sisli", - "market": "Istanbul", - "country": "Turkey", - "country_code": "TR", - "location": { - "type": "Point", - "coordinates": [28.98713, 41.04841], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 30, - "availability_60": 60, - "availability_90": 90, - "availability_365": 365 - }, - "review_scores": { - "review_scores_accuracy": 7, - "review_scores_cleanliness": 7, - "review_scores_checkin": 8, - "review_scores_communication": 8, - "review_scores_location": 10, - "review_scores_value": 7, - "review_scores_rating": 67 - }, - "reviews": [ - { - "_id": "61231578", - "date": { - "$date": "2016-02-01T05:00:00.000Z" - }, - "listing_id": "10115921", - "reviewer_id": "12352658", - "reviewer_name": "Johann", - "comments": "This place and the service has a lot of potential but certainly has not reached its prime yet. Ahmet was very quick to respond and approve my reservation which is great. He was also very responsive to additional questions. The residence service at the counter was also good and the blokes were helpful. The apartment is clean and spacious, although it could do with a couple of extra basic necessities particularly in the kitchen. \r\n\r\nHowever, two main things made this experience rather average instead of great.\r\n- I reserved a flat with TWO bedrooms and only got one. I complained and they relocated me to a bigger apartment with two separate beds, but still ONE bedroom. Upon booking make sure to confirm its two bedrooms you sign up to.\r\n\r\n- Saturday seemed to be national construction day which meant 8am rise and shine. sunday there was activity too although less loud. Normally not a problem but not great when you experienced some of the Istanbul night life the night before. A notice about this at least would have been great." - }, - { - "_id": "69120673", - "date": { - "$date": "2016-04-09T04:00:00.000Z" - }, - "listing_id": "10115921", - "reviewer_id": "20657237", - "reviewer_name": "Mohamed", - "comments": "The location is just perfect. You can find everything in the neighborhood such as restaurants, supermarket, exchange, shops....etc.\nThe lobby of the hotel is small.\nI stayed in the two bedroom apt.\nUnfortunately, the second room has the bathroom while the master bedroom has none. You have to use the one in the living room.\nSome of the staff in the Hotel are very very helpful like Mustafa and some are friendly like Mohamed." - }, - { - "_id": "179271487", - "date": { - "$date": "2017-08-07T04:00:00.000Z" - }, - "listing_id": "10115921", - "reviewer_id": "32336412", - "reviewer_name": "Abdulrazaq", - "comments": "They have to check the type of apartment" - } - ] - }, - { - "_id": "10116256", - "listing_url": "https://www.airbnb.com/rooms/10116256", - "name": "GOLF ROYAL RESIDENCE SUİTES(2+1)-2", - "summary": "A BIG BED ROOM WITH A BIG SALOON INCLUDING A NICE BALAKON TO HAVE SOME FRESH AIR . OUR RESIDENCE SITUATED AT THE CENTRE OF THE IMPORTANT MARKETS SUCH AS NİŞANTAŞİ,OSMANBEY AND TAKSIM SQUARE,", - "space": "", - "description": "A BIG BED ROOM WITH A BIG SALOON INCLUDING A NICE BALAKON TO HAVE SOME FRESH AIR . OUR RESIDENCE SITUATED AT THE CENTRE OF THE IMPORTANT MARKETS SUCH AS NİŞANTAŞİ,OSMANBEY AND TAKSIM SQUARE,", - "neighborhood_overview": "", - "notes": "", - "transit": "", - "access": "", - "interaction": "", - "house_rules": "", - "property_type": "Serviced apartment", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "1", - "maximum_nights": "1125", - "cancellation_policy": "moderate", - "last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "accommodates": 6, - "bedrooms": 2, - "beds": 5, - "number_of_reviews": 0, - "bathrooms": { - "$numberDecimal": "2.0" - }, - "amenities": [ - "TV", - "Internet", - "Wifi", - "Air conditioning", - "Wheelchair accessible", - "Kitchen", - "Paid parking off premises", - "Smoking allowed", - "Doorman", - "Elevator", - "Buzzer/wireless intercom", - "Heating", - "Family/kid friendly", - "Suitable for events", - "Washer", - "Dryer", - "Smoke detector", - "Carbon monoxide detector", - "Fire extinguisher", - "Essentials", - "Shampoo", - "24-hour check-in", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace", - "Self check-in", - "Building staff", - "Hot water", - "Luggage dropoff allowed", - "Long term stays allowed" - ], - "price": { - "$numberDecimal": "997.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/79955df9-923e-44ee-bc3c-5e88041a8c53.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "51471538", - "host_url": "https://www.airbnb.com/users/show/51471538", - "host_name": "Ahmet", - "host_location": "Istanbul, İstanbul, Turkey", - "host_about": "", - "host_response_time": "within an hour", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/user/d8c830d0-16da-455c-818a-790864132e0a.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/user/d8c830d0-16da-455c-818a-790864132e0a.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Şişli", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": false, - "host_listings_count": 16, - "host_total_listings_count": 16, - "host_verifications": ["email", "phone", "reviews"] - }, - "address": { - "street": "Şişli, İstanbul, Turkey", - "suburb": "Şişli", - "government_area": "Sisli", - "market": "Istanbul", - "country": "Turkey", - "country_code": "TR", - "location": { - "type": "Point", - "coordinates": [28.98818, 41.04772], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 30, - "availability_60": 60, - "availability_90": 90, - "availability_365": 365 - }, - "review_scores": {}, - "reviews": [] - }, - { - "_id": "10047964", - "listing_url": "https://www.airbnb.com/rooms/10047964", - "name": "Charming Flat in Downtown Moda", - "summary": "Fully furnished 3+1 flat decorated with vintage style. Located at the heart of Moda/Kadıköy, close to seaside and also to the public transportation (tram, metro, ferry, bus stations) 10 minutes walk.", - "space": "The apartment is composed of 1 big bedroom with double sized bed, a guest room with double sofa bed , a living room with 2 double sofa bed. It's suitable for max 6 people. Every bedroom has balcony overlooking the calm streets of Moda, where you will find relax and intimacy , even if at one step from the most vivid Moda's living streets. There's also private parking area with remote controller. The apartment is away from the traffic and the most crowded places, good for the ensuring quiet and a peaceful rest. The apartment is fully available to guests and is equipped with heating in all bedrooms. In the living room there is a HD TV and Apple TV. In the kitchen, there are dishwasher, oven and washing machine, with the respective detergents. You will find fresh water in the fridge and on the kitchen there are a toaster, kettle, coffee maker. In the bathroom you will find hand soap, shampoo and a hair dryer. For those who wish, there will be biscuits, tea and coffee for breakfast; oil, v", - "description": "Fully furnished 3+1 flat decorated with vintage style. Located at the heart of Moda/Kadıköy, close to seaside and also to the public transportation (tram, metro, ferry, bus stations) 10 minutes walk. The apartment is composed of 1 big bedroom with double sized bed, a guest room with double sofa bed , a living room with 2 double sofa bed. It's suitable for max 6 people. Every bedroom has balcony overlooking the calm streets of Moda, where you will find relax and intimacy , even if at one step from the most vivid Moda's living streets. There's also private parking area with remote controller. The apartment is away from the traffic and the most crowded places, good for the ensuring quiet and a peaceful rest. The apartment is fully available to guests and is equipped with heating in all bedrooms. In the living room there is a HD TV and Apple TV. In the kitchen, there are dishwasher, oven and washing machine, with the respective detergents. You will find fresh water in the fridge and on t", - "neighborhood_overview": "With its diversity Moda- Kadikoy is one of the most colorfull neighbourhood of Istanbul. The place is surrounded by lots of cafe's, pubs, concert halls, restaurants and art studios. Being local is wonderful if you're staying here.", - "notes": "", - "transit": "", - "access": "", - "interaction": "", - "house_rules": "Be and feel like your own home, with total respect and love..this would be wonderful!", - "property_type": "House", - "room_type": "Entire home/apt", - "bed_type": "Real Bed", - "minimum_nights": "2", - "maximum_nights": "1125", - "cancellation_policy": "flexible", - "last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-02-18T05:00:00.000Z" - }, - "first_review": { - "$date": "2016-04-02T04:00:00.000Z" - }, - "last_review": { - "$date": "2016-04-02T04:00:00.000Z" - }, - "accommodates": 6, - "bedrooms": 2, - "beds": 6, - "number_of_reviews": 1, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Cable TV", - "Internet", - "Wifi", - "Kitchen", - "Free parking on premises", - "Pets allowed", - "Pets live on this property", - "Cat(s)", - "Heating", - "Family/kid friendly", - "Washer", - "Essentials", - "Shampoo", - "24-hour check-in", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace" - ], - "price": { - "$numberDecimal": "527.00" - }, - "cleaning_fee": { - "$numberDecimal": "211.00" - }, - "extra_people": { - "$numberDecimal": "211.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/231120b6-e6e5-4514-93cd-53722ac67de1.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "1241644", - "host_url": "https://www.airbnb.com/users/show/1241644", - "host_name": "Zeynep", - "host_location": "Istanbul, Istanbul, Turkey", - "host_about": "Z.", - "host_thumbnail_url": "https://a0.muscache.com/im/users/1241644/profile_pic/1426581715/original.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/users/1241644/profile_pic/1426581715/original.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Moda", - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 2, - "host_total_listings_count": 2, - "host_verifications": [ - "email", - "phone", - "facebook", - "reviews", - "jumio", - "government_id" - ] - }, - "address": { - "street": "Kadıköy, İstanbul, Turkey", - "suburb": "Moda", - "government_area": "Kadikoy", - "market": "Istanbul", - "country": "Turkey", - "country_code": "TR", - "location": { - "type": "Point", - "coordinates": [29.03133, 40.98585], - "is_location_exact": true - } - }, - "availability": { - "availability_30": 27, - "availability_60": 57, - "availability_90": 87, - "availability_365": 362 - }, - "review_scores": { - "review_scores_accuracy": 10, - "review_scores_cleanliness": 10, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 10, - "review_scores_value": 10, - "review_scores_rating": 100 - }, - "reviews": [ - { - "_id": "68162172", - "date": { - "$date": "2016-04-02T04:00:00.000Z" - }, - "listing_id": "10047964", - "reviewer_id": "33536670", - "reviewer_name": "Mihra", - "comments": "Zeynep was a most welcoming and generous host, with a gorgeous, comfortable flat - as advertised! The flat is light and spacious, kitchen well-equipped, bed comfortable (both beds actually), and bathroom clean, with great shower pressure. Zeynep prepared a note with key information about the flat, which was great to have for reference. I especially appreciated the ground coffee and coffee maker. The fact that there was a desk in the house made my stay all the more comfortable - I had a proper place to sit down at my computer.\r\n\r\nIt's clear that Zeynep has put a lot of care into making her flat a home - it's an awesome flat! \r\n\r\nZeynep lives a five min walk to the sea, with a great park along the water front. There are plenty of hip cafes and coffee shops in the neighborhood (Moda), all a short walk from the flat. And it's only a 15 mins walk to the Kadikoy ferry, which offers easy access to the rest of Istanbul." - } - ] - }, - { - "_id": "1003530", - "listing_url": "https://www.airbnb.com/rooms/1003530", - "name": "New York City - Upper West Side Apt", - "summary": "", - "space": "Murphy bed, optional second bedroom available. Wifi available, Hulu, Netflix, TV Eat-in kitchen. Bathroom with great shower/bath. Washer/dryer in basement.", - "description": "Murphy bed, optional second bedroom available. Wifi available, Hulu, Netflix, TV Eat-in kitchen. Bathroom with great shower/bath. Washer/dryer in basement. New York City! Great neighborhood - many terrific restaurants, bakeries, bagelries. Within easy walking distance are restaurants with the cuisines from India, Thailand, Japan, China, Mexico, South America and Europe. As well as the many small independent stores that line Broadway, there chain stores such as Urban Outfitters (clothing), Whole Foods (groceries), Sephora (cosmetics), Michaels (crafts), and Modell's (sporting goods). Equidistant to Central Park and Riverside Park which have walking/running/biking trails as well as tennis and racquet ball courts. 10-15 blocks from Columbia University between Broadway and Amsterdam. The International Hostel across the street is an airport shuttle bus and double-decker tour bus stop. The Hostel also has services available to the neighborhood such as lectures, brochures, etc. Convenientl", - "neighborhood_overview": "Great neighborhood - many terrific restaurants, bakeries, bagelries. Within easy walking distance are restaurants with the cuisines from India, Thailand, Japan, China, Mexico, South America and Europe. As well as the many small independent stores that line Broadway, there chain stores such as Urban Outfitters (clothing), Whole Foods (groceries), Sephora (cosmetics), Michaels (crafts), and Modell's (sporting goods). Equidistant to Central Park and Riverside Park which have walking/running/biking trails as well as tennis and racquet ball courts. 10-15 blocks from Columbia University between Broadway and Amsterdam. The International Hostel across the street is an airport shuttle bus and double-decker tour bus stop. The Hostel also has services available to the neighborhood such as lectures, brochures, etc.", - "notes": "My cat, Samantha, are in and out during the summer. The apt is layed out in such a way that each bedroom is very private.", - "transit": "Conveniently located near 1, 2, 3, B & C subway lines. Also buses on Columbus Avenue (downtown), Amsterdam Avenue (uptown), and Broadway (uptown/downtown). Also near origination/termination of M60 bus which goes to/from LaGuardia Airport.", - "access": "New York City!", - "interaction": "", - "house_rules": "No smoking is permitted in the apartment. All towels that are used should be placed in the bath tub upon departure. I have a cat, Samantha, who can stay or go, whichever is preferred. Please text me upon departure.", - "property_type": "Apartment", - "room_type": "Private room", - "bed_type": "Real Bed", - "minimum_nights": "12", - "maximum_nights": "360", - "cancellation_policy": "strict_14_with_grace_period", - "last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-03-07T05:00:00.000Z" - }, - "first_review": { - "$date": "2013-04-29T04:00:00.000Z" - }, - "last_review": { - "$date": "2018-08-12T04:00:00.000Z" - }, - "accommodates": 2, - "bedrooms": 1, - "beds": 1, - "number_of_reviews": 70, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "Internet", - "Wifi", - "Air conditioning", - "Kitchen", - "Elevator", - "Buzzer/wireless intercom", - "Heating", - "Family/kid friendly", - "Washer", - "Dryer", - "translation missing: en.hosting_amenity_50" - ], - "price": { - "$numberDecimal": "135.00" - }, - "security_deposit": { - "$numberDecimal": "0.00" - }, - "cleaning_fee": { - "$numberDecimal": "135.00" - }, - "extra_people": { - "$numberDecimal": "0.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/15074036/a97119ed_original.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "454250", - "host_url": "https://www.airbnb.com/users/show/454250", - "host_name": "Greta", - "host_location": "New York, New York, United States", - "host_about": "By now I have lived longer in the city than the country however I feel equally at home in each. I like to keep one foot in each and help others to do the same!", - "host_response_time": "within an hour", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/f1022be4-e72a-4b35-b6d2-3d2736ddaff9.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/f1022be4-e72a-4b35-b6d2-3d2736ddaff9.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "", - "host_response_rate": 100, - "host_is_superhost": true, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 3, - "host_total_listings_count": 3, - "host_verifications": [ - "email", - "phone", - "reviews", - "jumio", - "offline_government_id", - "government_id" - ] - }, - "address": { - "street": "New York, NY, United States", - "suburb": "Manhattan", - "government_area": "Upper West Side", - "market": "New York", - "country": "United States", - "country_code": "US", - "location": { - "type": "Point", - "coordinates": [-73.96523, 40.79962], - "is_location_exact": false - } - }, - "availability": { - "availability_30": 0, - "availability_60": 0, - "availability_90": 0, - "availability_365": 93 - }, - "review_scores": { - "review_scores_accuracy": 10, - "review_scores_cleanliness": 9, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 10, - "review_scores_value": 10, - "review_scores_rating": 94 - }, - "reviews": [ - { - "_id": "4351675", - "date": { - "$date": "2013-04-29T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "3708459", - "reviewer_name": "Josh", - "comments": "i had a really pleasant stay at greta's place. the location is super convenient -- and the area is fun, with lots of hidden gems. the apartment was comfortable and charming. greta was accommodating and hospitable and left some great cupcakes in the fridge. i give this place a strong endorsement." - }, - { - "_id": "4848277", - "date": { - "$date": "2013-05-28T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "4609491", - "reviewer_name": "Ralf", - "comments": "Greta has been a great host, and her apartment a great place to stay. Perfectly located for our adventures around NYC, it is a place full of character and, as I see it, a very authentic NY Apartment that makes you feel at home imediately. Great neighbourhood, great infrastructure around the apartment block. Very good for a perfect stay." - }, - { - "_id": "5094175", - "date": { - "$date": "2013-06-11T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "6741662", - "reviewer_name": "Mame", - "comments": "Greta was wonderful. We arrived late at night and she made sure we were able to get in. We also had to transfer to a hotel for work, and she allowed us to keep our belongings there until 3 so we wouldn't be carting our luggage until we needed to. The apartment was clean and large and in a wonderful neighborhood. She had lists and menus of places to eat. I couldn't have been happier with our arrangement. Thanks Greta!" - }, - { - "_id": "5497796", - "date": { - "$date": "2013-07-02T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "474156", - "reviewer_name": "Lars", - "comments": "Greta's place is very well located, so close to the MTA 1 line, which takes you up and down Manhattan, which was perfect for me. It was an easy 3 minute walk and be on your way to anywhere in NY. \r\nThe shower is something else, the best water pressure I have experienced in a long time.\r\nI thought the decor was cool and eclectic, Greta must have an artistic streak. The Murphy bed was comfortable, and the AC unit (not pictured) kept it nice and cool. \r\nAlthough I didn't meet her, she arranged for a neighbor to meet me and show me in. He was cool and so were any other neighbors that I met. \r\nShe also left a list of suggestions where to shop and eat, along with menus which was helpful when I needed a late night meal. \r\nHighly recommended. \r\n" - }, - { - "_id": "6933005", - "date": { - "$date": "2013-08-31T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "6067127", - "reviewer_name": "Nicole", - "comments": "I had a wonderful stay at Greta's NYC apartment. The apartment was clean and comfortable. She was a wonderful and informative host. I felt very welcomed in her home." - }, - { - "_id": "7205029", - "date": { - "$date": "2013-09-10T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "7743438", - "reviewer_name": "Ute", - "comments": "Hi Greta,\r\nyou have a wonderful place. We had a great time an felt really comfortable - little bit like beeing home in NY :)\r\nEverything worked out pretty good, also the connection to La Guardia Airport was perfect.\r\n\r\nThanks again :)\r\n" - }, - { - "_id": "7464858", - "date": { - "$date": "2013-09-21T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "8543724", - "reviewer_name": "Drew", - "comments": "Greta was very welcoming and helpful. Prompt email communication.\r\n\r\nA great stay for those in town to see Broadway shows, shopping or sight seeing the West Side. A few minutes walking and I was in Central Park.\r\n\r\nA definite recommendation." - }, - { - "_id": "7515354", - "date": { - "$date": "2013-09-23T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "1683177", - "reviewer_name": "Don", - "comments": "Greta was flexible about our sign-in time, helpful as we arrived, and kind to return to us a bag that we left in her apartment. The apartment is wonderfully located for transportation and things to do. It is compact and comfy, full of interesting books and prints. The bed is quite firm." - }, - { - "_id": "7871931", - "date": { - "$date": "2013-10-07T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "2871729", - "reviewer_name": "Heidi", - "comments": "We spent three nights in Greta’s apartment. Her kitchen is fully-equipped and the sleeping arrangements are extremely comfortable. Greta is a very accommodating host and easy to communicate with. She was very gracious about our later-than-expected arrival and allowed us to stay in her apartment on our departure day until we left for an early evening flight. Her apartment is very well-located, with food markets, drug stores, restaurants and coffee shops within a block of her building. The building is ½ block from the #1 subway line, enabling easy access to all parts of Manhattan." - }, - { - "_id": "8136769", - "date": { - "$date": "2013-10-17T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "7811780", - "reviewer_name": "Julia", - "comments": "We enjoyed our stay in Greta's apartment very much. The location is perfect, near Central Park, Broadway and everything, and the apartment is extremely cosy. Greta was very welcoming and helpful. We had a great time and felt really comfortable. Thanks :-)\r\n" - }, - { - "_id": "8210485", - "date": { - "$date": "2013-10-21T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "8119019", - "reviewer_name": "Diane", - "comments": "Our stay are Greta's was wonderful- she was so accomodating and welcoming. Her apartment is lovely and such a great place to unwind from the hurried family weekend weekend we had. Greta even accomodated the joining of my niece and nephew for one of the two nights with no problem.. Thank you so much, Greta, for your hospitality! " - }, - { - "_id": "8478870", - "date": { - "$date": "2013-11-02T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "8140760", - "reviewer_name": "Linda", - "comments": "Greta was a gracious and accommodating host, even coming over during our stay to reset the wifi connections. The location of the apartment is ideal, basically half a short block to the subway. Her list of neighborhood restaurants was very helpful and we sampled as many as we were able to. She has everything organized well." - }, - { - "_id": "8587648", - "date": { - "$date": "2013-11-06T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "9307997", - "reviewer_name": "Maureen", - "comments": "Greta's apartment was comfortable and clean and the neighborhood was easily accessible to museums and other locations. A bit noisy at night." - }, - { - "_id": "8768655", - "date": { - "$date": "2013-11-17T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "9464736", - "reviewer_name": "George", - "comments": "Greta was extremely helpful throughout this process! Communication was maintained throughout the process and throughout our stay. She was able to answer any questions we had and even gave suggestions for us. Her instructions were awesome and the information left at her apartment were great! There was even a binder with additional information like places to eat and places to go. She even took the time to make sure we knew how to use the radiator! Being from Hawaii, we had no knowledge about radiators and how to use them - we really appreciated her thoughtfulness!" - }, - { - "_id": "9179262", - "date": { - "$date": "2013-12-11T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "9405167", - "reviewer_name": "Whitney", - "comments": "Greta's apartment was homey, clean and just the right size for two (probably even three) people. Greta was extremely helpful in getting us settled and providing information about the neighborhood....etc. Murphy bed was very firm but comfortable. Great water pressure in shower. Apt is close to everything and the neighborhood has so much to offer. I would definitely recommend Greta's place. " - }, - { - "_id": "9428838", - "date": { - "$date": "2013-12-28T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "10297643", - "reviewer_name": "Deborah & Tim", - "comments": "The apartment is very homey - it felt like I was staying in a friend's apartment. The bed is surprisingly comfy and firm (I have never slept in a Murphy bed before) and the shower had plenty of hot water and great pressure. Greta was very communicative and friendly, and was also incredibly accommodating when I had something come up unexpectedly and needed to stay a couple hours longer than I anticipated. Overall all it was a very cozy stay in a great location!" - }, - { - "_id": "9543118", - "date": { - "$date": "2014-01-02T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "8828184", - "reviewer_name": "Catherine", - "comments": "The location of this appartment was ideal! Just minutes away the subway and walking distance from Central Park, Riverside Park, and many good restaurants on Broadway. A quaint appartment, with a lot of cachet. I recommend it!!" - }, - { - "_id": "20872115", - "date": { - "$date": "2014-10-06T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "7838233", - "reviewer_name": "Brandon", - "comments": "What a delightful apartment! I plan to stay here next time I bring family to New York. I would describe the apartment as a very warm, personal, comfortable and homey place to stay. It’s a corner apartment with good light, lots of books and very good energy. Plus, the beds are quite comfortable. My daughter exclaimed “Hey Dad, come lie down in this one. You’ll never want to get up!”\r\n\r\nThe neighborhood was incredibly well located and enjoyable. The train stops a half block away, and there are numerous excellent restaurants and conveniences close by. Greta, the host, provides a first-rate list of recommendations.\r\n\r\nGreta was a personable, helpful and extremely welcoming host. She was very quick to respond to my online inquiries and, once we arrived, to any further requests for advice.\r\n" - }, - { - "_id": "22257920", - "date": { - "$date": "2014-11-02T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "4034556", - "reviewer_name": "Aparna", - "comments": "Greta was a lovely host! She was communicative, helpful and flexible during the booking process and very responsive and friendly when we were at her place in NYC. The house is charming and comfortable, and very conveniently located right by the 1 train. We were two of us and only used one room, but the house can fit another person or two. \r\nFor anyone visiting NYC and looking to be in the city but not in the midst of the city noise, this is a great place to stay! You can be downtown in 20 minutes. " - }, - { - "_id": "23415037", - "date": { - "$date": "2014-11-30T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "22685675", - "reviewer_name": "Karolina", - "comments": "I stayed at Greta's place for 3 days! It was a really pleasant stay! The bed was really comfortable. The place was clean and cozy. A minute from the subway station! I didnt meet Greta in person, but we were communicating all the time and she was really helpful and kind with everything I needed! I would love to stay again the next time! :) i surely recommend it! " - }, - { - "_id": "23924172", - "date": { - "$date": "2014-12-14T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "16534063", - "reviewer_name": "Paul", - "comments": "Staying at Greta's apartment was delightful! It's a wonderful, quirky New York apartment -- very compact, but also very comfy and accurately portrayed in the pictures. The Murphy (fold-down) bed has a very thick memory foam mattress that is extremely comfortable. Samantha (the cat) was a sweet little presence, but she kept mostly to herself so we barely saw her. The location is great, less than a block to the 1 line, and there are several nice restaurants in the neighborhood. Finally, Greta herself was a perfect host: kind but not intrusive, thoughtful in her preparations for guests, and available should any problems arise (though none did). A great airbnb experience!" - }, - { - "_id": "28737168", - "date": { - "$date": "2015-03-29T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "29115141", - "reviewer_name": "Catherine", - "comments": "Greta was a great host. She met us late on Friday and explained everything about the apartment which we found very comfortable and fun. I would highly recommend this apartment to anyone. It is not luxurious but well-equipped and I loved the location, near Central Park and easy walk to museums.\r\n" - }, - { - "_id": "29274122", - "date": { - "$date": "2015-04-06T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "18417452", - "reviewer_name": "Paul", - "comments": "Apt is beautifully as advertised, a comfortable place and beds, kitchen and bath. Arrival and departure were really well coordinated by host, the lovely Greta. Location is fabulously convenient on upper West side by subway, Amsterdam and Broadway restaurants, bars and history. We'd stay here again. " - }, - { - "_id": "29610000", - "date": { - "$date": "2015-04-10T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "27919376", - "reviewer_name": "Anika", - "comments": "Greta was a lovely host, making sure we settled in well and could contact her easily if there were any problems. \r\nThe apartment is cosy with very comfortable beds and a well equipped kitchen in case you need a place to cook. It is also a nice neighborhood, with a lot of great places to eat nearby and it is a minute away from a subway station, so you are well connected to the rest of the city. We had a great time! " - }, - { - "_id": "29775810", - "date": { - "$date": "2015-04-12T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "20415509", - "reviewer_name": "Jane", - "comments": "My daughter and I stayed the weekend at Gretas place. Greta was herself away for the weekend but everything was in great shape for us to stay and Greta was very available by phone for any questions. Everything was very tidy and clean..comfy beds and good shower. It was like home. The location in the city is perfect..close to the park and subway. I highly recommend Gretas home for aNew York stay!" - }, - { - "_id": "30732097", - "date": { - "$date": "2015-04-26T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "10570759", - "reviewer_name": "Lakshmi", - "comments": "Greta is an amazing host and her apartment was super convenient for my stay in Manhattan. Here are my reasons:\r\nFirst - she as a person made me feel super welcome. I requested her a really early check in and she agreed (which was helpful because I took a red eye from Seattle). She also allowed me to keep my bags after my check out time so I could pick it up before my flight.\r\nSecond - her apartment location is SUPER convenient. Right next to the #1 and pretty close to B and C lines. NYC is all about location is this fits perfectly.\r\nThird - her apartment gives the feel of a typical NYC apartment. It is tiny but has everything you need. Two beds, a great shower, clean towels, and a kitchen (which I didn't use). Safe building with elevator. \r\n\r\nI would totally stay here again. " - }, - { - "_id": "31196460", - "date": { - "$date": "2015-05-03T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "43102", - "reviewer_name": "Catherine Et Albert", - "comments": "Greta was a fantastic host, we had a problem with a lost luggage at JFK and she helped us to coordinate the delivery. We also arrived later than scheduled in Manhattan, Greta was waiting for us, she welcomed us very friendly. The apartment is very nice and bright, the bed is very comfortable. Nice building with elevator, perfect location, between Riverside Park and Central Park, a block from the subway, lot of nice restaurants all around, we had a fantastic stay! Greta est une hôte fantastique, nous avons eu un problème de baggage à JFK et Greta nous a aidé à coordonner la livraison. Nous sommes arrivés plus tard que prévu et Greta nous a accueilli très chaleureseument. L'appartement est parfaitement situé, a un block du métro et entre Cantral Park et Riverside Park, il y a un ascenceur. Nombreux restaurants tout autour! Parfait nous recommandons vivement." - }, - { - "_id": "31845940", - "date": { - "$date": "2015-05-10T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "14717479", - "reviewer_name": "Samantha", - "comments": "We had a great stay with Greta. She was a good communicator, flexible, and her apartment is a good value for the neighborhood. We will definitely think about staying with her again." - }, - { - "_id": "33677946", - "date": { - "$date": "2015-05-31T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "31114625", - "reviewer_name": "Rebecca", - "comments": "My stay at Greta's place was perfect! The space is exactly as described, and was very clean and welcoming. Greta was very responsive to me, and even helped me figure out how to pick up the keys with my late arrival. Thank you!" - }, - { - "_id": "36960183", - "date": { - "$date": "2015-07-03T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "33119724", - "reviewer_name": "Thomas", - "comments": "Greta was an excellent hostess and was very accommodating. She took care of the little things that made a difference such as providing an extra set of keys and having the air conditioning on prior to my arrival. The apartment was clean and very comfortable. It instantly felt like \"home\". The location of the apartment was great! There are multiple grocery stores, restaurants, bars, etc. within a very short walking distance. The subway stop was very conveniently located as was Central Park and Riverside Park, which are both beautiful places for an afternoon walk. I would and have recommended this listing to friends that are traveling to the city and I would stay there again without hesitation. " - }, - { - "_id": "37555310", - "date": { - "$date": "2015-07-08T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "6352182", - "reviewer_name": "Murray", - "comments": "Greta is an excellent Airbnb host! She is warm, accommodating and very well organized. Her apt is located close to all amenities, Central and Riverside Parks, and bus/subway stops. The apt is clean, more spacious than expected, well stocked with anything you could possibly need, and has very comfortable beds and a first rate shower. I was travelling with my soon-to-be 17 year old daughter so she was thrilled to have her own bedroom! This is not fancy accommodation but warm and homey and probably a good representation of how an average New Yorker lives, although I suspect not many are as organized as Greta. Much recommended!" - }, - { - "_id": "48783685", - "date": { - "$date": "2015-09-28T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "43967256", - "reviewer_name": "Judy", - "comments": "I have used airbnb in NYC several times, and this, by far, was my best experience. The location is on a quiet street and on the same block as a major subway line. You can't get any better than that. The apartment has all the conveniences of a home - everything you'd need in a kitchen, extremely comfortable mattress, cable TV, air conditioning, wifi, great water pressure. \r\n\r\nGreata goes way out of her way to make your stay as enjoyable as possible. Her recommendations are outstanding and the check-in and check-out were extremely easy. This is a five star host and you won't go wrong with this choice for a stay in NYC. \r\n" - }, - { - "_id": "53270568", - "date": { - "$date": "2015-11-07T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "21960218", - "reviewer_name": "Nicky", - "comments": "This is my second time staying at Greta's apartment. The best place to stay in NYC by far. There is a subway on the same street as the apartment, a two minute walk away (the 1 line) and multiple places to eat close by. There is also a CVS , Walgreens, grocery and liquor store within walking distance as well. The apartment is very clean and the bed is very comfortable. Checking in and out as easy as Greta is very flexible. The apartment is very spacious with a nice kitchen and lots of room in the fridge. I hope to continue to stay here every time I visit New York! " - }, - { - "_id": "57574836", - "date": { - "$date": "2015-12-27T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "50466815", - "reviewer_name": "Obaseki", - "comments": "Greta place is amazing and blissful. She was understand to our delayed scheduled. Her place is home away from home. Her details description gave us point of interest to visit. If I was to go to NYC again I would check Greta place available first." - }, - { - "_id": "58775965", - "date": { - "$date": "2016-01-05T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "2692640", - "reviewer_name": "Federica", - "comments": "We had a wonderful experience staying at Greta's flat. She is an amazing host: disposable, kind and always ready to answer back to our necessities, including late check-in and check-out. The flat itself is a true newyorker's one, warm, stylish and excellentlty connected with the subway. We strongly suggest it for everyone!" - }, - { - "_id": "63209635", - "date": { - "$date": "2016-02-21T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "4823592", - "reviewer_name": "Lucinda", - "comments": "Greta was very welcoming and flexible. My friend and I slept well in comfortable beds under soft cotton sheets in this lovely quiet flat on the upper west side. We walked to the Museum of Natural History and hopped on the subway (less than one block away!) to go downtown. Greta's place offered a sweet artful respite from the rush of the city." - }, - { - "_id": "70519082", - "date": { - "$date": "2016-04-18T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "59943151", - "reviewer_name": "H", - "comments": "Greta was as amazing as her beautiful home! The check in and check out process was easy. Greta's communication was super convenient as she used text messaging and email to keep in constant communication. Her apartment is beautifully decorated, extremely comfortable, and very welcoming. The neighborhood is safe with stores and restaurants within a short walking distance. I highly recommend Greta's apartment for renting. You won't be disappointed." - }, - { - "_id": "71843260", - "date": { - "$date": "2016-04-29T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "31368654", - "reviewer_name": "Peter", - "comments": "What can we say? Greta is a delight and her place is perfect. The beds are comfortable, the kitchen is an eat-in and the bathroom is classic New York. The building is almost 150 years old and holds up. The location is perfect, a block to the 1 train and four blocks from the bus to LA Guardia. Everything was perfect and we already miss Samantha. Thank you Greta and we hope to be able to stay there in the future." - }, - { - "_id": "82291166", - "date": { - "$date": "2016-06-27T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "4543077", - "reviewer_name": "Stuart", - "comments": "Easy to make arrangements, was readily available for questions, and flexible about arriving and departing. Highly recommend this Upper West Side accommodation." - }, - { - "_id": "84121201", - "date": { - "$date": "2016-07-05T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "62507392", - "reviewer_name": "Jean", - "comments": "Greta and I communicated regularly before my trip,she was prompt and helpful. She met me at the apartment and was very welcoming,filled me in on all I needed to know. The apartment is so lovely and convenient and cosy and beautiful! Great area. I'd love to come back." - }, - { - "_id": "85258241", - "date": { - "$date": "2016-07-10T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "42965012", - "reviewer_name": "Gwen", - "comments": "Greta went out of her way to assist us with difficult flight times and luggage which really made things easier for us. The area was good and as everyone notes, great for the subway, which works far better than the tourist buses which barely move at times with traffic. Lots of eateries etc handy. " - }, - { - "_id": "86034388", - "date": { - "$date": "2016-07-14T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "53587610", - "reviewer_name": "Nesta", - "comments": "Throughout, all Greta's communications were prompt, friendly and helpful. We felt very prepared and welcome in her apartment, which is charming, comfortable and well located - a real 'home-from-home' to return to after a hot day's sight-seeing. It was fun to have a brief swap over with the previous visitors (agreed previously by email with Greta). Many thanks for a great stay. " - }, - { - "_id": "86519833", - "date": { - "$date": "2016-07-16T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "576347", - "reviewer_name": "Ifeoma", - "comments": "This was a unique apartment in an excellent location. Great for anyone having business at Columbia University. Close to restaurants, transportation. Highly recommend. " - }, - { - "_id": "88541809", - "date": { - "$date": "2016-07-24T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "54217384", - "reviewer_name": "Terri", - "comments": "Greta is a wonderful Host. Very honest, helpful. Great to work with. The neighborhood was awesome, close to subway, parks. Fabulous bagel store. The house is cute, quaint. Your really get the feel of what it's like to live there. I recommend this place. My daughter and I had a great time." - }, - { - "_id": "89504780", - "date": { - "$date": "2016-07-28T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "6273647", - "reviewer_name": "Ralf", - "comments": "short version: if available, take it! more details: Greta is a great host. very helpful and pragmatic about arrival and departure to accommodate travellers plans as best as possible. the apartment is very cosy and you feel like visiting a good friend or relative big apple. location between 2 subway lines and a 10min walk from central park is just great. a whole foods and other shops are on the way to get all necesseties. building is well maintained and clean like the whole neighborhood. surprisingly quite for the central location. we habe been there in july and were able to manage the heat well by the window aircons to have a good night sleep. Greta always replied immediately in the outmost helpful way during preparation and stay. thank you Greta! we will hopefully be back soon :)\n" - }, - { - "_id": "90261939", - "date": { - "$date": "2016-07-31T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "57249998", - "reviewer_name": "Annesca", - "comments": "Greta went through a lot of trouble to accommodate our flight times and make our arrival and departure as convenient as possible. Much appreciated!" - }, - { - "_id": "92084727", - "date": { - "$date": "2016-08-07T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "24815450", - "reviewer_name": "Julien", - "comments": "Nous avons apprécié la flexibilité de Greta pour répondre à nos questions et la localisation de l appartement très proche du métro.\r\n" - }, - { - "_id": "94083156", - "date": { - "$date": "2016-08-14T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "42965012", - "reviewer_name": "Gwen", - "comments": "Our second stay at Greta's on the last leg of our holiday, the city was oh so hot and humid but fortunately her air conditioning was equal to the task. The subway entrance close to the apartment was manned and easy to buy and top up your pass, it is the only way to travel around the city. On leaving for the airport, after both our stays we had only to wait a minute on the corner with our luggage to get a taxi to the airport. Once again Greta allowed us to arrive and leave at times that suited us with our luggage which didn't leave us stranded for hours with difficult flight times from Australia which was a great relief, thanks so much Greta!" - }, - { - "_id": "98202715", - "date": { - "$date": "2016-08-29T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "20316967", - "reviewer_name": "Jenny", - "comments": "We had a wonderful stay in NYC. Greta was a wonderful host, excellent communication, very helpful and accommodating. The location was perfect for us near restaurants and Columbia. We would love to stay there again. Thank You Greta!" - }, - { - "_id": "98674131", - "date": { - "$date": "2016-09-01T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "54784830", - "reviewer_name": "Terri", - "comments": "The apartment is in a nice neighbor. The Italian restaurant next store was good. Communication with Greta was exceptional. We would definitely stay here again." - }, - { - "_id": "99485043", - "date": { - "$date": "2016-09-05T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "59943151", - "reviewer_name": "H", - "comments": "This was my second stay at Greta's place, and it was absolutely wonderful. The apartment is clean, comfortable, and very well organized. I especially love the decor and comfortable bedding. The neighborhood is safe and very convenient with easy access to bus lines and rail. There is also a wide variety of restaurants within walking distance. Greta is a very attentive and responsive hostess. You will not be disappointed with her or her wonderful home. I highly recommend!" - }, - { - "_id": "101291814", - "date": { - "$date": "2016-09-12T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "5919533", - "reviewer_name": "Jean And Letitia", - "comments": "We had a great stay in New York City. Greta's apartment was perfect, great location just three blocks from Central Park and 1 block from major subway lines. The apartment was spacious, clean, quiet and welcoming. Thank you for making our new York experience even better. I greatly recommend Greta's place. She even let us stay until 2pm the day of departure. Thank you\r\nJean" - }, - { - "_id": "106551953", - "date": { - "$date": "2016-10-06T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "47795517", - "reviewer_name": "Vicki", - "comments": "We thoroughly enjoyed our stay in New York. Greta's place was a home away from home. The location was ideal with subway station, shops and restaurants just a few minutes walk, even an Italian restaurant next door. Central Park only 2 blocks away. Greta is very obliging and helpful especially her flexibility with our late flights. If we ever make it back to New York would definitely love to stay at Greta's again." - }, - { - "_id": "117029086", - "date": { - "$date": "2016-12-04T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "103606558", - "reviewer_name": "Rachel And Jeff", - "comments": "Great location close to subway & everything you would possibly need! Greta was very accommodating & if we stay in NYC again we will definitely be looking into booking her place first ;)" - }, - { - "_id": "121690051", - "date": { - "$date": "2016-12-18T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "205255", - "reviewer_name": "Jill", - "comments": "Great place for families! Nice location (we even found on-street parking easily), and great neighborhood for restaurants. " - }, - { - "_id": "123299700", - "date": { - "$date": "2016-12-28T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "97224933", - "reviewer_name": "Sarah Michele", - "comments": "Fantastic location and value. Host is very accommodating and responsive." - }, - { - "_id": "125121514", - "date": { - "$date": "2017-01-04T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "23113137", - "reviewer_name": "Kim", - "comments": "Greta is an amazing host with a very cute apartment. She was very responsive to our questions and provided us with a lot of information about the area. The location was amazing, so close the the 1 and Starbucks, DD and grocery stores. Would 100% stay here again if I returned to New York. " - }, - { - "_id": "144377714", - "date": { - "$date": "2017-04-15T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "18142306", - "reviewer_name": "Tracy", - "comments": "Greta's place is comfortable and homey. Each of the three bedrooms has a door, which is a real treat in a big city! A true New York apartment experience! Birds singing in the trees outside, comfy beds, loads of restaurants and grocery nearby. Lovely easy walks to Central Park, Riverside Park. Subway stop right on the corner. We took Lyft around due to my mom's use of a wheelchair in the streets. Drivers made good time to and from lower Manhattan via larger roads. Elevator made it easy to get to and from the 4th floor apartment, and being on the 4th floor removed us from the street a bit. " - }, - { - "_id": "156378118", - "date": { - "$date": "2017-05-30T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "120945155", - "reviewer_name": "Adrian", - "comments": "Overall a very nice apartment in a nice location. Plenty of restaurants in the area. Greta was a friendly and forthcoming host" - }, - { - "_id": "158625916", - "date": { - "$date": "2017-06-07T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "39779933", - "reviewer_name": "Sandrina", - "comments": "Great host, great Appartement, great location. Again any time. " - }, - { - "_id": "166479697", - "date": { - "$date": "2017-07-03T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "13818466", - "reviewer_name": "Jeremy", - "comments": "The place was so conveniently located at a mere 1-2 minutes walk from the subway. It was very cozy, everything was neat and tidy and really made us feel like we were home. Greta went the extra mile to help us with the check-in, and was super fast and responsive and made sure we didn't lack anything! My go-to airbnb place of choice for the next time we'll be in town. " - }, - { - "_id": "175167108", - "date": { - "$date": "2017-07-28T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "22834501", - "reviewer_name": "Kayla", - "comments": "Greta's place was perfect. We were in a three-week program at Columbia and walked into class each day. The place was spacious and quiet. Air conditioning was perfect in the hot New York summer! Would 100% stay again." - }, - { - "_id": "177406415", - "date": { - "$date": "2017-08-02T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "20960360", - "reviewer_name": "Noel", - "comments": "Lovely and homely apartment right in the heart of uptown Manhattan. Has a subway station on the same block, surrounded by great eats and just a couple of streets away from Central Park. Totally would come back when we visit New York again." - }, - { - "_id": "179377561", - "date": { - "$date": "2017-08-07T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "91436256", - "reviewer_name": "Rob", - "comments": "Greta's apartment is perfect for a couple who want to stay away from the noisy downtown area. The neighbourhood is peaceful, but has some good taverns and restaurants as well. Greta could not be any more helpful, going way beyond to get an item we left at the apartment to us. We cannot recommend Greta,s highly enough." - }, - { - "_id": "182396917", - "date": { - "$date": "2017-08-14T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "45877006", - "reviewer_name": "Joan", - "comments": "It was lovely to have Greta to greet us. We thoroughly enjoyed our stay in New York and in her apartment." - }, - { - "_id": "187129145", - "date": { - "$date": "2017-08-26T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "42556498", - "reviewer_name": "Linda", - "comments": "Gretas Appartement ist wirklich schön und gemütlich. Wir waren super zufrieden! Die Lage ist perfekt um zügig in Richtung Downtown oder in den Central Park zu gelangen. Falls man mit dem Auto anreist ist das Parken auf der Straße kostenlos. Auch Check-In und Kommunikation liefen völlig reibungslos. Sehr zu empfehlen!" - }, - { - "_id": "201967739", - "date": { - "$date": "2017-10-09T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "13818466", - "reviewer_name": "Jeremy", - "comments": "This was our 2nd stay at Greta's place and we loved it just as much as the first time we stayed. Again, great location (1 min walk from the subway), and we even managed to find free parking around! The place is really cozy and it's starting to feel like our 2nd home in NYC. Good shower, kitchen, separate bedrooms and a nice neighborhood close to central park. What more can you need? Highly recommended!" - }, - { - "_id": "223508188", - "date": { - "$date": "2018-01-01T05:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "14723761", - "reviewer_name": "Bruna", - "comments": "We had a really great time at Greta’s apartment! First of all she was very nice with us since the beggining, helping with all our doubts. The apartment is near of Central Park and a lot of stores and restaurants. It’s a calm local neighborhood, to live your days in the city like a real new yorker :) . The place was very clean, the shower was good and beds was confortable. The heater sistem worked very well and kept us warm always - it was very important, because the temperatures was very low during our stay there. We definetelly recomend this place to stay! " - }, - { - "_id": "245763661", - "date": { - "$date": "2018-03-23T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "60691725", - "reviewer_name": "Andrea", - "comments": "Loved the stay here. Greta is a wonderful host. The room is spacious and you have everything you need. The neighborhood is beautiful, lots of restaurants nearby, and the red train is literally next to the apartment. \nDefinitely recommended." - }, - { - "_id": "306501176", - "date": { - "$date": "2018-08-12T04:00:00.000Z" - }, - "listing_id": "1003530", - "reviewer_id": "174551190", - "reviewer_name": "Elena", - "comments": "greta was a super host! the house really nice and well kept. the bed is very comfortable and equipped with everything. the neighborhood is very safe and close to all amenities: the metro, supermarkets, main attractions, restaurants. Greta made sure that my stay was unforgettable, always present but never intrusive and respectful of privacy. it has always been helpful for every kind of need. thanks to greta for making my stay beautiful. surely if i go back to new york i will definitely come back here." - } - ] - }, - { - "_id": "10084023", - "listing_url": "https://www.airbnb.com/rooms/10084023", - "name": "City center private room with bed", - "summary": "House is located 5mins walk from Sham Shui Po and Prince Edward MTR. The private room has a single bed and a big closet, with window. You can enjoy the living area, kitchen and bathroom. Speaks great Eng, Mandarin & Cantonese.Lots of restaurant here", - "space": "The house is old fashion type, and paint the whole flat by ourselves, it is unique in the city^^", - "description": "House is located 5mins walk from Sham Shui Po and Prince Edward MTR. The private room has a single bed and a big closet, with window. You can enjoy the living area, kitchen and bathroom. Speaks great Eng, Mandarin & Cantonese.Lots of restaurant here The house is old fashion type, and paint the whole flat by ourselves, it is unique in the city^^ Living Room , Kitchen and Toilet, All cooking equipment can be used too A phone card of unlimited data will be provided during the stay, and phone call can also be made by the card. We have 3 bedrooms in the house and 2 are occupied by us, lots of Hong Kong traveling tips will be provided and also simple local tour if time is allowed. Cheapest food, electronic device, clothing and other stuff in Hong Kong can be found in this area. Lots of Traditional and old fashion building are located here. \"Dark side\" of Hong Kong can also be found here, because many elderly and poor are living Here Close to 3 different MTR Station, Sham shui Po and Shek K", - "neighborhood_overview": "Cheapest food, electronic device, clothing and other stuff in Hong Kong can be found in this area. Lots of Traditional and old fashion building are located here. \"Dark side\" of Hong Kong can also be found here, because many elderly and poor are living Here", - "notes": "Deposit of $1000 will be charged and will return back when check out if nothing is damaged.", - "transit": "Close to 3 different MTR Station, Sham shui Po and Shek Kei Mei 5 mins Walk, Prince edward 6 mins and lots of bus stations just downstairs to Hong Kong Island, Kowloon and New territory", - "access": "Living Room , Kitchen and Toilet, All cooking equipment can be used too", - "interaction": "A phone card of unlimited data will be provided during the stay, and phone call can also be made by the card. We have 3 bedrooms in the house and 2 are occupied by us, lots of Hong Kong traveling tips will be provided and also simple local tour if time is allowed.", - "house_rules": "1. 禁止吸煙, 只限女生入住 (除得到批准) No smoking and only female is allowed 2.熱水爐是儲水式, 不能長開, 用後請關上 The water heater cannot be turned on overnight, it will keep boiling water, thus, turn it off after use. 3. 不收清潔費,請保持房間及客廳清潔, 用過之碗筷需即日自行清洗 No cleaning fee is charged, thus, Please keep all the places including your room and living room clean and tidy, and make sure the dishes are washed after cooking on the same day. 4. 將收取港幣$1000為按金, 退房時將會歸還 (除故意破壞物品) Deposit of HKD$1000 will be charged and will return back when check out if nothing is damaged. 5. 房人將得到1條大門 1條鐵閘 及1條房鎖匙, 遺失需扣除$50 配匙費 1 main door key, 1 gate key and 1 private room key will be given at check in, $50 will be charged if lost. 6.洗髮後請必須將掉出來的頭髮拾起丟到垃圾桶或廁所,以免堵塞渠口 After washing hair, Please pick up your own hair and throw them in rubbish bin or toilet bowl, to avoid blocking the drain.", - "property_type": "Guesthouse", - "room_type": "Private room", - "bed_type": "Futon", - "minimum_nights": "1", - "maximum_nights": "500", - "cancellation_policy": "strict_14_with_grace_period", - "last_scraped": { - "$date": "2019-03-11T04:00:00.000Z" - }, - "calendar_last_scraped": { - "$date": "2019-03-11T04:00:00.000Z" - }, - "first_review": { - "$date": "2015-12-22T05:00:00.000Z" - }, - "last_review": { - "$date": "2019-03-01T05:00:00.000Z" - }, - "accommodates": 1, - "bedrooms": 1, - "beds": 1, - "number_of_reviews": 81, - "bathrooms": { - "$numberDecimal": "1.0" - }, - "amenities": [ - "TV", - "Wifi", - "Air conditioning", - "Kitchen", - "Elevator", - "First aid kit", - "Hangers", - "Hair dryer", - "Iron", - "Laptop friendly workspace", - "Hot water", - "Microwave", - "Refrigerator", - "Dishes and silverware", - "Stove", - "Long term stays allowed", - "Host greets you" - ], - "price": { - "$numberDecimal": "181.00" - }, - "weekly_price": { - "$numberDecimal": "1350.00" - }, - "monthly_price": { - "$numberDecimal": "5000.00" - }, - "security_deposit": { - "$numberDecimal": "0.00" - }, - "cleaning_fee": { - "$numberDecimal": "50.00" - }, - "extra_people": { - "$numberDecimal": "100.00" - }, - "guests_included": { - "$numberDecimal": "1" - }, - "images": { - "thumbnail_url": "", - "medium_url": "", - "picture_url": "https://a0.muscache.com/im/pictures/e6275515-7d73-4a70-bdc4-39ba6497e7d3.jpg?aki_policy=large", - "xl_picture_url": "" - }, - "host": { - "host_id": "51744313", - "host_url": "https://www.airbnb.com/users/show/51744313", - "host_name": "Yi", - "host_location": "United States", - "host_about": "Hi, this is Yi from Hong Kong, nice to meet you.\n\n你好!我是來自香港的Yi .", - "host_response_time": "within an hour", - "host_thumbnail_url": "https://a0.muscache.com/im/pictures/user/a204d442-1ae1-47fc-8d26-cf0ff9efda1b.jpg?aki_policy=profile_small", - "host_picture_url": "https://a0.muscache.com/im/pictures/user/a204d442-1ae1-47fc-8d26-cf0ff9efda1b.jpg?aki_policy=profile_x_medium", - "host_neighbourhood": "Shek Kip Mei", - "host_response_rate": 100, - "host_is_superhost": false, - "host_has_profile_pic": true, - "host_identity_verified": true, - "host_listings_count": 2, - "host_total_listings_count": 2, - "host_verifications": [ - "email", - "phone", - "reviews", - "jumio", - "offline_government_id", - "government_id" - ] - }, - "address": { - "street": "Hong Kong , 九龍, Hong Kong", - "suburb": "Sham Shui Po District", - "government_area": "Sham Shui Po", - "market": "Hong Kong", - "country": "Hong Kong", - "country_code": "HK", - "location": { - "type": "Point", - "coordinates": [114.1669, 22.3314], - "is_location_exact": true - } - }, - "availability": { - "availability_30": 14, - "availability_60": 24, - "availability_90": 40, - "availability_365": 220 - }, - "review_scores": { - "review_scores_accuracy": 10, - "review_scores_cleanliness": 8, - "review_scores_checkin": 10, - "review_scores_communication": 10, - "review_scores_location": 10, - "review_scores_value": 10, - "review_scores_rating": 92 - }, - "reviews": [ - { - "_id": "57191745", - "date": { - "$date": "2015-12-22T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "47616302", - "reviewer_name": "Kampo", - "comments": "The location of the apartment is easily accessible - right in the heart of kowloon. The host is really nice and the apartment is clean and neat. The district has much to offer: Strolling through the stalls and shops selling local delicacy, cheap clothes, souvenirs, Chinese herb tea and computer hardware/tech product is an amazing opportunity to experience the Grass-root live of Hong Kong people. Highly recommended!" - }, - { - "_id": "58082044", - "date": { - "$date": "2015-12-31T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "35014670", - "reviewer_name": "Saleh", - "comments": "Vikki (the host) so considerate, transparent and generous. She was very professional yet it felt like I was travelling with home to HongKong. Her home is fully equipped and at the heart of HK, close to nearly most places and the price is fair. \n\nCan't thank her enough from waiting and welcoming me on the street, touring me in the area, getting me sim card for 3g internet, or taking me hiking in an astounding places. \n\nHighly recommended if you wanna have a unique experience in HK." - }, - { - "_id": "58232488", - "date": { - "$date": "2016-01-01T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "51775677", - "reviewer_name": "Chelsea", - "comments": "房东是个很nice,而且是个很漂亮的姐姐哦,房间干净,设备齐全,但是跟酒店不一样,在这里有温馨的感觉!价格很经济了。住处很容易找到,楼下吃喝都应有尽有,而且有特色的点心铺哦,每次经过都很多人排队呢~有机会,下次还会再来,谢谢房东,保重身体喔" - }, - { - "_id": "58685576", - "date": { - "$date": "2016-01-03T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "42492678", - "reviewer_name": "薇薇", - "comments": "房東姐姐人不僅漂亮還非常nice!東西都準備得很齊全,牙刷牙膏,甚至是喝水的杯子和潤膚乳都準備好了!住得非常開心!房子的地段也很好,離太子地鐵站和深水埗地鐵站都不遠,交通方便!下次去香港還想住在這裡!" - }, - { - "_id": "72161129", - "date": { - "$date": "2016-05-01T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "23238277", - "reviewer_name": "Chiung Yi", - "comments": "So nice to meet Vicky! She is pretty and helpful!" - }, - { - "_id": "72484927", - "date": { - "$date": "2016-05-02T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "29164352", - "reviewer_name": "Samson", - "comments": "location is really foos..the hoat is really nice..!" - }, - { - "_id": "72874776", - "date": { - "$date": "2016-05-05T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "34171343", - "reviewer_name": "Jumpei", - "comments": "I could had amazing experiense from great hot's hospitality! The appartment is located in super convenient place, I always go to main district just by walking. And bus stop is just nearby so you can came here from Apt whenever. Vicky gave me special treatment. Had a 飲茶, ane she helped me to buy phone haha\nAnyways I strongly recommed this great place to explore Hongkong local stayle!" - }, - { - "_id": "77339432", - "date": { - "$date": "2016-05-31T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "10695277", - "reviewer_name": "Gregory", - "comments": "Yi is very nice. My friend have good time here. And many thanks too for the phone card :-)" - }, - { - "_id": "82798316", - "date": { - "$date": "2016-06-29T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "24304920", - "reviewer_name": "Jae Sung", - "comments": "She is friendly. She is tidy. She is beautiful. It is unforgettable to me thanks to her gift haejun memories of Hong Kong." - }, - { - "_id": "83751604", - "date": { - "$date": "2016-07-04T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "14148967", - "reviewer_name": "Leah", - "comments": "Yi was a great great host! This was my first time in Sham Shui Po District of Hong Kong and I wasn't really sure what to expect. The unit is small but this is pretty standard for Hong Kong. Location was excellent . Right across the street from the MTR or bus station(walk about 5 mins). Good eating spots close by as well as a grocery store across the street. Highly recommended to all of you." - }, - { - "_id": "86909422", - "date": { - "$date": "2016-07-18T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "27398082", - "reviewer_name": "Betty", - "comments": "她很热情,住宿附近也蛮安全,物有所值吧^-^" - }, - { - "_id": "90274604", - "date": { - "$date": "2016-07-31T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "28725", - "reviewer_name": "Lou", - "comments": "Awesome spot with two super friendly, warm, helpful hosts! Cozy and comfortable, small but totally fine. Great location. Highly recommended for a visit to Hong Kong!" - }, - { - "_id": "122789614", - "date": { - "$date": "2016-12-25T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "10252486", - "reviewer_name": "Spring", - "comments": "The place is between two mtr stations, super convenient!! Yi is very accommodating and I had a wonderful stay :)) hope to stay again next time!" - }, - { - "_id": "122995289", - "date": { - "$date": "2016-12-26T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "104904187", - "reviewer_name": "Mickey", - "comments": "位置很赞!房东人很好" - }, - { - "_id": "123154164", - "date": { - "$date": "2016-12-27T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "107535133", - "reviewer_name": "Seiichiro", - "comments": "it's easy to find!\nshe helped me a lot!\nanyways thx a bunch!" - }, - { - "_id": "123820744", - "date": { - "$date": "2016-12-30T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "20204953", - "reviewer_name": "Yosuke", - "comments": "Great hospitality and communication skills. I felt really welcomed. Thank you!" - }, - { - "_id": "124052003", - "date": { - "$date": "2016-12-31T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "42681129", - "reviewer_name": "Marlene", - "comments": "Very nice and sweet hosts! No wifi :(" - }, - { - "_id": "124488201", - "date": { - "$date": "2017-01-01T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "108817454", - "reviewer_name": "Hyeong Gi", - "comments": "Actually I met Yi' sister but she was so cool. I enjoyed staying there. Nice location & reasonable price." - }, - { - "_id": "126697546", - "date": { - "$date": "2017-01-13T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "100194758", - "reviewer_name": "Wilsan", - "comments": "Yi - one of the most friendly host I ever met , very cheerful - top class hospitality . Location is quite convenient too - you have big supermarket, restaurants, market, train station all nearby." - }, - { - "_id": "127411195", - "date": { - "$date": "2017-01-17T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "107160581", - "reviewer_name": "Ewan", - "comments": "Wonderful host who's friendly, helpful, and easy to talk to. Convenient location with plenty of public transit within easy walking distance." - }, - { - "_id": "129745959", - "date": { - "$date": "2017-02-01T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "101705415", - "reviewer_name": "Taine", - "comments": "Vicky is the best. ☺ here location is at the heart of Hongkong. Very clean, comfy and very convenient. Highly recommended!!" - }, - { - "_id": "130983217", - "date": { - "$date": "2017-02-09T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "42876010", - "reviewer_name": "Y", - "comments": "姐姐 非常友善 ,在她家两天时间她给我们很多关于旅行的建议。而且位置位于深水埗 我很喜欢 因为太多好吃的店了!下次去香港还想住这里^-^" - }, - { - "_id": "131348032", - "date": { - "$date": "2017-02-11T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "38529727", - "reviewer_name": "Hebe", - "comments": "房东人超级好,很热情,是个很好的体验" - }, - { - "_id": "132427830", - "date": { - "$date": "2017-02-17T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "84105026", - "reviewer_name": "Yvette", - "comments": "房東很友好有耐心,而且房子地段不錯,繁華有生活感,交通也方便快捷。在香港找到這個地方住感覺很舒服。感謝!" - }, - { - "_id": "132635079", - "date": { - "$date": "2017-02-18T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "27293705", - "reviewer_name": "Wei Kang", - "comments": "Nice host!" - }, - { - "_id": "133491363", - "date": { - "$date": "2017-02-22T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "29203773", - "reviewer_name": "Ryoma", - "comments": "Great location, the host was so kind !!\nIt's good memory that build the bed together :) haha\nSurely recommended !!" - }, - { - "_id": "134384005", - "date": { - "$date": "2017-02-26T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "116226072", - "reviewer_name": "Sidney", - "comments": "交通方便,周边吃的也方便,可以好好体验当地居民生活,房间在香港民宿旅馆中算比较宽敞的,性价比超高!最难忘的是和几个住客一起装配新买的木头床,就是现在照片里的那张啦,几个不同国家地区的年轻人一起动手,其乐融融!房东也很nice,提供的sim卡很方便。" - }, - { - "_id": "141631238", - "date": { - "$date": "2017-04-03T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "62234237", - "reviewer_name": "Julien", - "comments": "very nice hostess, willing to help the guests and it is a clean and comfortable room." - }, - { - "_id": "142746824", - "date": { - "$date": "2017-04-09T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "4208105", - "reviewer_name": "Jt", - "comments": "Yi is so cool. She understands hospitality very well and has a great personality. The place is simple and the location is not far from the central action at all. Highly recommend it." - }, - { - "_id": "159448606", - "date": { - "$date": "2017-06-11T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "13123607", - "reviewer_name": "Dimitris", - "comments": "I stayed at Vicky's place as I was visiting HK for a conference in City University, for 6 days. It is so close that I could go there on foot, and that is why I chose it. However, the location itself appeared to be great: the neighborhood around was magnificent, a very vibrant area, with extremely cheap and nice restaurants, street food vendors, shops etc everywhere. And actually, not touristic at all, you are going to see how local people live around there. Especially Mong Kok, which is just a little bit south, I really enjoyed walking around in the evenings. There are plenty of bus stops around, and the subway is at most 5 minutes away.\n\nThe place was also very nice. It was nice to see how common houses there look like and live there. Do not expect high luxuries or something, it is just a normal house. For me, it was a very nice place and adequately clean. There is also kitchen but I did not use it; there is tons of great and cheap food around, so there was no reason to spend time cooking (but Vicky one cooked and shared)! In general, I did not experience any negative issue regarding the facilities of the house.\n\nVicky provides with a sim card with unlimited internet, so it is even better than just having wifi in the house, as you can have internet in your smartphone or laptop everywhere you go. This helped navigating around the city a lot.\n\nVicky and her flatmate, J., were really nice and friendly. Initially, Vicky gave exact and clear instructions on how to get there. Both gave quite many suggestions on what to do in the city, and I even spent time with them. Both seem very interested in interacting with the guests, as long as they have time, of course. \n\nOverall, I am really enjoyed my stay there. I enjoyed being in that location and walking around. Everything concerning the house was working fine, making my stay in Hong Kong comfortable. Also I am very happy to have met Vicky and J., and enjoyed our interactions. I would certainly recommend." - }, - { - "_id": "181577731", - "date": { - "$date": "2017-08-13T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "31316205", - "reviewer_name": "Ayça", - "comments": "Vicky was very helpful and provided any assistance I needed. The place was as advertised. Everything was within walking distance or one bus ride away. The shops and people around the area were more like China. I liked the area more than Central." - }, - { - "_id": "210016318", - "date": { - "$date": "2017-11-07T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "87837564", - "reviewer_name": "Xinxian", - "comments": "good value for the money concerning house price in HK today. cosy room in downtown Kowlong close to metro, supermarket, and Yi provides detailed route info and is really helpful. 房价和物价不断飞涨的香港啊,不错的房间,被美女房东打扫干净 。位置靠近深水埗地铁站,生活便利。" - }, - { - "_id": "210973073", - "date": { - "$date": "2017-11-12T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "153751688", - "reviewer_name": "林飞", - "comments": "面积不大,单人够用,干净整洁,性价比极高。" - }, - { - "_id": "213431341", - "date": { - "$date": "2017-11-21T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "150446702", - "reviewer_name": "Christine Joie", - "comments": "Yi is a very accomodating host. The place is exactly just the listing. I like the table where you can work and the window where you can see the buildings light up at night. The closet is huge too. And the kitchen is very accessible and useful." - }, - { - "_id": "216489928", - "date": { - "$date": "2017-12-04T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "125288703", - "reviewer_name": "Wei Yang", - "comments": "Vickey is a nice landlady. The location of the flat is also very good. It only takes a few minutes to walk to metro stations. You can buy everything you want just go downstairs. The bedroom, kitchen and shower room are tidy and well-prepared. Highly recommended!" - }, - { - "_id": "217360212", - "date": { - "$date": "2017-12-09T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "74939004", - "reviewer_name": "玉鑫", - "comments": "姐姐人很好,给我发的路线图也很详细,很容易就能找到。房子离旺角步行20分钟左右,很方便" - }, - { - "_id": "218611375", - "date": { - "$date": "2017-12-14T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "69716457", - "reviewer_name": "Cedric", - "comments": "Very welcoming, heart-warming, and helpful person! Accommodation is nice, small and cosy. Area is also nice, local and easy to get to main and central areas of Hong Kong via transport. Would not hesitate to come back!" - }, - { - "_id": "220144863", - "date": { - "$date": "2017-12-21T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "17232271", - "reviewer_name": "Chris", - "comments": "1、这应该是香港住宿条件最有性价比的民宿了,生活设施一应俱全,周边交通也很方便,靠近深水埗、太子地铁站;\n2、说实话这是我住的体验最不好(不是最差)的民宿,房间有些不太干净,感觉很久没有扫过地和拖过地,落了很多灰,能干净些是最好不过了。另外隔音效果不太好,第一晚到的时候很累,能清晰的听到客厅的电视声音,后面几天家里很安静,特别自由无约束;\n3、房东又瘦又美,很贴心,还借了转换接头借我用,家里一应俱全,啥啥都有;\n4、很感恩房东最后又让我留宿一晚,因为我的行程有变,感谢房东的支持。" - }, - { - "_id": "228491545", - "date": { - "$date": "2018-01-20T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "342635", - "reviewer_name": "Bir", - "comments": "Great comfy bed in a convenient location with easy access to public transport and Kowloon attractions and beyond. Yi is an amazing person with a heart of gold. She went and waited in the ER with me for multiple hours, in the middle of the night, when I had a medical emergency." - }, - { - "_id": "229144245", - "date": { - "$date": "2018-01-22T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "25124200", - "reviewer_name": "Tony", - "comments": "Had a greay stay at Yi's place. If you like shopping, dim sum, or just want to be somewhere conveniently close to the main attractions, her home is perfect! Didn't meet her but we managed to communicate through messenger and she explained everything well. I had no problems checking into her home on my own. Just be sure to take the odd numbered elevator and remember the building door gate is closed after 10pm. Also, the night bus to the airport is a short walk away on Nathan Road!" - }, - { - "_id": "229506330", - "date": { - "$date": "2018-01-24T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "68798873", - "reviewer_name": "Daisy", - "comments": "很整潔,很好的體驗" - }, - { - "_id": "229868405", - "date": { - "$date": "2018-01-26T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "132888757", - "reviewer_name": "策", - "comments": "很方便" - }, - { - "_id": "232087523", - "date": { - "$date": "2018-02-04T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "82421134", - "reviewer_name": "宇杰", - "comments": "我这次过来是为了找朋友玩所以就选择一间性价比 比较高的房子了。 这边的话 下楼大概10分钟的路程就有地铁站了 然后离旺角比较近 挺好的一个地理位置 而且房东人还很好 好相处。 一个人穷游的不错之选。" - }, - { - "_id": "232743175", - "date": { - "$date": "2018-02-06T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "114732583", - "reviewer_name": "Lynn", - "comments": "作为独自旅行的地方 最重要的是安全,Yi的地方很好啊,很安全,离购物中心很近,楼下有各种门店,超方便" - }, - { - "_id": "233104237", - "date": { - "$date": "2018-02-08T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "92693726", - "reviewer_name": "Stefan", - "comments": "Super" - }, - { - "_id": "238555185", - "date": { - "$date": "2018-02-26T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "64887566", - "reviewer_name": "Mixz", - "comments": "It's good. Location near 7-11 and McDonald's. The building have elevator easy to go. The room quite small you might hear peoples talk in sometime. Communications with host quite easy." - }, - { - "_id": "239005096", - "date": { - "$date": "2018-02-28T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "19370175", - "reviewer_name": "Jonathan", - "comments": "Great value with convenient location to public transport and restaurants and shops. Very responsive and the check in process was very clear. It was too bad I didn’t get to meet the host as it sounds like she is a great host." - }, - { - "_id": "254998138", - "date": { - "$date": "2018-04-19T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "161693235", - "reviewer_name": "Regina", - "comments": "Yi is a nice and easy going host. It feels like home being there. Location is excellent and easy access to everywhere. It is a great place to stay If you want to experience the local living style in Hong Kong." - }, - { - "_id": "255592135", - "date": { - "$date": "2018-04-21T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "131411790", - "reviewer_name": "凤鸣", - "comments": "挺方便的。也不难找。房间也很干净" - }, - { - "_id": "257653692", - "date": { - "$date": "2018-04-27T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "181013770", - "reviewer_name": "Jen", - "comments": "房东超级漂亮,人美心善,check out以后还让我待了一段时间等飞机。房间位置离地铁站还是挺近的,楼下很多吃的和商店很方便,性价比很不错。" - }, - { - "_id": "261176087", - "date": { - "$date": "2018-05-06T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "18835621", - "reviewer_name": "Ty", - "comments": "Good room for value. Located within easy walking distance of a number of MRT stations. Host was helpful and accommodating." - }, - { - "_id": "264478930", - "date": { - "$date": "2018-05-14T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "20043852", - "reviewer_name": "JunJie", - "comments": "地段好,夜晚安静,楼下吃饭方便,性价比高,单人入住非常合适!" - }, - { - "_id": "265367299", - "date": { - "$date": "2018-05-17T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "38641933", - "reviewer_name": "József", - "comments": "Everything went smooth, communications were great. The place is close to the public transportation. You get exactly what you are paying for. :)" - }, - { - "_id": "270349155", - "date": { - "$date": "2018-05-29T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "186879651", - "reviewer_name": "Lina", - "comments": "Yi的房子地理位置很好,距离深水埗地铁站5分钟脚程,楼下附近就有巴士站,出门左拐就能看到添好运,可以去吃早餐。大厦有保卫很安全,房间大小正合适,有窗户和冷气,只有有一点潮的味道,可以使用厨房和卫生间,很方便。房东人很好" - }, - { - "_id": "272009016", - "date": { - "$date": "2018-06-03T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "128793052", - "reviewer_name": "Daisy", - "comments": "因为是一个人旅行,刚开始真的慌张得不行,但还是抱着试一试的态度开始了一周的旅行。遇到的每个人都超好的,周边吃的很多,相比较来说物价也没有那么高,步行五分钟左右就可以到地铁站和机场巴士站,附近还有添好运。不用太担心安全问题,楼下伯伯们人都很好很负责,而且大家真的都很热情,都不想离开香港了T^T房东姐姐人超好的,一定要再去找姐姐玩~" - }, - { - "_id": "276180451", - "date": { - "$date": "2018-06-13T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "54557861", - "reviewer_name": "Alison", - "comments": "Yi is a very nice and helpful host. Would have rated higher if the place was more clean." - }, - { - "_id": "278404800", - "date": { - "$date": "2018-06-18T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "51366060", - "reviewer_name": "Liao", - "comments": "hospitality host. good location where you can experience local life. about 5 minutes walk to the subway station. safe and quiet little room." - }, - { - "_id": "280278705", - "date": { - "$date": "2018-06-23T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "106556711", - "reviewer_name": "Chris", - "comments": "Very minimal, but the location and the cooling are really good (crucial in hot and humid Hong Kong)" - }, - { - "_id": "280702731", - "date": { - "$date": "2018-06-24T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "174874739", - "reviewer_name": "Eva", - "comments": "地点很方便找,楼下就有很多餐馆,大厦很不错,房间都很不错很干净,美女房东很好。" - }, - { - "_id": "283032207", - "date": { - "$date": "2018-06-29T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "43461122", - "reviewer_name": "Alexander", - "comments": "Very good location. Perfect room for my needs.\nThe owner sent very clear instructions for check-in, so it all went very smooth.\nI would rent there again." - }, - { - "_id": "285139572", - "date": { - "$date": "2018-07-03T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "79480294", - "reviewer_name": "Nico", - "comments": "Yi is a wonderful host. She was very easy to communicate with and gave me detailed instructions on how to get to the apartment. She also let me check-in earlier than the listed time since my flight arrived early.\n\nAs for the place, what you get is a simple, cozy room that’s perfect for solo travelers. It is spacious enough to have room for luggage and there is a desk where I was able to comfortably work. The room has very good air conditioning that keeps the room cool despite the hot and humid weather in Hong Kong.\n\nIt is very close to MTR and bus stations so it’s easy to get around if you feel like exploring. There are also shops and convenience stores nearby if you ever decide to grab a quick snack.\n\nI had a very pleasant stay at Yi’s place during the 3 nights I spent in Hong Kong." - }, - { - "_id": "286713643", - "date": { - "$date": "2018-07-07T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "7455997", - "reviewer_name": "Jens", - "comments": "could not have found a better place ,very central location and easy going ...ll be back ...yi is super nice and efficient ..thanks for the stay" - }, - { - "_id": "287217032", - "date": { - "$date": "2018-07-08T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "189548919", - "reviewer_name": "Christopher", - "comments": "Yi was great about communicating and provided clear instructions on how to get there and check-in." - }, - { - "_id": "291523455", - "date": { - "$date": "2018-07-16T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "13620385", - "reviewer_name": "Shuji", - "comments": "シャワーと洗面所とトイレが2m x 1mの狭い空間に詰め込まれていて、それをオーナーとシェアするので大変に不便でした。またタオルは持参というのも困りました。お薦め出来ません。" - }, - { - "_id": "298891129", - "date": { - "$date": "2018-07-30T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "41766801", - "reviewer_name": "Astrid", - "comments": "Great location and nice, safe, and private room. A/C in the room was so handy during the summer months. I was here for a week and it felt like a little piece of home away from home. Yi is a great host! " - }, - { - "_id": "301289546", - "date": { - "$date": "2018-08-04T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "82192454", - "reviewer_name": "Yuncen", - "comments": "最好带口罩和眼罩 冷风会吹到脸上。枕头好像没有换枕套,我带了毛巾,wg值得是非常值得的,位置也好" - }, - { - "_id": "318947082", - "date": { - "$date": "2018-09-05T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "210108952", - "reviewer_name": "Janaggen", - "comments": "Great location, value for money and a welcoming and wonderful host." - }, - { - "_id": "327561184", - "date": { - "$date": "2018-09-24T04:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "74120187", - "reviewer_name": "弘均", - "comments": "香港的居住本來就不大 !" - }, - { - "_id": "357878543", - "date": { - "$date": "2018-12-12T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "207086300", - "reviewer_name": "Tim", - "comments": "Perfekte Lage, um Hongkong im Ganzen kennenzulernen, mit kleinen Bäckereien, Märkten und lokalen Restaurants in der direkten Umgebung.\nDie Wohnung ist verhältnismäßig groß und es gibt alles, was mach braucht. Sauberkeit ist noch in Ordnung.\nYi ist immer hilfsbereit und freundlich!\nGerne wieder :)" - }, - { - "_id": "360286223", - "date": { - "$date": "2018-12-20T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "223241104", - "reviewer_name": "Ailsa", - "comments": "Would definately stay here next time I come to Hong Kong!" - }, - { - "_id": "365999054", - "date": { - "$date": "2019-01-02T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "55588758", - "reviewer_name": "Elizabeth", - "comments": "You don't want to leave a bad review when the host is friendly and the price is low, but the truth is that the neighborhood is a slum and the apartment was unclean. I was not able to sleep when I was there because it was cold, and the bed blanket was very thin. I could also hear everything going on in the living room . The neighborhood was depressing. Several people warned me about walking alone in the area late at night. I ended up spending a lot of time and money looking for an alternative place after I arrived in Hong Kong. It was close to New Year's Eve, so I had to return to this place for the last nights of my stay as I could not find an affordable hotel room. I cannot recommend this place." - }, - { - "_id": "369019969", - "date": { - "$date": "2019-01-09T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "48795", - "reviewer_name": "Ellen", - "comments": "First the good things\n The location is very very central, it is very safe, and the doorman is great\n There is a laundry in the building on the entrance floor and it automatically includes soap. The apartment is very quiet for the area. Although I never met Yi in person she was always accessible to communicate. The room was very sparse but functional wifi was good.\n\nThe place has 3 bedrooms, a common living room with a table, a Chinese style kitchen and a very tiny showering area with hot water which is typical of Hong Kong apartments. There is a functional TV. The price is fantastic, cannot be matched.\n\nNow the not so good. The place is the most dirty place I have ever stayed at in HK, and at one point I lived for many years in HK. Yi mentioned the cleaning fee was for the bedrooms only and also no towel is included, which I believe she listed but I did not notice. The place is good for backpackers and undergraduates. The entire week I was there 4 to 5 men and women stayed there and I can state the bathroom was never cleaned. The kitchen looks like a tornado hit it, and some of the grime is that thick amber tacky stuff from years of grease buildup. The living room is a thick dusty mess, the sheet covering the couch reeks. I had no idea someone, an airline stewardess who I never saw, had her personal things there and in fact the apartment was her home. I borrowed a mirror left out on the table, and ate some crackers that I mistakenly thought were left by previous guests and it caused huge problems but I immediately returned the mirror and replaced the crackers.\n\nAll in all a mixed experience. I am sure others saw what I saw, but did not mention the messy aspect in their reviews. I am not being picky, but I wish I had know before I booked what I would actually encounter.\n\n Still it is a good deal, best walked into with your eyes wide open and a high tolerance for filthy common areas." - }, - { - "_id": "400087952", - "date": { - "$date": "2019-01-11T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "233481626", - "reviewer_name": "Li", - "comments": "地理位置方便,房东耐心,人美心善!" - }, - { - "_id": "402007119", - "date": { - "$date": "2019-01-17T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "38063545", - "reviewer_name": "Andy", - "comments": "." - }, - { - "_id": "402572546", - "date": { - "$date": "2019-01-19T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "234227205", - "reviewer_name": "Jason", - "comments": "很棒的一次體驗" - }, - { - "_id": "403896707", - "date": { - "$date": "2019-01-22T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "6469979", - "reviewer_name": "William", - "comments": "this is a cool little place - has a lounge - and kitchen! would definitely go back here for sure!" - }, - { - "_id": "406308919", - "date": { - "$date": "2019-01-29T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "6259521", - "reviewer_name": "Rhianwen", - "comments": "This is a good, affordable place to stay while exploring Hong Kong for a few days, especially if you're going to be out and about a lot. I didn't see much of my host but she was very helpful and gave me tips about hiking trails." - }, - { - "_id": "410943378", - "date": { - "$date": "2019-02-11T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "230887763", - "reviewer_name": "剑雄", - "comments": "唯一不方便的就是卫生间太小,真的好挤。总得来说性价比还是很高,楼下地铁站,中国银行,便利店,超市,麦当劳,茶餐厅,全都有。还有24小时自助洗衣店,用了一次很满意,算是惊喜发现。房间有3个,不过大家都很忙,只打过一次照面,没见过房东,回复很热情,有详细的入住路线图。" - }, - { - "_id": "414539735", - "date": { - "$date": "2019-02-20T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "242014718", - "reviewer_name": "鼎", - "comments": "交通便利,吃饭方便,房东指导到位。\n如果你只想出这点钱,又想要个私人空间,那这里是不二之选。 推荐" - }, - { - "_id": "417084196", - "date": { - "$date": "2019-02-26T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "97476281", - "reviewer_name": "SiewKueen", - "comments": "I stayed 5 nights in this apartment. Location is very close to Prince Edward Station (exit A; Tsuen Wan Line) and Shek Kip Mei Station (exit A; Kwun Tong Line) so it's very convenient. There are supermarkets, bakeries, and local eateries around the area.  Laundry facilities are located within the building. There is a lift to the apartment, which is a relief for travelers with heavy luggage. Inside the apartment, there's a kitchenette, which is good for getting hot or boiled water. Wifi connection is strong within apartment. Inside the bathroom, a shower curtain separates the shower and toilet areas. Hence, some people might feel cramped while showering. The room that I booked has windows and is equipped with strong aircon. Overall, I enjoyed my stay in this apartment. This is a very good area to experience local Hong Kong life where one can see elderly folks, mummies with children, blue collar workers, and of course office workers mingling with each other. Though this area in Sham Shui Po is not posh, its down to earth vibes makes this place very real. " - }, - { - "_id": "417991701", - "date": { - "$date": "2019-03-01T05:00:00.000Z" - }, - "listing_id": "10084023", - "reviewer_id": "244096303", - "reviewer_name": "雯", - "comments": "真的是一个性价比超高的体验,非常适合一个人单独居住。位置很棒,很方便就在荃湾线上离市中心很近。Yi只有一面之缘,不过真的是个很漂亮很nice女孩,房间很小不够还不错啦~" - } - ] - } - ] -} diff --git a/packages/compass-import-export/package.json b/packages/compass-import-export/package.json index 49cba9cf1d2..6d6d344e4a8 100644 --- a/packages/compass-import-export/package.json +++ b/packages/compass-import-export/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "7.37.0", + "version": "7.38.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -49,23 +49,23 @@ }, "dependencies": { "@electron/remote": "^2.1.2", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "debug": "^4.3.4", - "electron": "^29.4.5", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", - "hadron-ipc": "^3.2.20", + "electron": "^30.4.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", + "hadron-ipc": "^3.2.21", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "mongodb-schema": "^12.2.0", @@ -78,9 +78,9 @@ "strip-bom-stream": "^4.0.0" }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-import-export/src/components/import-modal.tsx b/packages/compass-import-export/src/components/import-modal.tsx index 66ab6f583fe..b16ebf65867 100644 --- a/packages/compass-import-export/src/components/import-modal.tsx +++ b/packages/compass-import-export/src/components/import-modal.tsx @@ -274,7 +274,7 @@ function ImportModal({ const mapStateToProps = (state: RootImportState) => ({ ns: state.import.namespace, isOpen: state.import.isOpen, - errors: state.import.errors, + errors: state.import.firstErrors, fileType: state.import.fileType, fileName: state.import.fileName, status: state.import.status, diff --git a/packages/compass-import-export/src/import/import-csv.spec.ts b/packages/compass-import-export/src/import/import-csv.spec.ts index c498c5b4f0e..a97d715ebf0 100644 --- a/packages/compass-import-export/src/import/import-csv.spec.ts +++ b/packages/compass-import-export/src/import/import-csv.spec.ts @@ -113,19 +113,9 @@ describe('importCSV', function () { }); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: totalRows, docsWritten: totalRows, - dbErrors: [], - dbStats: { - insertedCount: totalRows, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: Math.ceil(totalRows / 1000), - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -264,19 +254,9 @@ describe('importCSV', function () { }); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: totalRows, docsWritten: totalRows, - dbErrors: [], - dbStats: { - insertedCount: totalRows, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: Math.ceil(totalRows / 1000), - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -356,19 +336,9 @@ describe('importCSV', function () { expect(errorLog).to.equal(''); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: totalRows, docsWritten: totalRows, - dbErrors: [], - dbStats: { - insertedCount: totalRows, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: Math.ceil(totalRows / 1000), - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -429,19 +399,9 @@ describe('importCSV', function () { expect(errorLog).to.equal(''); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: 2000, docsWritten: 2000, - dbErrors: [], - dbStats: { - insertedCount: 2000, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 2, // expected two batches - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -670,7 +630,12 @@ describe('importCSV', function () { errorCallback, }); - expect(result.dbStats.insertedCount).to.equal(1); + expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 2, + docsProcessed: 3, + docsWritten: 1, + hasUnboundArray: false, + }); expect(progressCallback.callCount).to.equal(3); expect(errorCallback.callCount).to.equal(2); @@ -778,43 +743,45 @@ describe('importCSV', function () { errorCallback, }); - expect(result.dbStats.insertedCount).to.equal(0); + expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 3, + docsProcessed: 3, + docsWritten: 0, + hasUnboundArray: false, + }); expect(progressCallback.callCount).to.equal(3); - expect(errorCallback.callCount).to.equal(1); // once for the batch + expect(errorCallback.callCount).to.equal(3); const expectedErrors: ErrorJSON[] = [ { - name: 'MongoBulkWriteError', + name: 'WriteError', message: 'Document failed validation', + index: 0, + code: 121, + }, + { + name: 'WriteError', + message: 'Document failed validation', + index: 1, + code: 121, + }, + { + name: 'WriteError', + message: 'Document failed validation', + index: 2, code: 121, - numErrors: 3, }, ]; const errors = errorCallback.args.map((args) => args[0]); + for (const [index, error] of errors.entries()) { + expect(error.op).to.exist; + // cheat and copy them over because it is big and with buffers + expectedErrors[index].op = error.op; + } expect(errors).to.deep.equal(expectedErrors); - // the log file has one for each error in the bulk write too - expectedErrors.push({ - name: 'WriteConcernError', - message: 'Document failed validation', - index: 0, - code: 121, - }); - expectedErrors.push({ - name: 'WriteConcernError', - message: 'Document failed validation', - index: 1, - code: 121, - }); - expectedErrors.push({ - name: 'WriteConcernError', - message: 'Document failed validation', - index: 2, - code: 121, - }); - const errorsText = await fs.promises.readFile(output.path, 'utf8'); expect(errorsText).to.equal(formatErrorLines(expectedErrors)); }); @@ -842,19 +809,9 @@ describe('importCSV', function () { // only looked at the first row because we aborted before even starting expect(omit(result, 'biggestDocSize')).to.deep.equal({ aborted: true, + docsErrored: 0, docsProcessed: 0, docsWritten: 0, - dbErrors: [], - dbStats: { - insertedCount: 0, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 0, - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); }); diff --git a/packages/compass-import-export/src/import/import-csv.ts b/packages/compass-import-export/src/import/import-csv.ts index bb304542c38..4d730e35d94 100644 --- a/packages/compass-import-export/src/import/import-csv.ts +++ b/packages/compass-import-export/src/import/import-csv.ts @@ -1,47 +1,91 @@ -import { Transform } from 'stream'; -import { pipeline } from 'stream/promises'; -import type { Readable, Writable } from 'stream'; +import type { Document } from 'bson'; +import type { Readable } from 'stream'; import Papa from 'papaparse'; import toNS from 'mongodb-ns'; -import type { DataService } from 'mongodb-data-service'; -import stripBomStream from 'strip-bom-stream'; -import { createCollectionWriteStream } from '../utils/collection-stream'; import { makeDocFromCSV, parseCSVHeaderName } from '../csv/csv-utils'; -import { - DocStatsStream, - makeImportResult, - processParseError, - processWriteStreamErrors, -} from './import-utils'; +import { doImport } from './import-utils'; import type { Delimiter, Linebreak, IncludedFields, PathPart, } from '../csv/csv-types'; -import type { ImportResult, ErrorJSON, ImportProgress } from './import-types'; +import type { ImportResult, ImportOptions } from './import-types'; import { createDebug } from '../utils/logger'; -import { Utf8Validator } from '../utils/utf8-validator'; -import { ByteCounter } from '../utils/byte-counter'; const debug = createDebug('import-csv'); -type ImportCSVOptions = { - dataService: Pick; - ns: string; +type ImportCSVOptions = ImportOptions & { input: Readable; - output?: Writable; - abortSignal?: AbortSignal; - progressCallback?: (progress: ImportProgress) => void; - errorCallback?: (error: ErrorJSON) => void; delimiter?: Delimiter; newline: Linebreak; ignoreEmptyStrings?: boolean; - stopOnErrors?: boolean; fields: IncludedFields; // the type chosen by the user to make each field }; +class CSVTransformer { + fields: IncludedFields; + ignoreEmptyStrings?: boolean; + headerFields: string[]; + parsedHeader?: Record; + + constructor({ + fields, + ignoreEmptyStrings, + }: { + fields: IncludedFields; + ignoreEmptyStrings?: boolean; + }) { + this.fields = fields; + this.ignoreEmptyStrings = ignoreEmptyStrings; + this.headerFields = []; + } + + addHeaderField(field: string) { + this.headerFields.push(field); + } + + transform(row: Record): Document { + if (!this.parsedHeader) { + // There's a quirk in papaparse where it calls transformHeader() + // before it finishes auto-detecting the line endings. We could pass + // in a line ending that we previously detected (in guessFileType(), + // perhaps?) or we can just strip the extra \r from the final header + // name if it exists. + if (this.headerFields.length) { + const fixupFrom = this.headerFields[this.headerFields.length - 1]; + const fixupTo = fixupFrom.replace(/\r$/, ''); + this.headerFields[this.headerFields.length - 1] = fixupTo; + } + + this.parsedHeader = {}; + for (const name of this.headerFields) { + this.parsedHeader[name] = parseCSVHeaderName(name); + } + + // TODO(COMPASS-7158): make sure array indexes start at 0 and have no + // gaps, otherwise clean them up (ie. treat those parts as part of the + // field name). So that you can't have a foo[1000000] + // edge case. + } + + return makeDocFromCSV( + row, + this.headerFields, + this.parsedHeader, + this.fields, + { + ignoreEmptyStrings: this.ignoreEmptyStrings, + } + ); + } + + lineAnnotation(numProcessed: number): string { + return `[Row ${numProcessed}]`; + } +} + export async function importCSV({ dataService, ns, @@ -58,82 +102,12 @@ export async function importCSV({ }: ImportCSVOptions): Promise { debug('importCSV()', { ns: toNS(ns), stopOnErrors }); - const byteCounter = new ByteCounter(); - - let numProcessed = 0; - const headerFields: string[] = []; // will be filled via transformHeader callback below - let parsedHeader: Record; - if (ns === 'test.compass-import-abort-e2e-test') { // Give the test more than enough time to click the abort before we continue. await new Promise((resolve) => setTimeout(resolve, 3000)); } - const docStream = new Transform({ - objectMode: true, - transform: function (chunk: Record, encoding, callback) { - if (!parsedHeader) { - // There's a quirk in papaparse where it calls transformHeader() - // before it finishes auto-detecting the line endings. We could pass - // in a line ending that we previously detected (in guessFileType(), - // perhaps?) or we can just strip the extra \r from the final header - // name if it exists. - if (headerFields.length) { - const fixupFrom = headerFields[headerFields.length - 1]; - const fixupTo = fixupFrom.replace(/\r$/, ''); - headerFields[headerFields.length - 1] = fixupTo; - } - - parsedHeader = {}; - for (const name of headerFields) { - parsedHeader[name] = parseCSVHeaderName(name); - } - - // TODO(COMPASS-7158): make sure array indexes start at 0 and have no - // gaps, otherwise clean them up (ie. treat those parts as part of the - // field name). So that you can't have a foo[1000000] - // edge case. - } - - // Call progress and increase the number processed even if it errors - // below. The collection write stream stats at the end stores how many - // got written. This way progress updates continue even if every row - // fails to parse. - ++numProcessed; - if (!abortSignal?.aborted) { - progressCallback?.({ - bytesProcessed: byteCounter.total, - docsProcessed: numProcessed, - docsWritten: collectionStream.docsWritten, - }); - } - - try { - const doc = makeDocFromCSV(chunk, headerFields, parsedHeader, fields, { - ignoreEmptyStrings, - }); - callback(null, doc); - } catch (err: unknown) { - processParseError({ - annotation: `[Row ${numProcessed}]`, - stopOnErrors, - err, - output, - errorCallback, - callback, - }); - } - }, - }); - - const docStatsStream = new DocStatsStream(); - - const collectionStream = createCollectionWriteStream( - dataService, - ns, - stopOnErrors ?? false, - errorCallback - ); + const transformer = new CSVTransformer({ fields, ignoreEmptyStrings }); const parseStream = Papa.parse(Papa.NODE_STREAM_INPUT, { delimiter, @@ -141,66 +115,20 @@ export async function importCSV({ header: true, transformHeader: function (header: string, index: number): string { debug('importCSV:transformHeader', header, index); - headerFields.push(header); + transformer.addHeaderField(header); return header; }, }); - const params = [ - input, - new Utf8Validator(), - byteCounter, - stripBomStream(), - parseStream, - docStream, - docStatsStream, - collectionStream, - ...(abortSignal ? [{ signal: abortSignal }] : []), - ] as const; - - try { - await pipeline(...params); - } catch (err: any) { - if (err.code === 'ABORT_ERR') { - debug('importCSV:aborting'); - - await processWriteStreamErrors({ - collectionStream, - output, - }); - - const result = makeImportResult( - collectionStream, - numProcessed, - docStatsStream, - true - ); - debug('importCSV:aborted', result); - return result; - } - - // stick the result onto the error so that we can tell how far it got - err.result = makeImportResult( - collectionStream, - numProcessed, - docStatsStream - ); - - throw err; - } - - debug('importCSV:completing'); + const streams = [parseStream]; - await processWriteStreamErrors({ - collectionStream, + return await doImport(input, streams, transformer, { + dataService, + ns, output, + abortSignal, + progressCallback, + errorCallback, + stopOnErrors, }); - - const result = makeImportResult( - collectionStream, - numProcessed, - docStatsStream - ); - debug('importCSV:completed', result); - return result; } diff --git a/packages/compass-import-export/src/import/import-json.spec.ts b/packages/compass-import-export/src/import/import-json.spec.ts index 74c20d55fb4..a348e6814a9 100644 --- a/packages/compass-import-export/src/import/import-json.spec.ts +++ b/packages/compass-import-export/src/import/import-json.spec.ts @@ -116,19 +116,9 @@ describe('importJSON', function () { }); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsWritten: totalRows, docsProcessed: totalRows, - dbErrors: [], - dbStats: { - insertedCount: totalRows, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: Math.ceil(totalRows / 1000), - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -186,19 +176,9 @@ describe('importJSON', function () { }); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: 1, docsWritten: 1, - dbErrors: [], - dbStats: { - insertedCount: 1, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 1, - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -236,19 +216,9 @@ describe('importJSON', function () { }); expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 0, docsProcessed: 2000, docsWritten: 2000, - dbErrors: [], - dbStats: { - insertedCount: 2000, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 2, // expected two batches - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); @@ -470,7 +440,12 @@ describe('importJSON', function () { errorCallback, }); - expect(result.dbStats.insertedCount).to.equal(1); + expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 1, + docsProcessed: 2, + docsWritten: 1, + hasUnboundArray: false, + }); expect(progressCallback.callCount).to.equal(2); expect(errorCallback.callCount).to.equal(1); @@ -552,37 +527,39 @@ describe('importJSON', function () { errorCallback, }); - expect(result.dbStats.insertedCount).to.equal(0); + expect(omit(result, 'biggestDocSize')).to.deep.equal({ + docsErrored: 2, + docsProcessed: 2, + docsWritten: 0, + hasUnboundArray: false, + }); expect(progressCallback.callCount).to.equal(2); - expect(errorCallback.callCount).to.equal(1); // once for the batch + expect(errorCallback.callCount).to.equal(2); const expectedErrors: ErrorJSON[] = [ { - name: 'MongoBulkWriteError', + name: 'WriteError', + message: 'Document failed validation', + index: 0, + code: 121, + }, + { + name: 'WriteError', message: 'Document failed validation', + index: 1, code: 121, - numErrors: 2, }, ]; const errors = errorCallback.args.map((args) => args[0]); + for (const [index, error] of errors.entries()) { + expect(error.op).to.exist; + // cheat and copy them over because it is big and with buffers + expectedErrors[index].op = error.op; + } expect(errors).to.deep.equal(expectedErrors); - // the log file has one for each error in the bulk write too - expectedErrors.push({ - name: 'WriteConcernError', - message: 'Document failed validation', - index: 0, - code: 121, - }); - expectedErrors.push({ - name: 'WriteConcernError', - message: 'Document failed validation', - index: 1, - code: 121, - }); - const errorsText = await fs.promises.readFile(output.path, 'utf8'); expect(errorsText).to.equal(formatErrorLines(expectedErrors)); }); @@ -608,19 +585,9 @@ describe('importJSON', function () { // only looked at the first row because we aborted before even starting expect(omit(result, 'biggestDocSize')).to.deep.equal({ aborted: true, + docsErrored: 0, docsProcessed: 0, docsWritten: 0, - dbErrors: [], - dbStats: { - insertedCount: 0, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 0, - writeConcernErrors: [], - writeErrors: [], - }, hasUnboundArray: false, }); }); diff --git a/packages/compass-import-export/src/import/import-json.ts b/packages/compass-import-export/src/import/import-json.ts index 344b78b595f..7b638a782ef 100644 --- a/packages/compass-import-export/src/import/import-json.ts +++ b/packages/compass-import-export/src/import/import-json.ts @@ -1,162 +1,79 @@ import { EJSON } from 'bson'; -import { Transform } from 'stream'; -import { pipeline } from 'stream/promises'; -import type { Readable, Writable } from 'stream'; +import type { Readable } from 'stream'; import toNS from 'mongodb-ns'; -import type { DataService } from 'mongodb-data-service'; import Parser from 'stream-json/Parser'; import StreamArray from 'stream-json/streamers/StreamArray'; import StreamValues from 'stream-json/streamers/StreamValues'; -import stripBomStream from 'strip-bom-stream'; -import { - DocStatsStream, - makeImportResult, - processParseError, - processWriteStreamErrors, -} from './import-utils'; -import type { ImportResult, ErrorJSON, ImportProgress } from './import-types'; -import { createCollectionWriteStream } from '../utils/collection-stream'; +import { doImport } from './import-utils'; +import type { ImportOptions, ImportResult } from './import-types'; import { createDebug } from '../utils/logger'; -import { Utf8Validator } from '../utils/utf8-validator'; -import { ByteCounter } from '../utils/byte-counter'; const debug = createDebug('import-json'); type JSONVariant = 'json' | 'jsonl'; -type ImportJSONOptions = { - dataService: Pick; - ns: string; +type ImportJSONOptions = ImportOptions & { input: Readable; - output?: Writable; - abortSignal?: AbortSignal; - progressCallback?: (progress: ImportProgress) => void; - errorCallback?: (error: ErrorJSON) => void; - stopOnErrors?: boolean; jsonVariant: JSONVariant; }; +class JSONTransformer { + transform(chunk: any) { + // make sure files parsed as jsonl only contain objects with no arrays and simple values + // (this will either stop the entire import and throw or just skip this + // one value depending on the value of stopOnErrors) + if (Object.prototype.toString.call(chunk.value) !== '[object Object]') { + throw new Error('Value is not an object'); + } + + return EJSON.deserialize(chunk.value as Document, { + relaxed: false, + }); + } + + lineAnnotation(numProcessed: number): string { + return ` [Index ${numProcessed - 1}]`; + } +} + export async function importJSON({ dataService, ns, - input, output, abortSignal, progressCallback, errorCallback, stopOnErrors, + input, jsonVariant, }: ImportJSONOptions): Promise { debug('importJSON()', { ns: toNS(ns) }); - const byteCounter = new ByteCounter(); - - let numProcessed = 0; - if (ns === 'test.compass-import-abort-e2e-test') { // Give the test more than enough time to click the abort before we continue. await new Promise((resolve) => setTimeout(resolve, 3000)); } - const docStream = new Transform({ - objectMode: true, - transform: function (chunk: any, encoding, callback) { - ++numProcessed; - if (!abortSignal?.aborted) { - progressCallback?.({ - bytesProcessed: byteCounter.total, - docsProcessed: numProcessed, - docsWritten: collectionStream.docsWritten, - }); - } - try { - // make sure files parsed as jsonl only contain objects with no arrays and simple values - // (this will either stop the entire import and throw or just skip this - // one value depending on the value of stopOnErrors) - if (Object.prototype.toString.call(chunk.value) !== '[object Object]') { - throw new Error('Value is not an object'); - } - - const doc = EJSON.deserialize(chunk.value as Document, { - relaxed: false, - }); - callback(null, doc); - } catch (err: unknown) { - processParseError({ - annotation: ` [Index ${numProcessed - 1}]`, - stopOnErrors, - err, - output, - errorCallback, - callback, - }); - } - }, - }); + const transformer = new JSONTransformer(); - const docStatsStream = new DocStatsStream(); + const streams = []; - const collectionStream = createCollectionWriteStream( - dataService, - ns, - stopOnErrors ?? false, - errorCallback - ); - - const parserStreams = []; if (jsonVariant === 'jsonl') { - parserStreams.push( - Parser.parser({ jsonStreaming: true }), - StreamValues.streamValues() - ); + streams.push(Parser.parser({ jsonStreaming: true })); + streams.push(StreamValues.streamValues()); } else { - parserStreams.push(Parser.parser(), StreamArray.streamArray()); - } - - try { - await pipeline( - [ - input, - new Utf8Validator(), - byteCounter, - stripBomStream(), - ...parserStreams, - docStream, - docStatsStream, - collectionStream, - ], - ...(abortSignal ? [{ signal: abortSignal }] : []) - ); - } catch (err: any) { - if (err.code === 'ABORT_ERR') { - await processWriteStreamErrors({ - collectionStream, - output, - }); - - return makeImportResult( - collectionStream, - numProcessed, - docStatsStream, - true - ); - } - - // stick the result onto the error so that we can tell how far it got - err.result = makeImportResult( - collectionStream, - numProcessed, - docStatsStream - ); - - throw err; + streams.push(Parser.parser()); + streams.push(StreamArray.streamArray()); } - await processWriteStreamErrors({ - collectionStream, + return await doImport(input, streams, transformer, { + dataService, + ns, output, + abortSignal, + progressCallback, + errorCallback, + stopOnErrors, }); - - return makeImportResult(collectionStream, numProcessed, docStatsStream); } diff --git a/packages/compass-import-export/src/import/import-types.ts b/packages/compass-import-export/src/import/import-types.ts index 5dfb678cb02..252263298cb 100644 --- a/packages/compass-import-export/src/import/import-types.ts +++ b/packages/compass-import-export/src/import/import-types.ts @@ -1,16 +1,23 @@ import type { Document } from 'bson'; -import type { - CollectionStreamStats, - CollectionStreamProgressError, -} from '../utils/collection-stream'; +import type { DataService } from 'mongodb-data-service'; +import type { Writable } from 'stream'; + +export type ImportOptions = { + dataService: Pick; + ns: string; + output?: Writable; + abortSignal?: AbortSignal; + progressCallback?: (progress: ImportProgress) => void; + errorCallback?: (error: ErrorJSON) => void; + stopOnErrors?: boolean; +}; export type ImportResult = { aborted?: boolean; - dbErrors: CollectionStreamProgressError[]; - dbStats: CollectionStreamStats; docsWritten: number; docsProcessed: number; + docsErrored: number; biggestDocSize: number; hasUnboundArray: boolean; }; diff --git a/packages/compass-import-export/src/import/import-utils.spec.ts b/packages/compass-import-export/src/import/import-utils.spec.ts index 5e3329f296a..312d62d3c99 100644 --- a/packages/compass-import-export/src/import/import-utils.spec.ts +++ b/packages/compass-import-export/src/import/import-utils.spec.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; -import { Readable, Writable } from 'stream'; -import { pipeline } from 'stream/promises'; +import { Readable } from 'stream'; +import type { Document } from 'bson'; -import { DocStatsStream } from './import-utils'; +import { DocStatsCollector } from './import-utils'; const SIMPLE_DOC_1 = { name: 'Compass', @@ -79,55 +79,26 @@ const createMockReadable = (readFn?: (readable: Readable) => void) => { }); }; -const createMockWritable = ( - writeFn = ( - c: any, - e: string, - callback: (error?: Error, chunk?: any) => void - ) => callback() -) => - new Writable({ - objectMode: true, - write: writeFn, - }); +async function iteratePipeline( + stream: Readable, + docStatsCollector: DocStatsCollector +) { + for await (const doc of stream) { + docStatsCollector.collect(doc as Document); + } +} describe('import-utils', function () { - describe('DocStatsStream', function () { + describe('DocStatsCollector', function () { it('should track the size of biggest doc encountered', async function () { - const docStatsStream = new DocStatsStream(); - await pipeline([ - createMockReadable(), - docStatsStream, - createMockWritable(), - ]); + const docStatsCollector = new DocStatsCollector(); + await iteratePipeline(createMockReadable(), docStatsCollector); - expect(docStatsStream.getStats().biggestDocSize).to.equal( + expect(docStatsCollector.getStats().biggestDocSize).to.equal( JSON.stringify(COMPLEX_DOC).length ); }); - it('should pass through the input unaltered', async function () { - const mockReadableStream = createMockReadable(function ( - readable: Readable - ) { - readable.push(COMPLEX_DOC); - readable.push(null); - }); - - const docStatsStream = new DocStatsStream(); - - const mockWritableStream = createMockWritable(function ( - chunk, - encoding, - callback - ) { - expect(chunk).to.deep.equal(COMPLEX_DOC); - callback(); - }); - - await pipeline([mockReadableStream, docStatsStream, mockWritableStream]); - }); - context('when there is an error while calculating doc stats', function () { it('should pass through the doc without throwing an error', async function () { // Circular reference will fail JSON.stringify @@ -143,25 +114,11 @@ describe('import-utils', function () { readable.push(null); }); - const docStatsStream = new DocStatsStream(); - - const mockWritableStream = createMockWritable(function ( - chunk, - encoding, - callback - ) { - expect(chunk).to.deep.equal(CIRCULAR_REF_DOC); - callback(); - }); - - await pipeline([ - mockReadableStream, - docStatsStream, - mockWritableStream, - ]); + const docStatsCollector = new DocStatsCollector(); + await iteratePipeline(mockReadableStream, docStatsCollector); // Since the stringify will fail we will always have doc size set to 0 - expect(docStatsStream.getStats().biggestDocSize).to.equal(0); + expect(docStatsCollector.getStats().biggestDocSize).to.equal(0); }); }); }); diff --git a/packages/compass-import-export/src/import/import-utils.ts b/packages/compass-import-export/src/import/import-utils.ts index d61448af016..4100b94bfaf 100644 --- a/packages/compass-import-export/src/import/import-utils.ts +++ b/packages/compass-import-export/src/import/import-utils.ts @@ -1,29 +1,29 @@ import os from 'os'; -import { Transform } from 'stream'; -import type { Writable } from 'stream'; - -import type { ImportResult, ErrorJSON } from './import-types'; - -import type { WritableCollectionStream } from '../utils/collection-stream'; - +import type { Document } from 'bson'; +import type { Readable, Writable, Duplex } from 'stream'; +import { addAbortSignal } from 'stream'; +import type { ImportResult, ErrorJSON, ImportOptions } from './import-types'; +import { ImportWriter } from './import-writer'; import { createDebug } from '../utils/logger'; +import { Utf8Validator } from '../utils/utf8-validator'; +import { ByteCounter } from '../utils/byte-counter'; +import stripBomStream from 'strip-bom-stream'; const debug = createDebug('import'); export function makeImportResult( - collectionStream: WritableCollectionStream, + importWriter: ImportWriter, numProcessed: number, - docStatsStream: DocStatsStream, + numParseErrors: number, + docStatsStream: DocStatsCollector, aborted?: boolean ): ImportResult { const result: ImportResult = { - dbErrors: collectionStream.getErrors(), - dbStats: collectionStream.getStats(), - docsWritten: collectionStream.docsWritten, + docsErrored: numParseErrors + importWriter.docsErrored, + docsWritten: importWriter.docsWritten, ...docStatsStream.getStats(), - // docsProcessed is not on collectionStream so that it includes docs that - // produced parse errors and therefore never made it to the collection - // stream. + // docsProcessed is not on importWriter so that it includes docs that + // produced parse errors and therefore never made it that far docsProcessed: numProcessed, }; @@ -46,92 +46,19 @@ export function errorToJSON(error: any): ErrorJSON { } } - // For bulk write errors we include the number of errors that were in the - // batch. So one error actually maps to (potentially) many failed documents. - if (error.writeErrors && Array.isArray(error.writeErrors)) { - obj.numErrors = error.writeErrors.length; - } - return obj; } -export async function processWriteStreamErrors({ - collectionStream, - output, -}: { - collectionStream: WritableCollectionStream; - output?: Writable; - errorCallback?: (err: ErrorJSON) => void; -}) { - // This is temporary until we change WritableCollectionStream so it can pipe - // us its errors as they occur. - - const errors = collectionStream.getErrors(); - const stats = collectionStream.getStats(); - const allErrors = errors - .concat(stats.writeErrors) - .concat(stats.writeConcernErrors); - - for (const error of allErrors) { - debug('write error', error); - - const transformedError = errorToJSON(error); - - if (!output) { - continue; - } - - try { - await new Promise((resolve) => { - output.write(JSON.stringify(transformedError) + os.EOL, 'utf8', () => - resolve() - ); - }); - } catch (err: any) { - debug('error while writing error', err); - } - } -} - -export function processParseError({ - annotation, - stopOnErrors, - err, - output, - errorCallback, - callback, -}: { - annotation: string; - stopOnErrors?: boolean; - err: unknown; - output?: Writable; - errorCallback?: (error: ErrorJSON) => void; - callback: (err?: any) => void; -}) { - // rethrow with the line number / array index appended to aid debugging - (err as Error).message = `${(err as Error).message}${annotation}`; - - if (stopOnErrors) { - callback(err as Error); - } else { - const transformedError = errorToJSON(err); - debug('transform error', transformedError); - errorCallback?.(transformedError); - if (output) { - output.write( - JSON.stringify(transformedError) + os.EOL, - 'utf8', - (err: any) => { - if (err) { - debug('error while writing error', err); - } - callback(); - } - ); - } else { - callback(); - } - } +export function writeErrorToLog(output: Writable, error: any): Promise { + return new Promise(function (resolve) { + output.write(JSON.stringify(error) + os.EOL, 'utf8', (err: unknown) => { + if (err) { + debug('error while writing error', err); + } + // we always resolve because we ignore the error + resolve(); + }); + }); } function hasArrayOfLength( @@ -158,31 +85,223 @@ function hasArrayOfLength( type DocStats = { biggestDocSize: number; hasUnboundArray: boolean }; -export class DocStatsStream extends Transform { +export class DocStatsCollector { private stats: DocStats = { biggestDocSize: 0, hasUnboundArray: false }; - constructor() { - super({ - objectMode: true, - transform: (doc, encoding, callback) => { - this.stats.hasUnboundArray = - this.stats.hasUnboundArray || hasArrayOfLength(doc, 250); - try { - const docString = JSON.stringify(doc); - this.stats.biggestDocSize = Math.max( - this.stats.biggestDocSize, - docString.length - ); - } catch (error) { - // We ignore the JSON stringification error - } finally { - callback(null, doc); - } - }, - }); + collect(doc: Document) { + this.stats.hasUnboundArray = + this.stats.hasUnboundArray || hasArrayOfLength(doc, 250); + try { + const docString = JSON.stringify(doc); + this.stats.biggestDocSize = Math.max( + this.stats.biggestDocSize, + docString.length + ); + } catch (error) { + // We ignore the JSON stringification error + } } - getStats(): Readonly { + getStats() { return this.stats; } } + +type Transformer = { + transform: (chunk: any) => Document; + lineAnnotation: (numProcessed: number) => string; +}; + +export async function doImport( + input: Readable, + streams: Duplex[], + transformer: Transformer, + { + dataService, + ns, + output, + abortSignal, + progressCallback, + errorCallback, + stopOnErrors, + }: ImportOptions +): Promise { + const byteCounter = new ByteCounter(); + + let stream: Readable | Duplex; + + const docStatsCollector = new DocStatsCollector(); + + const importWriter = new ImportWriter(dataService, ns, stopOnErrors); + + let numProcessed = 0; + let numParseErrors = 0; + + // Stream errors just get thrown synchronously unless we listen for the event + // on each stream we use in the pipeline. By destroying the stream we're + // iterating on and passing the error, the "for await line" will throw inside + // the try/catch below. Relevant test: "errors if a file is truncated utf8" + function streamErrorListener(error: Error) { + stream.destroy(error); + } + + input.once('error', streamErrorListener); + + stream = input; + + const allStreams = [ + new Utf8Validator(), + byteCounter, + stripBomStream(), + ...streams, + ]; + + for (const s of allStreams) { + stream = stream.pipe(s); + stream.once('error', streamErrorListener); + } + + if (abortSignal) { + stream = addAbortSignal(abortSignal, stream); + } + + try { + for await (const chunk of stream as Readable) { + // Call progress and increase the number processed even if it errors + // below. The import writer stats at the end stores how many got written. + // This way progress updates continue even if every row fails to parse. + ++numProcessed; + if (!abortSignal?.aborted) { + progressCallback?.({ + bytesProcessed: byteCounter.total, + docsProcessed: numProcessed, + docsWritten: importWriter.docsWritten, + }); + } + + let doc: Document; + try { + doc = transformer.transform(chunk); + } catch (err: unknown) { + ++numParseErrors; + // deal with transform error + + // rethrow with the line number / array index appended to aid debugging + (err as Error).message = `${ + (err as Error).message + }${transformer.lineAnnotation(numProcessed)}`; + + if (stopOnErrors) { + throw err; + } else { + const transformedError = errorToJSON(err); + debug('transform error', transformedError); + errorCallback?.(transformedError); + if (output) { + await writeErrorToLog(output, transformedError); + } + } + continue; + } + + docStatsCollector.collect(doc); + + try { + // write + await importWriter.write(doc); + } catch (err: any) { + // if there is no writeErrors property, then it isn't an + // ImportWriteError, so probably not recoverable + if (!err.writeErrors) { + throw err; + } + + // deal with write error + debug('write error', err); + + if (stopOnErrors) { + throw err; + } + + if (!output) { + continue; + } + + const errors = err.writeErrors; + for (const error of errors) { + const transformedError = errorToJSON(error); + errorCallback?.(transformedError); + await writeErrorToLog(output, transformedError); + } + } + } + + input.removeListener('error', streamErrorListener); + for (const s of allStreams) { + s.removeListener('error', streamErrorListener); + } + + // also insert the remaining partial batch + try { + await importWriter.finish(); + } catch (err: any) { + // if there is no writeErrors property, then it isn't an + // ImportWriteError, so probably not recoverable + if (!err.writeErrors) { + throw err; + } + + // deal with write error + debug('write error', err); + + if (stopOnErrors) { + throw err; + } + + if (output) { + const errors = err.writeErrors; + for (const error of errors) { + const transformedError = errorToJSON(error); + errorCallback?.(transformedError); + await writeErrorToLog(output, transformedError); + } + } + } + } catch (err: any) { + if (err.code === 'ABORT_ERR') { + debug('import:aborting'); + + const result = makeImportResult( + importWriter, + numProcessed, + numParseErrors, + docStatsCollector, + true + ); + debug('import:aborted', result); + return result; + } + + // stick the result onto the error so that we can tell how far it got + err.result = makeImportResult( + importWriter, + numProcessed, + numParseErrors, + docStatsCollector + ); + + throw err; + } + + debug('import:completing'); + + const result = makeImportResult( + importWriter, + numProcessed, + numParseErrors, + docStatsCollector + ); + debug('import:completed', result); + + return result; +} diff --git a/packages/compass-import-export/src/import/import-writer.spec.ts b/packages/compass-import-export/src/import/import-writer.spec.ts new file mode 100644 index 00000000000..3cc77653ed2 --- /dev/null +++ b/packages/compass-import-export/src/import/import-writer.spec.ts @@ -0,0 +1,225 @@ +import sinon from 'sinon'; +import { expect } from 'chai'; + +import { ImportWriter } from './import-writer'; + +const BATCH_SIZE = 1000; + +type FakeError = Error & { + result: { + getWriteErrors?: () => Error[]; + }; +}; + +function getDataService({ + isFLE, + throwErrors, +}: { + isFLE: boolean; + throwErrors: boolean; +}) { + return { + bulkWrite: (ns: string, docs: any[] /*, options: any*/) => { + return new Promise((resolve, reject) => { + if (isFLE && docs.length !== 1) { + const error: any = new Error( + 'Only single insert batches are supported in FLE2' + ); + error.code = 6371202; + return reject(error); + } + + if (throwErrors) { + const error = new Error('fake bulkWrite error'); + (error as FakeError).result = { + getWriteErrors: () => { + const errors: Error[] = []; + for (let i = 0; i < docs.length; ++i) { + const writeError = new Error(`Fake error for doc ${i}`); + delete writeError.stack; + errors.push(writeError); + } + return errors; + }, + }; + delete error.stack; // slows down tests due to excess output + return reject(error); + } + + resolve({ + insertedCount: docs.length, + matchedCount: 0, + modifiedCount: 0, + deletedCount: 0, + upsertedCount: 0, + ok: 1, + }); + }); + }, + + insertOne: () => { + if (throwErrors) { + const error = new Error('fake insertOne error'); + delete error.stack; // slows down tests due to excess output + return Promise.reject(error); + } + + return Promise.resolve({ acknowledged: true }); + }, + }; +} + +function getExpectedNumBatches( + numDocs: number, + isFLE: boolean, + stopOnErrors: boolean +) { + if (stopOnErrors) { + return 1; + } + + if (isFLE) { + // one attempted batch at the batch size (followed by insertOne() on retry), then subsequent batches are all size 1. + return numDocs > BATCH_SIZE ? 1 + numDocs - BATCH_SIZE : 1; + } + + return Math.ceil(numDocs / BATCH_SIZE); +} + +function getExpectedDocsInBatch( + batchNum: number, + numDocs: number, + isFLE: boolean +) { + if (batchNum === 1) { + return Math.min(numDocs, BATCH_SIZE); + } + + if (isFLE && batchNum > 1) { + return 1; + } + + const numBatches = getExpectedNumBatches(numDocs, isFLE, false); + + return batchNum < numBatches + ? BATCH_SIZE + : numDocs - (batchNum - 1) * BATCH_SIZE; +} + +describe('ImportWriter', function () { + const docs: { i: number }[] = []; + for (let i = 0; i < BATCH_SIZE * 2 + 1; ++i) { + docs.push({ i }); + } + + for (const isFLE of [true, false]) { + it(`inserts documents ${isFLE ? 'one by one' : 'in batches'} to ${ + isFLE ? 'FLE2' : 'regular' + } collection`, async function () { + const numBatches = getExpectedNumBatches(docs.length, isFLE, false); + + const dataService = getDataService({ isFLE, throwErrors: false }); + + const bulkWriteSpy = sinon.spy(dataService, 'bulkWrite'); + const insertOneSpy = sinon.spy(dataService, 'insertOne'); + + const writer = new ImportWriter(dataService as any, 'db.col', false); + + for (const doc of docs) { + await writer.write(doc); + } + + await writer.finish(); + + expect(bulkWriteSpy.callCount).to.equal(numBatches); + for (const [index, args] of bulkWriteSpy.args.entries()) { + const [, _docs] = args; + const expected = getExpectedDocsInBatch(index + 1, docs.length, isFLE); + expect(_docs.length).to.equal(expected); + } + if (isFLE) { + expect(insertOneSpy.callCount).to.equal(BATCH_SIZE); + } else { + expect(insertOneSpy.callCount).to.equal(0); + } + }); + + for (const stopOnErrors of [true, false]) { + it(`${stopOnErrors ? 'stops' : 'does not stop'} on the first error for ${ + isFLE ? 'FLE2' : 'regular' + } collection if stopOnErrors is ${stopOnErrors}`, async function () { + const dataService = getDataService({ isFLE, throwErrors: true }); + + const bulkWriteSpy = sinon.spy(dataService, 'bulkWrite'); + const insertOneSpy = sinon.spy(dataService, 'insertOne'); + + const writer = new ImportWriter( + dataService as any, + 'db.col', + stopOnErrors + ); + + // It always throws, it just depends if it finished the batch or not and + // whether it threw the first database error itself or a wrapped error + // that wraps all the errors in the batch + try { + for (const doc of docs) { + await writer.write(doc); + } + + await writer.finish(); + } catch (err: any) { + if (stopOnErrors) { + if (isFLE) { + expect(err.message).to.equal('fake insertOne error'); + expect(bulkWriteSpy.callCount).to.equal(1); + expect(insertOneSpy.callCount).to.equal(1); + expect(writer.docsWritten).to.equal(0); + expect(writer.docsProcessed).to.equal(1000); + // stop after the first insertOne call + expect(writer.docsErrored).to.equal(1); + } else { + expect(err.message).to.equal('fake bulkWrite error'); + expect(bulkWriteSpy.callCount).to.equal(1); + expect(insertOneSpy.callCount).to.equal(0); + expect(writer.docsWritten).to.equal(0); + expect(writer.docsProcessed).to.equal(1000); + // stop after the first bulkWrite call. in this case the whole + // first batch failed which is why there are so many docsErrored + // (see our mocks above) + expect(writer.docsErrored).to.equal(1000); + } + } else { + if (isFLE) { + expect(err.message).to.equal( + 'Something went wrong while writing data to a collection' + ); + expect(err.writeErrors).to.have.length(1000); + expect(bulkWriteSpy.callCount).to.equal(1); + expect(insertOneSpy.callCount).to.equal(1000); + + expect(writer.docsWritten).to.equal(0); + expect(writer.docsProcessed).to.equal(1000); + expect(writer.docsErrored).to.equal(1000); + } else { + expect(err.message).to.equal( + 'Something went wrong while writing data to a collection' + ); + expect(err.writeErrors).to.have.length(1000); + expect(bulkWriteSpy.callCount).to.equal(1); + expect(insertOneSpy.callCount).to.equal(0); + + expect(writer.docsWritten).to.equal(0); + expect(writer.docsProcessed).to.equal(1000); + expect(writer.docsErrored).to.equal(1000); + } + } + + return; + } + + expect.fail('expected to throw regardless'); + }); + } + } +}); diff --git a/packages/compass-import-export/src/import/import-writer.ts b/packages/compass-import-export/src/import/import-writer.ts new file mode 100644 index 00000000000..0ebea11be2b --- /dev/null +++ b/packages/compass-import-export/src/import/import-writer.ts @@ -0,0 +1,215 @@ +/* eslint-disable no-console */ +import type { + Document, + MongoBulkWriteError, + AnyBulkWriteOperation, + WriteError, + BulkWriteResult, + MongoServerError, +} from 'mongodb'; +import type { DataService } from 'mongodb-data-service'; +import type { ErrorJSON } from '../import/import-types'; + +import { createDebug } from '../utils/logger'; + +const debug = createDebug('import-writer'); + +type PartialBulkWriteResult = Partial< + Pick +>; + +type BulkOpResult = { + insertedCount: number; + numWriteErrors: number; +}; + +class ImportWriterError extends Error { + writeErrors: any[]; + name = 'ImportWriterError'; + + constructor(writeErrors: any[]) { + super('Something went wrong while writing data to a collection'); + this.writeErrors = writeErrors; + } +} + +type ImportWriterProgressError = Error & { + index: number; + code: MongoServerError['code']; + op: MongoServerError['op']; + errInfo: MongoServerError['errInfo']; +}; + +function writeErrorToJSError({ + errInfo, + errmsg, + err, + code, + index, +}: WriteError): ImportWriterProgressError { + const op = err?.op; + + const e: ImportWriterProgressError = new Error(errmsg) as any; + e.index = index; + e.code = code; + e.op = op; + e.errInfo = errInfo; + + // https://www.mongodb.com/docs/manual/reference/method/BulkWriteResult/#mongodb-data-BulkWriteResult.writeErrors + e.name = index !== undefined && op ? 'WriteError' : 'WriteConcernError'; + + return e; +} + +export class ImportWriter { + dataService: Pick; + ns: string; + BATCH_SIZE: number; + docsWritten: number; + docsProcessed: number; + docsErrored: number; + stopOnErrors?: boolean; + batch: Document[]; + _batchCounter: number; + errorCallback?: (error: ErrorJSON) => void; + + constructor( + dataService: Pick, + ns: string, + stopOnErrors?: boolean + ) { + this.dataService = dataService; + this.ns = ns; + this.BATCH_SIZE = 1000; + this.docsWritten = 0; + this.docsProcessed = 0; + this.docsErrored = 0; + this.stopOnErrors = stopOnErrors; + + this.batch = []; + this._batchCounter = 0; + } + + async write(document: Document) { + this.batch.push(document); + + if (this.batch.length >= this.BATCH_SIZE) { + await this._executeBatch(); + } + } + + async finish() { + if (this.batch.length === 0) { + debug('%d docs written', this.docsWritten); + return; + } + + debug('draining buffered docs', this.batch.length); + + await this._executeBatch(); + } + + async _executeBatch() { + const documents = this.batch; + + this.docsProcessed += documents.length; + + this.batch = []; + + let bulkWriteResult: PartialBulkWriteResult; + try { + bulkWriteResult = await this.dataService.bulkWrite( + this.ns, + documents.map( + (document: any): AnyBulkWriteOperation => ({ + insertOne: { document }, + }) + ), + { + ordered: this.stopOnErrors, + retryWrites: false, + checkKeys: false, + } + ); + } catch (bulkWriteError: any) { + // Currently, the server does not support batched inserts for FLE2: + // https://jira.mongodb.org/browse/SERVER-66315 + // We check for this specific error and re-try inserting documents one by one. + if (bulkWriteError.code === 6371202) { + this.BATCH_SIZE = 1; + + bulkWriteResult = await this._insertOneByOne(documents); + } else { + // If we are writing with `ordered: false`, bulkWrite will throw and + // will not return any result, but server might write some docs and bulk + // result can still be accessed on the error instance + + // Driver seems to return null instead of undefined in some rare cases + // when the operation ends in error, instead of relying on + // `_mergeBulkOpResult` default argument substitution, we need to keep + // this OR expression here + bulkWriteResult = ((bulkWriteError as MongoBulkWriteError).result || + {}) as PartialBulkWriteResult; + + if (this.stopOnErrors) { + this.docsWritten += bulkWriteResult.insertedCount || 0; + this.docsErrored += + (bulkWriteResult.getWriteErrors?.() || []).length || 0; + throw bulkWriteError; + } + } + } + + const bulkOpResult = this._getBulkOpResult(bulkWriteResult); + + const writeErrors = (bulkWriteResult?.getWriteErrors?.() || []).map( + writeErrorToJSError + ); + + this.docsWritten += bulkOpResult.insertedCount; + this.docsErrored += bulkOpResult.numWriteErrors; + this._batchCounter++; + + if (writeErrors.length) { + throw new ImportWriterError(writeErrors); + } + } + + async _insertOneByOne( + documents: Document[] + ): Promise { + let insertedCount = 0; + const errors: WriteError[] = []; + + for (const doc of documents) { + try { + await this.dataService.insertOne(this.ns, doc); + insertedCount += 1; + } catch (insertOneByOneError: any) { + if (this.stopOnErrors) { + this.docsWritten += insertedCount; + this.docsErrored += 1; + throw insertOneByOneError; + } + + errors.push(insertOneByOneError as WriteError); + } + } + + return { + insertedCount, + getWriteErrors: () => { + return errors; + }, + }; + } + + _getBulkOpResult(result: PartialBulkWriteResult): BulkOpResult { + const writeErrors = result.getWriteErrors?.() || []; + + return { + insertedCount: result.insertedCount || 0, + numWriteErrors: writeErrors.length, + }; + } +} diff --git a/packages/compass-import-export/src/modules/export.spec.ts b/packages/compass-import-export/src/modules/export.spec.ts index 7a863021e10..2d5450bf082 100644 --- a/packages/compass-import-export/src/modules/export.spec.ts +++ b/packages/compass-import-export/src/modules/export.spec.ts @@ -5,8 +5,6 @@ import path from 'path'; import Sinon from 'sinon'; import type { DataService } from 'mongodb-data-service'; import { connect } from 'mongodb-data-service'; -import AppRegistry from 'hadron-app-registry'; - import { openExport, addFieldToExport, @@ -20,294 +18,284 @@ import { } from './export'; import { mochaTestServer } from '@mongodb-js/compass-test-server'; import { - type ExportPluginServices, - configureStore, -} from '../stores/export-store'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; -import { - ConnectionsManager, - type ConnectionRepository, -} from '@mongodb-js/compass-connections/provider'; -import { type PreferencesAccess } from 'compass-preferences-model/provider'; - -const logger = createNoopLogger(); -const track = createNoopTrack(); -const dataService = { - findCursor() {}, - aggregateCursor() {}, -} as unknown as DataService; -const connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, -}); -const connectionRepository = { - getConnectionInfoById: () => ({ id: 'TEST' }), -} as unknown as ConnectionRepository; -const mockServices: ExportPluginServices = { - connectionsManager, - globalAppRegistry: new AppRegistry(), - logger: createNoopLogger(), - track: createNoopTrack(), - connectionRepository, - preferences: {} as PreferencesAccess, -}; + activatePluginWithConnections, + cleanup, +} from '@mongodb-js/compass-connections/test'; +import { ExportPlugin } from '../index'; +import type { ExportStore } from '../stores/export-store'; +import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; + +function activatePlugin( + dataService = { + findCursor() {}, + aggregateCursor() {}, + } as any +) { + return activatePluginWithConnections( + ExportPlugin, + {}, + { + connectFn() { + return dataService; + }, + } + ); +} describe('export [module]', function () { - // This is re-created in the `beforeEach`, it's useful for typing to have it here as well. - let testStore = configureStore(mockServices); - - beforeEach(function () { - Sinon.stub(connectionsManager, 'getDataServiceForConnection').returns( - dataService - ); - testStore = configureStore(mockServices); - }); + let testStore: ExportStore; afterEach(function () { Sinon.restore(); + cleanup(); }); - describe('#openExport', function () { - it('sets isInProgressMessageOpen to true when export is in progress and does not open', function () { - // TODO(COMPASS-6580) + describe('with mock dataService', function () { + beforeEach(function () { + testStore = activatePlugin().plugin.store; }); - it('opens and sets the namespace and connectionId', function () { - const testNS = 'test.123'; - expect(testStore.getState().export.status).to.equal(undefined); - expect(testStore.getState().export.namespace).to.not.equal(testNS); - expect(testStore.getState().export.isInProgressMessageOpen).to.equal( - false - ); - expect(testStore.getState().export.isOpen).to.equal(false); - expect(testStore.getState().export.exportFullCollection).to.equal( - undefined - ); - - testStore.dispatch( - openExport({ - connectionId: 'TEST', - namespace: 'test.123', - query: { - filter: {}, - }, - origin: 'crud-toolbar', - exportFullCollection: true, - }) - ); + describe('#openExport', function () { + it('sets isInProgressMessageOpen to true when export is in progress and does not open', function () { + // TODO(COMPASS-6580) + }); - expect(testStore.getState().export.namespace).to.equal(testNS); - expect(testStore.getState().export.connectionId).to.equal('TEST'); - expect(testStore.getState().export.isInProgressMessageOpen).to.equal( - false - ); - expect(testStore.getState().export.isOpen).to.equal(true); - expect(testStore.getState().export.exportFullCollection).to.equal(true); + it('opens and sets the namespace and connectionId', function () { + const testNS = 'test.123'; + expect(testStore.getState().export.status).to.equal(undefined); + expect(testStore.getState().export.namespace).to.not.equal(testNS); + expect(testStore.getState().export.isInProgressMessageOpen).to.equal( + false + ); + expect(testStore.getState().export.isOpen).to.equal(false); + expect(testStore.getState().export.exportFullCollection).to.equal( + undefined + ); + + testStore.dispatch( + openExport({ + connectionId: 'TEST', + namespace: 'test.123', + query: { + filter: {}, + }, + origin: 'crud-toolbar', + exportFullCollection: true, + }) + ); + + expect(testStore.getState().export.namespace).to.equal(testNS); + expect(testStore.getState().export.connectionId).to.equal('TEST'); + expect(testStore.getState().export.isInProgressMessageOpen).to.equal( + false + ); + expect(testStore.getState().export.isOpen).to.equal(true); + expect(testStore.getState().export.exportFullCollection).to.equal(true); + }); }); - }); - describe('#addFieldToExport', function () { - it('adds the field to the fields to export', function () { - expect(testStore.getState().export.fieldsToExport).to.deep.equal({}); + describe('#addFieldToExport', function () { + it('adds the field to the fields to export', function () { + expect(testStore.getState().export.fieldsToExport).to.deep.equal({}); - testStore.dispatch(addFieldToExport(['one', 'two'])); + testStore.dispatch(addFieldToExport(['one', 'two'])); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["one","two"]': { - path: ['one', 'two'], - selected: true, - }, + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["one","two"]': { + path: ['one', 'two'], + selected: true, + }, + }); }); }); - }); - describe('#toggleFieldToExport', function () { - it('toggles the field to export', function () { - testStore.dispatch(addFieldToExport(['five'])); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["five"]': { - path: ['five'], - selected: true, - }, - }); + describe('#toggleFieldToExport', function () { + it('toggles the field to export', function () { + testStore.dispatch(addFieldToExport(['five'])); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["five"]': { + path: ['five'], + selected: true, + }, + }); - testStore.dispatch(toggleFieldToExport('["five"]')); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["five"]': { - path: ['five'], - selected: false, - }, - }); - testStore.dispatch(toggleFieldToExport('["five"]')); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["five"]': { - path: ['five'], - selected: true, - }, + testStore.dispatch(toggleFieldToExport('["five"]')); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["five"]': { + path: ['five'], + selected: false, + }, + }); + testStore.dispatch(toggleFieldToExport('["five"]')); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["five"]': { + path: ['five'], + selected: true, + }, + }); }); }); - }); - describe('#toggleExportAllSelectedFields', function () { - it('toggles all of the fields', function () { - testStore.dispatch(addFieldToExport(['one'])); - testStore.dispatch(toggleFieldToExport('["one"]')); - testStore.dispatch(addFieldToExport(['one', 'two'])); - testStore.dispatch(addFieldToExport(['five'])); - - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["one"]': { - path: ['one'], - selected: false, - }, - '["one","two"]': { - path: ['one', 'two'], - selected: true, - }, - '["five"]': { - path: ['five'], - selected: true, - }, - }); + describe('#toggleExportAllSelectedFields', function () { + it('toggles all of the fields', function () { + testStore.dispatch(addFieldToExport(['one'])); + testStore.dispatch(toggleFieldToExport('["one"]')); + testStore.dispatch(addFieldToExport(['one', 'two'])); + testStore.dispatch(addFieldToExport(['five'])); + + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["one"]': { + path: ['one'], + selected: false, + }, + '["one","two"]': { + path: ['one', 'two'], + selected: true, + }, + '["five"]': { + path: ['five'], + selected: true, + }, + }); - testStore.dispatch(toggleExportAllSelectedFields()); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["one"]': { - path: ['one'], - selected: true, - }, - '["one","two"]': { - path: ['one', 'two'], - selected: true, - }, - '["five"]': { - path: ['five'], - selected: true, - }, - }); + testStore.dispatch(toggleExportAllSelectedFields()); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["one"]': { + path: ['one'], + selected: true, + }, + '["one","two"]': { + path: ['one', 'two'], + selected: true, + }, + '["five"]': { + path: ['five'], + selected: true, + }, + }); - testStore.dispatch(toggleExportAllSelectedFields()); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["one"]': { - path: ['one'], - selected: false, - }, - '["one","two"]': { - path: ['one', 'two'], - selected: false, - }, - '["five"]': { - path: ['five'], - selected: false, - }, - }); + testStore.dispatch(toggleExportAllSelectedFields()); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["one"]': { + path: ['one'], + selected: false, + }, + '["one","two"]': { + path: ['one', 'two'], + selected: false, + }, + '["five"]': { + path: ['five'], + selected: false, + }, + }); - testStore.dispatch(toggleExportAllSelectedFields()); - expect(testStore.getState().export.fieldsToExport).to.deep.equal({ - '["one"]': { - path: ['one'], - selected: true, - }, - '["one","two"]': { - path: ['one', 'two'], - selected: true, - }, - '["five"]': { - path: ['five'], - selected: true, - }, + testStore.dispatch(toggleExportAllSelectedFields()); + expect(testStore.getState().export.fieldsToExport).to.deep.equal({ + '["one"]': { + path: ['one'], + selected: true, + }, + '["one","two"]': { + path: ['one', 'two'], + selected: true, + }, + '["five"]': { + path: ['five'], + selected: true, + }, + }); }); }); - }); - describe('#selectFieldsToExport', function () { - it('sets errors on the store', async function () { - expect(testStore.getState().export.errorLoadingFieldsToExport).to.equal( - undefined - ); + describe('#selectFieldsToExport', function () { + it('sets errors on the store', async function () { + expect(testStore.getState().export.errorLoadingFieldsToExport).to.equal( + undefined + ); - await testStore.dispatch(selectFieldsToExport() as any); - - expect( - testStore.getState().export.errorLoadingFieldsToExport - ).to.not.equal(undefined); - }); - }); + await testStore.dispatch(selectFieldsToExport() as any); - describe('#closeExport', function () { - it('signals an abort on the export abort controller', function () { - const testAbortController = new AbortController(); - testStore.dispatch({ - type: ExportActionTypes.RunExport, - exportAbortController: testAbortController, + expect( + testStore.getState().export.errorLoadingFieldsToExport + ).to.not.equal(undefined); }); - - expect(testStore.getState().export.exportAbortController).to.equal( - testAbortController - ); - expect( - testStore.getState().export.exportAbortController?.signal.aborted - ).to.equal(false); - expect(testAbortController.signal.aborted).to.equal(false); - - testStore.dispatch(closeExport()); - - expect(testAbortController.signal.aborted).to.equal(true); }); - it('signals an abort on the fetch schema fields abort controller', function () { - const testAbortController = new AbortController(); - testStore.dispatch({ - type: ExportActionTypes.FetchFieldsToExport, - fieldsToExportAbortController: testAbortController, + describe('#closeExport', function () { + it('signals an abort on the export abort controller', function () { + const testAbortController = new AbortController(); + testStore.dispatch({ + type: ExportActionTypes.RunExport, + exportAbortController: testAbortController, + }); + + expect(testStore.getState().export.exportAbortController).to.equal( + testAbortController + ); + expect( + testStore.getState().export.exportAbortController?.signal.aborted + ).to.equal(false); + expect(testAbortController.signal.aborted).to.equal(false); + + testStore.dispatch(closeExport()); + + expect(testAbortController.signal.aborted).to.equal(true); }); - expect( - testStore.getState().export.fieldsToExportAbortController - ).to.equal(testAbortController); - expect( - testStore.getState().export.fieldsToExportAbortController?.signal - .aborted - ).to.equal(false); - expect(testAbortController.signal.aborted).to.equal(false); - - testStore.dispatch(closeExport()); - - expect(testAbortController.signal.aborted).to.equal(true); + it('signals an abort on the fetch schema fields abort controller', function () { + const testAbortController = new AbortController(); + testStore.dispatch({ + type: ExportActionTypes.FetchFieldsToExport, + fieldsToExportAbortController: testAbortController, + }); + + expect( + testStore.getState().export.fieldsToExportAbortController + ).to.equal(testAbortController); + expect( + testStore.getState().export.fieldsToExportAbortController?.signal + .aborted + ).to.equal(false); + expect(testAbortController.signal.aborted).to.equal(false); + + testStore.dispatch(closeExport()); + + expect(testAbortController.signal.aborted).to.equal(true); + }); }); - }); - describe('#cancelExport', function () { - it('aborts the export', function () { - const testAbortController = new AbortController(); - testStore.dispatch({ - type: ExportActionTypes.RunExport, - exportAbortController: testAbortController, + describe('#cancelExport', function () { + it('aborts the export', function () { + const testAbortController = new AbortController(); + testStore.dispatch({ + type: ExportActionTypes.RunExport, + exportAbortController: testAbortController, + }); + + expect(testStore.getState().export.exportAbortController).to.equal( + testAbortController + ); + expect( + testStore.getState().export.exportAbortController?.signal.aborted + ).to.equal(false); + expect(testAbortController.signal.aborted).to.equal(false); + + testStore.dispatch(cancelExport()); + + expect(testAbortController.signal.aborted).to.equal(true); + expect(testStore.getState().export.exportAbortController).to.equal( + undefined + ); }); - - expect(testStore.getState().export.exportAbortController).to.equal( - testAbortController - ); - expect( - testStore.getState().export.exportAbortController?.signal.aborted - ).to.equal(false); - expect(testAbortController.signal.aborted).to.equal(false); - - testStore.dispatch(cancelExport()); - - expect(testAbortController.signal.aborted).to.equal(true); - expect(testStore.getState().export.exportAbortController).to.equal( - undefined - ); }); }); - describe('#runExport', function () { + describe('with real dataService', function () { const cluster = mochaTestServer(); + let connectionInfo: ConnectionInfo; let dataService: DataService; let tmpdir: string; - let appRegistry: AppRegistry; const testDB = 'export-runExport-test'; const testNS = `${testDB}.test-col`; @@ -320,11 +308,14 @@ describe('export [module]', function () { ); await fs.promises.mkdir(tmpdir, { recursive: true }); - dataService = await connect({ + connectionInfo = { + id: 'TEST', connectionOptions: { connectionString: cluster().connectionString, }, - }); + }; + + dataService = await connect(connectionInfo); try { await dataService.dropCollection(testNS); @@ -337,116 +328,102 @@ describe('export [module]', function () { testDoc: true, }); - appRegistry = new AppRegistry(); - const logger = createNoopLogger(); - const connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, - }); - - Sinon.stub(connectionsManager, 'getDataServiceForConnection').returns( - dataService - ); - - testStore = configureStore({ - globalAppRegistry: appRegistry, - preferences: await createSandboxFromDefaultPreferences(), - logger, - track, - connectionsManager, - connectionRepository, - }); + const result = activatePlugin(dataService); + await result.connectionsStore.actions.connect(connectionInfo); + testStore = result.plugin.store; }); afterEach(async function () { await fs.promises.rm(tmpdir, { recursive: true }); - await dataService.disconnect(); }); - it('runs an export', async function () { - testStore.dispatch( - openExport({ - connectionId: 'TEST', - namespace: testNS, - query: { - filter: {}, - }, - origin: 'menu', - exportFullCollection: true, - }) - ); - - const textExportFilePath = path.join(tmpdir, 'run-export-test.json'); - await testStore.dispatch( - runExport({ - filePath: textExportFilePath, - jsonFormatVariant: 'default', - fileType: 'json', - }) as any - ); - - let resultText; - try { - resultText = await fs.promises.readFile(textExportFilePath, 'utf8'); - } catch (err) { - // eslint-disable-next-line no-console - console.log(textExportFilePath); - throw err; - } - - const expectedText = `[{ + describe('#runExport', function () { + it('runs an export', async function () { + testStore.dispatch( + openExport({ + connectionId: connectionInfo.id, + namespace: testNS, + query: { + filter: {}, + }, + origin: 'menu', + exportFullCollection: true, + }) + ); + + const textExportFilePath = path.join(tmpdir, 'run-export-test.json'); + await testStore.dispatch( + runExport({ + filePath: textExportFilePath, + jsonFormatVariant: 'default', + fileType: 'json', + }) as any + ); + + let resultText; + try { + resultText = await fs.promises.readFile(textExportFilePath, 'utf8'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(textExportFilePath); + throw err; + } + + const expectedText = `[{ "_id": 2, "testDoc": true }]`; - expect(resultText).to.equal(expectedText); - }); - - it('runs an aggregation export', async function () { - testStore.dispatch( - openExport({ - connectionId: 'TEST', - namespace: testNS, - origin: 'aggregations-toolbar', - query: { - filter: {}, - }, - aggregation: { - stages: [ - { - $match: {}, - }, - { - $project: { _id: 0 }, - }, - ], - }, - }) - ); - - const textExportFilePath = path.join(tmpdir, 'run-export-test-2.json'); - await testStore.dispatch( - runExport({ - filePath: textExportFilePath, - jsonFormatVariant: 'default', - fileType: 'json', - }) as any - ); - - let resultText; - try { - resultText = await fs.promises.readFile(textExportFilePath, 'utf8'); - } catch (err) { - // eslint-disable-next-line no-console - console.log(textExportFilePath); - throw err; - } + expect(resultText).to.equal(expectedText); + }); - const expectedText = `[{ + it('runs an aggregation export', async function () { + testStore.dispatch( + openExport({ + connectionId: connectionInfo.id, + namespace: testNS, + origin: 'aggregations-toolbar', + query: { + filter: {}, + }, + aggregation: { + stages: [ + { + $match: {}, + }, + { + $project: { _id: 0 }, + }, + ], + }, + }) + ); + + const textExportFilePath = path.join(tmpdir, 'run-export-test-2.json'); + await testStore.dispatch( + runExport({ + filePath: textExportFilePath, + jsonFormatVariant: 'default', + fileType: 'json', + }) as any + ); + + let resultText; + try { + resultText = await fs.promises.readFile(textExportFilePath, 'utf8'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(textExportFilePath); + throw err; + } + + const expectedText = `[{ "testDoc": true }]`; - expect(resultText).to.equal(expectedText); + expect(resultText).to.equal(expectedText); + }); }); }); }); diff --git a/packages/compass-import-export/src/modules/import.spec.ts b/packages/compass-import-export/src/modules/import.spec.ts index 9b673f833f6..bd40d7abebd 100644 --- a/packages/compass-import-export/src/modules/import.spec.ts +++ b/packages/compass-import-export/src/modules/import.spec.ts @@ -1,38 +1,32 @@ import { expect } from 'chai'; import path from 'path'; import { onStarted, openImport, selectImportFileName } from './import'; -import { - type ImportPluginServices, - configureStore, -} from '../stores/import-store'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { - type ConnectionRepository, - ConnectionsManager, -} from '@mongodb-js/compass-connections/provider'; -import { AppRegistry } from 'hadron-app-registry'; -import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; - -const logger = createNoopLogger(); -const track = createNoopTrack(); - -const mockServices = { - globalAppRegistry: new AppRegistry(), - logger, - track, - connectionsManager: new ConnectionsManager({ logger: logger.log.unbound }), - workspaces: {} as WorkspacesService, - connectionRepository: { - getConnectionInfoById: () => ({ id: 'TEST' }), - } as unknown as ConnectionRepository, -} as ImportPluginServices; +import type { ImportStore } from '../stores/import-store'; +import { ImportPlugin } from '../index'; +import { activatePluginWithConnections } from '@mongodb-js/compass-connections/test'; + +function activatePlugin( + dataService = { + findCursor() {}, + aggregateCursor() {}, + } as any +) { + return activatePluginWithConnections( + ImportPlugin, + {}, + { + connectFn() { + return dataService; + }, + } + ); +} describe('import [module]', function () { - // This is re-created in the `beforeEach`, it's useful for typing to have it here as well. - let mockStore = configureStore(mockServices); + let mockStore: ImportStore; + beforeEach(function () { - mockStore = configureStore(mockServices); + mockStore = activatePlugin().plugin.store; }); describe('#openImport', function () { @@ -112,13 +106,13 @@ describe('import [module]', function () { const noExistFile = path.join(__dirname, 'no-exist.json'); expect(mockStore.getState().import.fileName).to.equal(''); - expect(mockStore.getState().import.errors.length).to.equal(0); + expect(mockStore.getState().import.firstErrors.length).to.equal(0); await mockStore.dispatch(selectImportFileName(noExistFile) as any); expect(mockStore.getState().import.fileName).to.equal(''); - expect(mockStore.getState().import.errors.length).to.equal(1); + expect(mockStore.getState().import.firstErrors.length).to.equal(1); }); }); }); diff --git a/packages/compass-import-export/src/modules/import.ts b/packages/compass-import-export/src/modules/import.ts index 5ac4f3d9498..e5b3983e804 100644 --- a/packages/compass-import-export/src/modules/import.ts +++ b/packages/compass-import-export/src/modules/import.ts @@ -83,7 +83,7 @@ type FieldType = FieldFromJSON | FieldFromCSV; type ImportState = { isOpen: boolean; isInProgressMessageOpen: boolean; - errors: Error[]; + firstErrors: Error[]; fileType: AcceptedFileType | ''; fileName: string; errorLogFilePath: string; @@ -119,7 +119,7 @@ type ImportState = { export const INITIAL_STATE: ImportState = { isOpen: false, isInProgressMessageOpen: false, - errors: [], + firstErrors: [], fileName: '', errorLogFilePath: '', fileIsMultilineJSON: false, @@ -157,14 +157,14 @@ export const onStarted = ({ const onFinished = ({ aborted, - errors, + firstErrors, }: { aborted: boolean; - errors: Error[]; + firstErrors: Error[]; }) => ({ type: FINISHED, aborted, - errors, + firstErrors, }); const onFailed = (error: Error) => ({ type: FAILED, error }); @@ -228,7 +228,7 @@ export const startImport = (): ImportThunkAction> => { } const input = fs.createReadStream(fileName, 'utf8'); - const errors: ErrorJSON[] = []; + const firstErrors: ErrorJSON[] = []; let errorLogFilePath: string | undefined; let errorLogWriteStream: fs.WriteStream | undefined; @@ -242,7 +242,7 @@ export const startImport = (): ImportThunkAction> => { (err as Error).message = `unable to create import error log file: ${ (err as Error).message }`; - errors.push(err as Error); + firstErrors.push(err as Error); } log.info( @@ -280,16 +280,13 @@ export const startImport = (): ImportThunkAction> => { let numErrors = 0; const errorCallback = (err: ErrorJSON) => { - // For bulk write errors we'll get one callback for the whole batch and - // then numErrors is the number of documents that failed for that batch. - // Usually but not necessarily the entire batch. - numErrors += err.numErrors ?? 1; - if (errors.length < 5) { + numErrors += 1; + if (firstErrors.length < 5) { // Only store the first few errors in memory. // The log file tracks all of them. // If we are importing a massive file with many errors we don't // want to run out of memory. We show the first few errors in the UI. - errors.push(err); + firstErrors.push(err); } }; @@ -415,7 +412,7 @@ export const startImport = (): ImportThunkAction> => { track( 'Import Error Log Opened', { - errorCount: errors.length, + errorCount: numErrors, }, connectionRepository.getConnectionInfoById(connectionId) ); @@ -426,7 +423,7 @@ export const startImport = (): ImportThunkAction> => { if (result.aborted) { showCancelledToast({ - errors, + errors: firstErrors, actionHandler: openErrorLogFilePathActionHandler, }); } else { @@ -446,10 +443,10 @@ export const startImport = (): ImportThunkAction> => { showUnboundArraySignalToast({ onReviewDocumentsClick }); } - if (errors.length > 0) { + if (firstErrors.length > 0) { showCompletedWithErrorsToast({ docsWritten: result.docsWritten, - errors, + errors: firstErrors, docsProcessed: result.docsProcessed, actionHandler: openErrorLogFilePathActionHandler, }); @@ -463,7 +460,7 @@ export const startImport = (): ImportThunkAction> => { dispatch( onFinished({ aborted: !!result.aborted, - errors, + firstErrors, }) ); @@ -845,7 +842,7 @@ export const setDelimiter = ( * by the user attempting to resume from a previous import without * removing all documents sucessfully imported. * - * @see utils/collection-stream.js + * @see import/import-writer.ts, import-utils.ts * @see https://www.mongodb.com/docs/database-tools/mongoimport/#std-option-mongoimport.--stopOnError */ export const setStopOnErrors = (stopOnErrors: boolean) => ({ @@ -935,7 +932,7 @@ export const importReducer: Reducer = ( fileStats: action.fileStats, fileIsMultilineJSON: action.fileIsMultilineJSON, status: PROCESS_STATUS.UNSPECIFIED, - errors: [], + firstErrors: [], abortController: undefined, analyzeAbortController: undefined, fields: [], @@ -1060,7 +1057,7 @@ export const importReducer: Reducer = ( if (action.type === FILE_SELECT_ERROR) { return { ...state, - errors: [action.error], + firstErrors: [action.error], }; } @@ -1070,7 +1067,7 @@ export const importReducer: Reducer = ( if (action.type === FAILED) { return { ...state, - errors: [action.error], + firstErrors: [action.error], status: PROCESS_STATUS.FAILED, abortController: undefined, }; @@ -1080,7 +1077,7 @@ export const importReducer: Reducer = ( return { ...state, isOpen: false, - errors: [], + firstErrors: [], status: PROCESS_STATUS.STARTED, abortController: action.abortController, errorLogFilePath: action.errorLogFilePath, @@ -1095,7 +1092,7 @@ export const importReducer: Reducer = ( return { ...state, status, - errors: action.errors, + firstErrors: action.firstErrors, abortController: undefined, }; } diff --git a/packages/compass-import-export/src/stores/export-store.spec.tsx b/packages/compass-import-export/src/stores/export-store.spec.tsx index 54d74d85cd8..ced0050f48c 100644 --- a/packages/compass-import-export/src/stores/export-store.spec.tsx +++ b/packages/compass-import-export/src/stores/export-store.spec.tsx @@ -1,50 +1,24 @@ -import { createActivateHelpers } from 'hadron-app-registry'; -import AppRegistry from 'hadron-app-registry'; -import { activatePlugin } from './export-store'; -import { - type ConnectionRepository, - ConnectionsManager, -} from '@mongodb-js/compass-connections/provider'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; +import type AppRegistry from 'hadron-app-registry'; import { expect } from 'chai'; -import { type PreferencesAccess } from 'compass-preferences-model/provider'; +import { + activatePluginWithConnections, + cleanup, +} from '@mongodb-js/compass-connections/test'; +import { ExportPlugin } from '..'; +import type { ExportStore } from './export-store'; describe('ExportStore [Store]', function () { - let store: any; - let deactivate: any; + let store: ExportStore; let globalAppRegistry: AppRegistry; - let connectionsManager: ConnectionsManager; - const preferences = {} as PreferencesAccess; - const connectionId = 'TEST'; beforeEach(function () { - const logger = createNoopLogger(); - const track = createNoopTrack(); - globalAppRegistry = new AppRegistry(); - connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, - }); - const connectionRepository = { - getConnectionInfoById: () => ({ id: connectionId }), - } as unknown as ConnectionRepository; - - ({ store, deactivate } = activatePlugin( - {}, - { - globalAppRegistry, - connectionsManager, - logger, - track, - preferences, - connectionRepository, - }, - createActivateHelpers() - )); + const result = activatePluginWithConnections(ExportPlugin, {}); + store = result.plugin.store; + globalAppRegistry = result.globalAppRegistry; }); afterEach(function () { - deactivate(); + cleanup(); }); it(`throws when 'open-export' is emitted without connection metadata`, function () { @@ -53,16 +27,16 @@ describe('ExportStore [Store]', function () { namespace: 'test.coll', origin: 'menu', }); - }).to.throw; + }).to.throw(); }); it('opens the import modal with properly set state', function () { globalAppRegistry.emit( 'open-export', { namespace: 'test.coll', origin: 'menu' }, - { connectionId } + { connectionId: 'TEST' } ); - expect(store.getState().export.connectionId).to.equal(connectionId); + expect(store.getState().export.connectionId).to.equal('TEST'); expect(store.getState().export.namespace).to.equal('test.coll'); }); }); diff --git a/packages/compass-import-export/src/stores/export-store.ts b/packages/compass-import-export/src/stores/export-store.ts index 015de3486d2..b6aecc2149c 100644 --- a/packages/compass-import-export/src/stores/export-store.ts +++ b/packages/compass-import-export/src/stores/export-store.ts @@ -11,7 +11,6 @@ import { } from '../modules/export'; import type { PreferencesAccess } from 'compass-preferences-model'; import type { Logger } from '@mongodb-js/compass-logging/provider'; -import { ConnectionsManagerEvents } from '@mongodb-js/compass-connections/provider'; import type { ActivateHelpers } from 'hadron-app-registry'; import type { ConnectionRepositoryAccess, @@ -116,13 +115,9 @@ export function activatePlugin( ); } ); - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionDisconnected, - function (connectionId: string) { - store.dispatch(connectionDisconnected(connectionId)); - } - ); + on(connectionsManager, 'disconnected', function (connectionId: string) { + store.dispatch(connectionDisconnected(connectionId)); + }); addCleanup(() => { // We use close and not cancel because cancel doesn't actually cancel @@ -135,3 +130,5 @@ export function activatePlugin( deactivate: cleanup, }; } + +export type ExportStore = ReturnType; diff --git a/packages/compass-import-export/src/stores/import-store.spec.tsx b/packages/compass-import-export/src/stores/import-store.spec.tsx index 74e922b269d..3d05a4bc538 100644 --- a/packages/compass-import-export/src/stores/import-store.spec.tsx +++ b/packages/compass-import-export/src/stores/import-store.spec.tsx @@ -1,50 +1,24 @@ -import { createActivateHelpers } from 'hadron-app-registry'; -import AppRegistry from 'hadron-app-registry'; -import { activatePlugin } from './import-store'; -import { - type ConnectionRepository, - ConnectionsManager, -} from '@mongodb-js/compass-connections/provider'; -import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; +import type AppRegistry from 'hadron-app-registry'; import { expect } from 'chai'; +import { + activatePluginWithConnections, + cleanup, +} from '@mongodb-js/compass-connections/test'; +import type { ImportStore } from './import-store'; +import { ImportPlugin } from '..'; describe('ImportStore [Store]', function () { - let store: any; - let deactivate: any; + let store: ImportStore; let globalAppRegistry: AppRegistry; - let connectionsManager: ConnectionsManager; - let workspaces: WorkspacesService; - const connectionId = 'TEST'; beforeEach(function () { - const logger = createNoopLogger(); - const track = createNoopTrack(); - globalAppRegistry = new AppRegistry(); - connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, - }); - const connectionRepository = { - getConnectionInfoById: () => ({ id: connectionId }), - } as unknown as ConnectionRepository; - - ({ store, deactivate } = activatePlugin( - {}, - { - globalAppRegistry, - connectionsManager, - logger, - track, - workspaces, - connectionRepository, - }, - createActivateHelpers() - )); + const result = activatePluginWithConnections(ImportPlugin, {}); + globalAppRegistry = result.globalAppRegistry; + store = result.plugin.store; }); afterEach(function () { - deactivate(); + cleanup(); }); it(`throws when 'open-import' is emitted without connection metadata`, function () { @@ -53,16 +27,16 @@ describe('ImportStore [Store]', function () { namespace: 'test.coll', origin: 'menu', }); - }).to.throw; + }).to.throw(); }); it('opens the import modal with properly set state', function () { globalAppRegistry.emit( 'open-import', { namespace: 'test.coll', origin: 'menu' }, - { connectionId } + { connectionId: 'TEST' } ); - expect(store.getState().import.connectionId).to.equal(connectionId); + expect(store.getState().import.connectionId).to.equal('TEST'); expect(store.getState().import.namespace).to.equal('test.coll'); }); }); diff --git a/packages/compass-import-export/src/stores/import-store.ts b/packages/compass-import-export/src/stores/import-store.ts index da57bb92cd2..c516f69ee18 100644 --- a/packages/compass-import-export/src/stores/import-store.ts +++ b/packages/compass-import-export/src/stores/import-store.ts @@ -11,7 +11,6 @@ import { } from '../modules/import'; import type { WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; import type { Logger } from '@mongodb-js/compass-logging/provider'; -import { ConnectionsManagerEvents } from '@mongodb-js/compass-connections/provider'; import type { ConnectionRepositoryAccess, ConnectionsManager, @@ -97,16 +96,14 @@ export function activatePlugin( } ); - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionDisconnected, - function (connectionId: string) { - store.dispatch(connectionDisconnected(connectionId)); - } - ); + on(connectionsManager, 'disconnected', function (connectionId: string) { + store.dispatch(connectionDisconnected(connectionId)); + }); return { store, deactivate: cleanup, }; } + +export type ImportStore = ReturnType; diff --git a/packages/compass-import-export/src/utils/collection-stream.spec.ts b/packages/compass-import-export/src/utils/collection-stream.spec.ts deleted file mode 100644 index 48fe7b2f19f..00000000000 --- a/packages/compass-import-export/src/utils/collection-stream.spec.ts +++ /dev/null @@ -1,274 +0,0 @@ -import type { Writable } from 'stream'; -import { expect } from 'chai'; - -import { createCollectionWriteStream } from './collection-stream'; - -const BATCH_SIZE = 1000; - -function getDataService({ - isFLE, - throwErrors, -}: { - isFLE: boolean; - throwErrors: boolean; -}) { - return { - bulkWrite: (ns: string, docs: any[] /*, options: any*/) => { - return new Promise((resolve, reject) => { - if (isFLE && docs.length !== 1) { - const error: any = new Error( - 'Only single insert batches are supported in FLE2' - ); - error.code = 6371202; - return reject(error); - } - - if (throwErrors) { - const error = new Error('fake bulkWrite error'); - delete error.stack; // slows down tests due to excess output - return reject(error); - } - - resolve({ - insertedCount: docs.length, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - ok: 1, - }); - }); - }, - - insertOne: () => { - if (throwErrors) { - const error = new Error('fake insertOne error'); - delete error.stack; // slows down tests due to excess output - return Promise.reject(error); - } - - return Promise.resolve({ acknowledged: true }); - }, - }; -} - -async function insertDocs(dest: Writable, docs: any) { - try { - for (const doc of docs) { - await new Promise((resolve, reject) => { - dest.write(doc, (err) => (err ? reject(err) : resolve())); - }); - } - - return new Promise((resolve, reject) => { - dest.end((err?: Error) => (err ? reject(err) : resolve())); - }); - } catch (err) { - // we'll get here if stopOnErrors is true, because dest.write will throw - // ignore this for now (and stop writing more). we'll detect it via the - // stream's error event in the tests - } -} - -function getExpectedNumBatches( - numDocs: number, - isFLE: boolean, - stopOnErrors: boolean -) { - if (stopOnErrors) { - return 1; - } - - if (isFLE) { - // one attempted batch at the batch size (followed by insertOne() on retry), then subsequent batches are all size 1. - return numDocs > BATCH_SIZE ? 1 + numDocs - BATCH_SIZE : 1; - } - - return Math.ceil(numDocs / BATCH_SIZE); -} - -function getExpectedDocsInBatch( - batchNum: number, - numDocs: number, - isFLE: boolean -) { - if (batchNum === 1) { - return Math.min(numDocs, BATCH_SIZE); - } - - if (isFLE && batchNum > 1) { - return 1; - } - - const numBatches = getExpectedNumBatches(numDocs, isFLE, false); - - return batchNum < numBatches - ? BATCH_SIZE - : numDocs - (batchNum - 1) * BATCH_SIZE; -} - -describe('collection-stream', function () { - const docs: { i: number }[] = []; - for (let i = 0; i < BATCH_SIZE * 2 + 1; ++i) { - docs.push({ i }); - } - - for (const isFLE of [true, false]) { - it(`inserts documents ${isFLE ? 'one by one' : 'in batches'} to ${ - isFLE ? 'FLE2' : 'regular' - } collection`, async function () { - const numBatches = getExpectedNumBatches(docs.length, isFLE, false); - - const dataService = getDataService({ isFLE, throwErrors: false }); - - const dest = createCollectionWriteStream( - dataService as any, - 'db.col', - false - ); - - let resolveWrite: () => void; - const writePromise = new Promise((resolve) => { - resolveWrite = resolve; - }); - - let batchNum = 0; - let totalDocs = 0; - dest.on('progress', (progressStats) => { - batchNum++; - const docsInBatch = getExpectedDocsInBatch( - batchNum, - docs.length, - isFLE - ); - totalDocs += docsInBatch; - expect(progressStats).to.deep.equal({ - docsProcessed: totalDocs, - docsWritten: totalDocs, - errors: [], - }); - - const streamStats = dest.getStats(); - if (streamStats.insertedCount === docs.length) { - resolveWrite(); - } - }); - - await insertDocs(dest, docs); - - await writePromise; - - const stats = dest.getStats(); - - expect(stats).to.deep.equal({ - ok: numBatches, - insertedCount: docs.length, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - writeErrors: [], - writeConcernErrors: [], - }); - }); - - for (const stopOnErrors of [true, false]) { - it(`${stopOnErrors ? 'stops' : 'does not stop'} on the first error for ${ - isFLE ? 'FLE2' : 'regular' - } collection if stopOnErrors is ${stopOnErrors}`, async function () { - const numBatches = getExpectedNumBatches(docs.length, isFLE, true); - - const dataService = getDataService({ isFLE, throwErrors: true }); - - const dest = createCollectionWriteStream( - dataService as any, - 'db.col', - stopOnErrors - ); - - let resolveWrite: () => void; - const writePromise = new Promise((resolve) => { - resolveWrite = resolve; - }); - - let batchNum = 0; - let totalDocs = 0; - const errors: Error[] = []; - - let rejectError: (err: Error) => void; - const errorPromise = new Promise((resolve, reject) => { - rejectError = reject; - }); - - dest.on('error', (err) => { - // we'll only get here if stopOnErrors is true - rejectError(err); - }); - - dest.on('progress', (progressStats) => { - batchNum++; - if (batchNum > 1) { - // we should never have made it to the second batch if stopOnErrors is true - expect(stopOnErrors).to.equal(false); - } - const docsInBatch = getExpectedDocsInBatch( - batchNum, - docs.length, - isFLE - ); - totalDocs += docsInBatch; - - if (isFLE && batchNum === 1) { - const errorsInBatch = stopOnErrors ? 1 : docsInBatch; - for (let i = 0; i < errorsInBatch; ++i) { - errors.push(new Error('fake insertOne error')); - } - } else { - errors.push(new Error('fake bulkWrite error')); - } - - // comparing errors is weird - for (let i = 0; i < errors.length; ++i) { - expect(progressStats.errors[i].message).to.equal(errors[i].message); - } - expect(progressStats.errors.length).to.equal(errors.length); - delete progressStats.errors; - - expect(progressStats).to.deep.equal({ - docsProcessed: totalDocs, - docsWritten: 0, - }); - - if (batchNum === numBatches) { - resolveWrite(); - } - }); - - await insertDocs(dest, docs); - - await writePromise; - - if (stopOnErrors) { - await expect(errorPromise).to.be.rejectedWith( - Error, - isFLE ? 'fake insertOne error' : 'fake bulkWrite error' - ); - } - - const stats = dest.getStats(); - - expect(stats).to.deep.equal({ - ok: isFLE ? 1 : 0, // wat? - insertedCount: 0, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - // all the errors are on dest._errors, so only on the progress stats, not on the stream stats - writeErrors: [], - writeConcernErrors: [], - }); - }); - } - } -}); diff --git a/packages/compass-import-export/src/utils/collection-stream.ts b/packages/compass-import-export/src/utils/collection-stream.ts deleted file mode 100644 index 41fcb5e7423..00000000000 --- a/packages/compass-import-export/src/utils/collection-stream.ts +++ /dev/null @@ -1,301 +0,0 @@ -/* eslint-disable no-console */ -import { Writable } from 'stream'; -import type { - MongoServerError, - Document, - MongoBulkWriteError, - AnyBulkWriteOperation, - WriteError, - WriteConcernError, - BulkWriteResult, -} from 'mongodb'; -import type { DataService } from 'mongodb-data-service'; -import type { ErrorJSON } from '../import/import-types'; -import { errorToJSON } from '../import/import-utils'; - -import { createDebug } from './logger'; - -const debug = createDebug('collection-stream'); - -export type CollectionStreamProgressError = - | Error - | WriteError - | WriteConcernError; - -type CollectionStreamError = Error & { - cause?: CollectionStreamProgressError; -}; - -type WriteCollectionStreamProgressError = Error & { - index: number; - code: MongoServerError['code']; - op: MongoServerError['op']; - errInfo: MongoServerError['errInfo']; -}; - -function mongodbServerErrorToJSError({ - index, - code, - errmsg, - op, - errInfo, -}: Pick & - Partial< - Pick - >): WriteCollectionStreamProgressError { - const e: WriteCollectionStreamProgressError = new Error(errmsg) as any; - e.index = index; - e.code = code; - e.op = op; - e.errInfo = errInfo; - // https://www.mongodb.com/docs/manual/reference/method/BulkWriteResult/#mongodb-data-BulkWriteResult.writeErrors - e.name = index && op ? 'WriteError' : 'WriteConcernError'; - return e; -} - -const numKeys = [ - 'insertedCount', - 'matchedCount', - 'modifiedCount', - 'deletedCount', - 'upsertedCount', - // Even though it's a boolean, treating it as num might allow us to see - // how many batches finished "correctly" if `stopOnErrors` is `false` if - // we ever need that - 'ok', -] as const; // `as const satisfies readonly (keyof BulkWriteResult)[]` once prettier understands this syntax - -type NumericBulkWriteResult = { - [numkey in keyof BulkWriteResult & typeof numKeys[number]]?: number; -}; - -export type CollectionStreamProgress = { - docsWritten: number; - docsProcessed: number; - errors: CollectionStreamProgressError[]; -}; - -export type CollectionStreamStats = Required & { - writeErrors: WriteCollectionStreamProgressError[]; - writeConcernErrors: WriteCollectionStreamProgressError[]; -}; -export class WritableCollectionStream extends Writable { - dataService: Pick; - ns: string; - BATCH_SIZE: number; - docsWritten: number; - docsProcessed: number; - stopOnErrors: boolean; - batch: Document[]; - _batchCounter: number; - _stats: CollectionStreamStats; - _errors: CollectionStreamProgressError[]; - errorCallback?: (error: ErrorJSON) => void; - - constructor( - dataService: Pick, - ns: string, - stopOnErrors: boolean, - errorCallback?: (error: ErrorJSON) => void - ) { - super({ objectMode: true }); - this.dataService = dataService; - this.ns = ns; - this.BATCH_SIZE = 1000; - this.docsWritten = 0; - this.docsProcessed = 0; - this.stopOnErrors = stopOnErrors; - - this.batch = []; - this._batchCounter = 0; - - this._stats = { - ok: 0, - insertedCount: 0, - matchedCount: 0, - modifiedCount: 0, - deletedCount: 0, - upsertedCount: 0, - writeErrors: [], - writeConcernErrors: [], - }; - - this._errors = []; - this.errorCallback = errorCallback; - } - - _write( - document: Document, - _encoding: BufferEncoding, - next: (err?: Error) => void - ) { - this.batch.push(document); - - if (this.batch.length >= this.BATCH_SIZE) { - return this._executeBatch(next); - } - - next(); - } - - _final(callback: (err?: Error) => void) { - debug('running _final()'); - - if (this.batch.length === 0) { - debug('%d docs written', this.docsWritten); - return callback(); - } - - debug('draining buffered docs', this.batch.length); - - void this._executeBatch(callback); - } - - async _executeBatch(callback: (err?: Error) => void) { - const documents = this.batch; - - this.batch = []; - - let result: NumericBulkWriteResult & Partial; - - try { - result = await this.dataService.bulkWrite( - this.ns, - documents.map( - (document: any): AnyBulkWriteOperation => ({ - insertOne: { document }, - }) - ), - { - ordered: this.stopOnErrors, - retryWrites: false, - checkKeys: false, - } - ); - } catch (bulkWriteError: any) { - // Currently, the server does not support batched inserts for FLE2: - // https://jira.mongodb.org/browse/SERVER-66315 - // We check for this specific error and re-try inserting documents one by one. - if (bulkWriteError.code === 6371202) { - this.BATCH_SIZE = 1; - - let insertedCount = 0; - - for (const doc of documents) { - try { - await this.dataService.insertOne(this.ns, doc); - insertedCount += 1; - } catch (insertOneByOneError: any) { - this._errors.push(insertOneByOneError); - this.errorCallback?.(errorToJSON(insertOneByOneError)); - - if (this.stopOnErrors) { - break; - } - } - } - - result = { ok: 1, insertedCount }; - } else { - // If we are writing with `ordered: false`, bulkWrite will throw and - // will not return any result, but server might write some docs and bulk - // result can still be accessed on the error instance - result = (bulkWriteError as MongoBulkWriteError).result; - - this._errors.push(bulkWriteError); - this.errorCallback?.(errorToJSON(bulkWriteError)); - } - } - - // Driver seems to return null instead of undefined in some rare cases - // when the operation ends in error, instead of relying on - // `_mergeBulkOpResult` default argument substitution, we need to keep - // this OR expression here - this._mergeBulkOpResult(result || {}); - - this.docsWritten = this._stats.insertedCount; - this.docsProcessed += documents.length; - this._batchCounter++; - - const progressStats: CollectionStreamProgress = { - docsWritten: this.docsWritten, - docsProcessed: this.docsProcessed, - errors: this._errors - .concat(this._stats.writeErrors) - .concat(this._stats.writeConcernErrors), - }; - - this.emit('progress', progressStats); - - return callback(this._makeStreamError()); - } - - _makeStreamError(): CollectionStreamError | undefined { - if (this.stopOnErrors && this._errors.length) { - const error = this._errors[0]; - if (Object.prototype.toString.call(error) === '[object Error]') { - return error as Error; - } - return { - name: 'CollectionStreamError', - message: 'Something went wrong while writing data to a collection', - cause: error, - }; - } - return undefined; - } - - _mergeBulkOpResult( - result: NumericBulkWriteResult & Partial = {} - ) { - for (const key of numKeys) { - this._stats[key] += result[key] || 0; - } - - this._stats.writeErrors.push( - ...(result?.getWriteErrors?.() || []).map(mongodbServerErrorToJSError) - ); - - const writeConcernError = result?.getWriteConcernError?.(); - if (writeConcernError) { - this._stats.writeConcernErrors.push( - mongodbServerErrorToJSError(writeConcernError) - ); - } - } - - getErrors() { - return this._errors; - } - - getStats() { - return this._stats; - } - - printJobStats() { - console.group('Import Info'); - console.table(this.getStats()); - const errors = this._errors - .concat(this._stats.writeErrors) - .concat(this._stats.writeConcernErrors); - if (errors.length) { - console.log('Errors Seen'); - console.log(errors); - } - console.groupEnd(); - } -} - -export const createCollectionWriteStream = function ( - dataService: Pick, - ns: string, - stopOnErrors: boolean, - errorCallback?: (error: ErrorJSON) => void -) { - return new WritableCollectionStream( - dataService, - ns, - stopOnErrors, - errorCallback - ); -}; diff --git a/packages/compass-indexes/package.json b/packages/compass-indexes/package.json index 12e2a96ade4..a9b0fd94c5d 100644 --- a/packages/compass-indexes/package.json +++ b/packages/compass-indexes/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "5.37.0", + "version": "5.38.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,15 +48,15 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -67,24 +67,24 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-storage": "^0.17.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-storage": "^0.18.0", "@mongodb-js/mongodb-constants": "^0.10.0", "@mongodb-js/shell-bson-parser": "^1.1.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "mongodb-query-parser": "^4.2.0", "numeral": "^2.0.6", "react": "^17.0.2", diff --git a/packages/compass-indexes/src/index.ts b/packages/compass-indexes/src/index.ts index 7e83527fdf4..e58ae962af9 100644 --- a/packages/compass-indexes/src/index.ts +++ b/packages/compass-indexes/src/index.ts @@ -5,38 +5,21 @@ import { DropIndexComponent, } from './stores/drop-index'; import { registerHadronPlugin } from 'hadron-app-registry'; -import type { CollectionTabPluginMetadata } from '@mongodb-js/compass-collection'; -import type { IndexesDataService } from './stores/store'; import { activateIndexesPlugin, type IndexesDataServiceProps, } from './stores/store'; import Indexes from './components/indexes/indexes'; import { - type ConnectionInfoAccess, connectionInfoAccessLocator, dataServiceLocator, type DataServiceLocator, } from '@mongodb-js/compass-connections/provider'; -import type { MongoDBInstance } from '@mongodb-js/compass-app-stores/provider'; import { mongoDBInstanceLocator } from '@mongodb-js/compass-app-stores/provider'; -import type { Logger } from '@mongodb-js/compass-logging'; import { createLoggerLocator } from '@mongodb-js/compass-logging/provider'; -import { - telemetryLocator, - type TrackFunction, -} from '@mongodb-js/compass-telemetry/provider'; +import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider'; -export const CompassIndexesHadronPlugin = registerHadronPlugin< - CollectionTabPluginMetadata, - { - dataService: () => IndexesDataService; - connectionInfoAccess: () => ConnectionInfoAccess; - instance: () => MongoDBInstance; - logger: () => Logger; - track: () => TrackFunction; - } ->( +export const CompassIndexesHadronPlugin = registerHadronPlugin( { name: 'CompassIndexes', component: Indexes as React.FunctionComponent, diff --git a/packages/compass-intercom/package.json b/packages/compass-intercom/package.json index d8b4a08eba7..37c2ca3727b 100644 --- a/packages/compass-intercom/package.json +++ b/packages/compass-intercom/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.10.0", + "version": "0.11.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -50,8 +50,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -67,7 +67,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "compass-preferences-model": "^2.26.0", - "@mongodb-js/compass-logging": "^1.4.3" + "compass-preferences-model": "^2.27.0", + "@mongodb-js/compass-logging": "^1.4.4" } } diff --git a/packages/compass-logging/package.json b/packages/compass-logging/package.json index 3fc3733df9f..631a32de16c 100644 --- a/packages/compass-logging/package.json +++ b/packages/compass-logging/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.4.3", + "version": "1.4.4", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -52,15 +52,15 @@ }, "dependencies": { "debug": "^4.3.4", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "is-electron-renderer": "^2.0.1", "mongodb-log-writer": "^1.4.2", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/compass-maybe-protect-connection-string/package.json b/packages/compass-maybe-protect-connection-string/package.json index 79a1cb6a0be..565e29b90d7 100644 --- a/packages/compass-maybe-protect-connection-string/package.json +++ b/packages/compass-maybe-protect-connection-string/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.24.0", + "version": "0.25.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -50,12 +50,12 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "mongodb-connection-string-url": "^3.0.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/compass-preferences-model/package.json b/packages/compass-preferences-model/package.json index d0c6ced88a9..afb71983ebe 100644 --- a/packages/compass-preferences-model/package.json +++ b/packages/compass-preferences-model/package.json @@ -2,7 +2,7 @@ "name": "compass-preferences-model", "description": "Compass preferences model", "author": "Lucas Hrabovsky ", - "version": "2.26.0", + "version": "2.27.0", "bugs": { "url": "https://jira.mongodb.org/projects/COMPASS/issues", "email": "compass@mongodb.com" @@ -49,12 +49,12 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/devtools-proxy-support": "^0.3.5", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "bson": "^6.7.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "react": "^17.0.2", @@ -62,8 +62,8 @@ "zod": "^3.22.3" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@testing-library/react": "^12.1.5", "@types/js-yaml": "^4.0.5", "@types/yargs-parser": "21.0.0", diff --git a/packages/compass-preferences-model/src/feature-flags.ts b/packages/compass-preferences-model/src/feature-flags.ts index 0e3c28a238e..58a2774d09b 100644 --- a/packages/compass-preferences-model/src/feature-flags.ts +++ b/packages/compass-preferences-model/src/feature-flags.ts @@ -18,7 +18,6 @@ export type FeatureFlags = { newExplainPlan: boolean; showInsights: boolean; enableRenameCollectionModal: boolean; - enableNewMultipleConnectionSystem: boolean; enableQueryHistoryAutocomplete: boolean; enableProxySupport: boolean; }; @@ -64,18 +63,6 @@ export const featureFlags: Required<{ }, }, - /** - * Feature flag for the new multiple connection UI. - * Epic: COMPASS-6410 - */ - enableNewMultipleConnectionSystem: { - stage: 'development', - description: { - short: 'Enables support for multiple connections.', - long: 'Allows users to open multiple connections in the same window.', - }, - }, - /** * Feature flag for adding query history items to the query bar autocompletion. COMPASS-8096 */ @@ -91,7 +78,7 @@ export const featureFlags: Required<{ * Feature flag for explicit proxy configuration support. */ enableProxySupport: { - stage: 'development', + stage: 'released', description: { short: 'Enables support for explicit proxy configuration.', long: 'Allows users to specify proxy configuration for the entire Compass application.', diff --git a/packages/compass-preferences-model/src/preferences-schema.ts b/packages/compass-preferences-model/src/preferences-schema.ts index f7cfa99b9d5..c7952637caf 100644 --- a/packages/compass-preferences-model/src/preferences-schema.ts +++ b/packages/compass-preferences-model/src/preferences-schema.ts @@ -67,6 +67,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & enablePerformanceAdvisorBanner: boolean; maximumNumberOfActiveConnections?: number; enableShowDialogOnQuit: boolean; + enableMultipleConnectionSystem: boolean; enableProxySupport: boolean; proxy: string; }; @@ -213,7 +214,7 @@ const featureFlagsProps: Required<{ }> = Object.fromEntries( Object.entries(featureFlags).map(([key, value]) => [ key as keyof FeatureFlags, - featureFlagToPreferenceDefinition(value), + featureFlagToPreferenceDefinition(key, value), ]) ) as unknown as Required<{ [K in keyof FeatureFlags]: PreferenceDefinition; @@ -780,6 +781,18 @@ export const storedUserPreferencesProps: Required<{ type: 'boolean', }, + enableMultipleConnectionSystem: { + ui: true, + cli: true, + global: true, + description: { + short: 'Enables support for multiple connections.', + long: 'Allows users to open multiple connections in the same window.', + }, + validator: z.boolean().default(true), + type: 'boolean', + }, + proxy: { ui: true, cli: true, @@ -1026,6 +1039,7 @@ function deriveReadOnlyOptionState( // Helper to convert feature flag definitions to preference definitions function featureFlagToPreferenceDefinition( + key: string, featureFlag: FeatureFlagDefinition ): PreferenceDefinition { return { diff --git a/packages/compass-preferences-model/src/read-only-preferences-access.ts b/packages/compass-preferences-model/src/read-only-preferences-access.ts index d58d980284f..0cdcea9a32d 100644 --- a/packages/compass-preferences-model/src/read-only-preferences-access.ts +++ b/packages/compass-preferences-model/src/read-only-preferences-access.ts @@ -1,5 +1,6 @@ import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { Preferences, type PreferencesAccess } from './preferences'; +import type { UserPreferences } from './preferences-schema'; import { type AllPreferences } from './preferences-schema'; import { InMemoryStorage } from './preferences-in-memory-storage'; import { getActiveUser } from './utils'; @@ -13,7 +14,10 @@ export class ReadOnlyPreferenceAccess implements PreferencesAccess { }); } - savePreferences() { + // Not used, but we extend this interface elsewhere so need to provide those + // for types + // eslint-disable-next-line @typescript-eslint/no-unused-vars + savePreferences(_attributes: Partial) { return Promise.resolve(this._preferences.getPreferences()); } @@ -37,7 +41,10 @@ export class ReadOnlyPreferenceAccess implements PreferencesAccess { return Promise.resolve(this._preferences.getPreferenceStates()); } - onPreferenceValueChanged() { + // Not used, but we extend this interface elsewhere so need to provide those + // for types + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onPreferenceValueChanged(_key: any, _cb: (value: any) => void) { return () => { // noop }; diff --git a/packages/compass-query-bar/package.json b/packages/compass-query-bar/package.json index f89756f74ba..6e30d95a82b 100644 --- a/packages/compass-query-bar/package.json +++ b/packages/compass-query-bar/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "8.39.0", + "version": "8.40.0", "homepage": "https://github.com/mongodb-js/compass", "license": "SSPL", "bugs": { @@ -48,8 +48,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^13.5.0", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", @@ -68,27 +68,27 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", "@mongodb-js/mongodb-constants": "^0.10.0", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "react": "^17.0.2", "react-redux": "^8.1.3", diff --git a/packages/compass-saved-aggregations-queries/package.json b/packages/compass-saved-aggregations-queries/package.json index 6ba000c273f..f1563465ce3 100644 --- a/packages/compass-saved-aggregations-queries/package.json +++ b/packages/compass-saved-aggregations-queries/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.38.0", + "version": "1.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,19 +48,19 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "fuse.js": "^6.5.3", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -68,9 +68,8 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx index 6576f46561f..71c6b6e8786 100644 --- a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx @@ -122,7 +122,7 @@ describe('SavedItemCard', function () { it('should render an "Open in" action', async function () { const preferences = await createSandboxFromDefaultPreferences(); await preferences.savePreferences({ - enableNewMultipleConnectionSystem: true, + enableMultipleConnectionSystem: true, }); const onAction = Sinon.spy(); diff --git a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx index 9fdece76613..15135830e40 100644 --- a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx @@ -144,7 +144,7 @@ const CardActions: React.FunctionComponent<{ onAction: SavedItemCardProps['onAction']; }> = ({ itemId, isVisible, onAction }) => { const multiConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const actions: MenuAction[] = useMemo(() => { return multiConnectionsEnabled diff --git a/packages/compass-saved-aggregations-queries/src/index.spec.tsx b/packages/compass-saved-aggregations-queries/src/index.spec.tsx index eea0e26c61b..adb05c3ebec 100644 --- a/packages/compass-saved-aggregations-queries/src/index.spec.tsx +++ b/packages/compass-saved-aggregations-queries/src/index.spec.tsx @@ -1,12 +1,5 @@ import React from 'react'; import Sinon from 'sinon'; -import { - render, - screen, - cleanup, - within, - waitFor, -} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; import { queries, pipelines } from '../test/fixtures'; @@ -15,46 +8,32 @@ import type { PipelineStorage, FavoriteQueryStorage, } from '@mongodb-js/my-queries-storage/provider'; -import { - ConnectionStatus, - ConnectionsManager, - ConnectionsManagerProvider, - type DataService, -} from '@mongodb-js/compass-connections/provider'; -import { - type Logger, - createNoopLogger, -} from '@mongodb-js/compass-logging/provider'; import { type MongoDBInstance, type MongoDBInstancesManager, TestMongoDBInstanceManager, } from '@mongodb-js/compass-app-stores/provider'; -import { - PreferencesProvider, - type PreferencesAccess, -} from 'compass-preferences-model/provider'; -import { - type ConnectionStorage, - ConnectionStorageProvider, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import { type WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; import { getConnectionTitle } from '@mongodb-js/connection-info'; +import type { RenderWithConnectionsResult } from '@mongodb-js/compass-connections/test'; +import { + renderWithConnections, + screen, + cleanup, + within, + waitFor, +} from '@mongodb-js/compass-connections/test'; let id = 0; function getConnection() { id += 1; return { - status: ConnectionStatus.Disconnected, connectionInfo: { id: `${id}FOO`, connectionOptions: { connectionString: `mongodb://localhost:2702${id}`, }, }, - dataService: {} as DataService, instance: { fetchDatabases() {}, getNamespace() {}, @@ -87,17 +66,14 @@ describe('AggregationsQueriesList', function () { } as any; let connectionOne: ReturnType; let connectionTwo: ReturnType; - let logger: Logger; let queryStorage: FavoriteQueryStorage; let pipelineStorage: PipelineStorage; - let connectionStorage: ConnectionStorage; - let connectionsManager: ConnectionsManager; let instancesManager: MongoDBInstancesManager; - let preferencesAccess: PreferencesAccess; - let workspaces: WorkspacesService; + let workspaces: Sinon.SinonSpiedInstance; + let connectionsStore: RenderWithConnectionsResult['connectionsStore']; const renderPlugin = () => { - const Plugin = MyQueriesPlugin.withMockServices({ + const PluginWithMocks = MyQueriesPlugin.withMockServices({ instancesManager, favoriteQueryStorageAccess: { getStorage() { @@ -107,15 +83,13 @@ describe('AggregationsQueriesList', function () { pipelineStorage: pipelineStorage, workspaces, }); - render( - - - - - - - - ); + const result = renderWithConnections(, { + connections: [connectionOne.connectionInfo, connectionTwo.connectionInfo], + preferences: { + enableMultipleConnectionSystem: true, + }, + }); + connectionsStore = result.connectionsStore; }; const selectContextMenuItem = ( @@ -135,10 +109,9 @@ describe('AggregationsQueriesList', function () { return queryCard; }; - beforeEach(async function () { + beforeEach(function () { connectionOne = getConnection(); connectionTwo = getConnection(); - logger = createNoopLogger(); queryStorage = { loadAll() { return Promise.resolve([]); @@ -151,43 +124,10 @@ describe('AggregationsQueriesList', function () { }, updateAttributes() {}, } as unknown as PipelineStorage; - connectionStorage = new InMemoryConnectionStorage([ - connectionOne.connectionInfo, - connectionTwo.connectionInfo, - ]); - connectionsManager = new ConnectionsManager({ - logger: logger.log.unbound, - }); instancesManager = new TestMongoDBInstanceManager(); - preferencesAccess = await createSandboxFromDefaultPreferences(); - await preferencesAccess.savePreferences({ - enableNewMultipleConnectionSystem: true, - }); - workspaces = { + workspaces = sandbox.spy({ openCollectionWorkspace() {}, - } as unknown as WorkspacesService; - - sandbox.stub(connectionsManager, 'statusOf').callsFake((id) => { - if (id === connectionOne.connectionInfo.id) { - return connectionOne.status; - } else if (id === connectionTwo.connectionInfo.id) { - return connectionTwo.status; - } else { - throw new Error('Unexpected id'); - } - }); - - sandbox - .stub(connectionsManager, 'getDataServiceForConnection') - .callsFake((id) => { - if (id === connectionOne.connectionInfo.id) { - return connectionOne.dataService; - } else if (id === connectionTwo.connectionInfo.id) { - return connectionTwo.dataService; - } else { - throw new Error('Unexpected id'); - } - }); + } as unknown as WorkspacesService); sandbox .stub(instancesManager, 'getMongoDBInstanceForConnection') @@ -480,24 +420,20 @@ describe('AggregationsQueriesList', function () { }); context('when connected to just one connection', function () { - beforeEach(function () { - connectionOne.status = ConnectionStatus.Connected; - }); - context('and clicked on a saved item', function () { it('should open the query right away if the namespace exist in the current connection', async function () { - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); sandbox .stub(connectionOne.instance, 'getNamespace') .resolves({} as any); await renderPluginWithWait(); + await connectionsStore.actions.connect(connectionOne.connectionInfo); + selectCardForItem(query._id); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionOne.connectionInfo.id}`, `${query._ns}`, { @@ -513,10 +449,6 @@ describe('AggregationsQueriesList', function () { 'and namespace does not exist in the current connection', function () { it('should open the namespace not found modal and allow opening query from right within the modal', async function () { - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); const queryStorageUpdateSpy = sandbox.spy( queryStorage, 'updateAttributes' @@ -524,6 +456,9 @@ describe('AggregationsQueriesList', function () { connectionOne.instance = mockedInstanceWithDatabaseAndCollection('connection-one'); await renderPluginWithWait(); + await connectionsStore.actions.connect( + connectionOne.connectionInfo + ); selectCardForItem(query._id); await waitFor(() => { expect(screen.getByTestId('open-item-modal')).to.exist; @@ -550,7 +485,9 @@ describe('AggregationsQueriesList', function () { userEvent.click(screen.getByTestId('submit-button')); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionOne.connectionInfo.id}`, 'connection-one-dummy-db.connection-one-dummy-coll', { @@ -573,10 +510,6 @@ describe('AggregationsQueriesList', function () { 'and trying to open the query using context menu "Open in"', function () { it('should open the select namespace modal and allow running query right from the modal', async function () { - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); const queryStorageUpdateSpy = sandbox.spy( queryStorage, 'updateAttributes' @@ -584,6 +517,9 @@ describe('AggregationsQueriesList', function () { connectionOne.instance = mockedInstanceWithDatabaseAndCollection('connection-one'); await renderPluginWithWait(); + await connectionsStore.actions.connect( + connectionOne.connectionInfo + ); selectContextMenuItem(query._id, 'open-in'); await waitFor(() => { @@ -613,7 +549,9 @@ describe('AggregationsQueriesList', function () { userEvent.click(screen.getByTestId('submit-button')); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionOne.connectionInfo.id}`, 'connection-one-dummy-db.connection-one-dummy-coll', { @@ -633,25 +571,20 @@ describe('AggregationsQueriesList', function () { }); context('when connected to multiple connections', function () { - beforeEach(function () { - connectionOne.status = ConnectionStatus.Connected; - connectionTwo.status = ConnectionStatus.Connected; - }); - context('and clicked on a saved item', function () { it('should open the query right away if the namespace exists in only one of the active connections', async function () { - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); sandbox .stub(connectionTwo.instance, 'getNamespace') .resolves({} as any); await renderPluginWithWait(); + await connectionsStore.actions.connect(connectionOne.connectionInfo); + await connectionsStore.actions.connect(connectionTwo.connectionInfo); selectCardForItem(query._id); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionTwo.connectionInfo.id}`, query._ns, { @@ -673,11 +606,13 @@ describe('AggregationsQueriesList', function () { sandbox .stub(connectionTwo.instance, 'getNamespace') .resolves({} as any); - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); await renderPluginWithWait(); + await connectionsStore.actions.connect( + connectionOne.connectionInfo + ); + await connectionsStore.actions.connect( + connectionTwo.connectionInfo + ); selectCardForItem(query._id); await waitFor(() => { @@ -706,7 +641,9 @@ describe('AggregationsQueriesList', function () { userEvent.click(screen.getByTestId('submit-button')); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionTwo.connectionInfo.id}`, query._ns, { @@ -724,10 +661,6 @@ describe('AggregationsQueriesList', function () { 'and namespace does not exist in any of the active connections', function () { it('should open select connection and namespace modal and allow running query right from the modal', async function () { - const openCollectionWorkspaceSpy = sandbox.spy( - workspaces, - 'openCollectionWorkspace' - ); const queryStorageUpdateSpy = sandbox.spy( queryStorage, 'updateAttributes' @@ -735,6 +668,12 @@ describe('AggregationsQueriesList', function () { connectionTwo.instance = mockedInstanceWithDatabaseAndCollection('connection-two'); await renderPluginWithWait(); + await connectionsStore.actions.connect( + connectionOne.connectionInfo + ); + await connectionsStore.actions.connect( + connectionTwo.connectionInfo + ); selectCardForItem(query._id); await waitFor(() => { @@ -767,7 +706,9 @@ describe('AggregationsQueriesList', function () { userEvent.click(screen.getByTestId('submit-button')); await waitFor(() => { - expect(openCollectionWorkspaceSpy).to.be.calledOnceWithExactly( + expect( + workspaces.openCollectionWorkspace + ).to.be.calledOnceWithExactly( `${connectionTwo.connectionInfo.id}`, 'connection-two-dummy-db.connection-two-dummy-coll', { diff --git a/packages/compass-saved-aggregations-queries/src/index.ts b/packages/compass-saved-aggregations-queries/src/index.ts index c977c816dbd..adaacddc538 100644 --- a/packages/compass-saved-aggregations-queries/src/index.ts +++ b/packages/compass-saved-aggregations-queries/src/index.ts @@ -24,10 +24,7 @@ const serviceLocators = { favoriteQueryStorageAccess: favoriteQueryStorageAccessLocator, }; -export const MyQueriesPlugin = registerHadronPlugin< - React.ComponentProps, - typeof serviceLocators ->( +export const MyQueriesPlugin = registerHadronPlugin( { name: 'MyQueries', component: AggregationsQueriesList, diff --git a/packages/compass-saved-aggregations-queries/tsconfig.json b/packages/compass-saved-aggregations-queries/tsconfig.json index 8176eabdf2c..4b817cbde74 100644 --- a/packages/compass-saved-aggregations-queries/tsconfig.json +++ b/packages/compass-saved-aggregations-queries/tsconfig.json @@ -5,5 +5,5 @@ "lib": ["ES2020", "DOM"] }, "include": ["src/**/*"], - "exclude": ["./src/**/*.spec.*"] + "exclude": ["./src/**/*.spec.*", "./src/**/*.test.*"] } diff --git a/packages/compass-schema-validation/package.json b/packages/compass-schema-validation/package.json index 0a3b1c83801..e4b10bb72e1 100644 --- a/packages/compass-schema-validation/package.json +++ b/packages/compass-schema-validation/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "6.38.0", + "version": "6.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,37 +48,37 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", - "hadron-ipc": "^3.2.20", + "hadron-ipc": "^3.2.21", "mocha": "^10.2.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "nyc": "^15.1.0", "react-dom": "^17.0.2", "sinon": "^8.1.1", "typescript": "^5.0.4" }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-crud": "^13.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-crud": "^13.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "javascript-stringify": "^2.0.1", "lodash": "^4.17.21", "mongodb-ns": "^2.4.2", diff --git a/packages/compass-schema/package.json b/packages/compass-schema/package.json index 76d47f7fde5..9222501a6ef 100644 --- a/packages/compass-schema/package.json +++ b/packages/compass-schema/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "6.39.0", + "version": "6.40.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,9 +48,9 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/my-queries-storage": "^0.15.0", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/my-queries-storage": "^0.15.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -73,26 +73,26 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { - "@mongodb-js/compass-collection": "^4.37.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-field-store": "^9.13.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-query-bar": "^8.39.0", - "@mongodb-js/connection-storage": "^0.17.0", + "@mongodb-js/compass-collection": "^4.38.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-field-store": "^9.14.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-query-bar": "^8.40.0", + "@mongodb-js/connection-storage": "^0.18.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", + "compass-preferences-model": "^2.27.0", "d3": "^3.5.17", - "hadron-app-registry": "^9.2.2", - "hadron-document": "^8.6.0", + "hadron-app-registry": "^9.2.3", + "hadron-document": "^8.6.1", "leaflet": "^1.5.1", "leaflet-defaulticon-compatibility": "^0.1.1", "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "moment": "^2.29.4", "mongodb": "^6.8.0", - "mongodb-query-util": "^2.2.5", + "mongodb-query-util": "^2.2.6", "mongodb-schema": "^12.2.0", "numeral": "^1.5.6", "prop-types": "^15.7.2", @@ -100,7 +100,7 @@ "react-leaflet": "^2.4.0", "react-leaflet-draw": "^0.19.0", "reflux": "^0.4.1", - "@mongodb-js/reflux-state-mixin": "^1.0.4" + "@mongodb-js/reflux-state-mixin": "^1.0.5" }, "is_compass_plugin": true } diff --git a/packages/compass-serverstats/package.json b/packages/compass-serverstats/package.json index 053de06d82a..6a57454d53e 100644 --- a/packages/compass-serverstats/package.json +++ b/packages/compass-serverstats/package.json @@ -2,7 +2,7 @@ "name": "@mongodb-js/compass-serverstats", "description": "Compass Real Time", "private": true, - "version": "16.37.0", + "version": "16.38.0", "main": "dist/index.js", "compass:main": "src/index.ts", "exports": { @@ -30,15 +30,15 @@ }, "license": "SSPL", "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", "d3": "^3.5.17", "d3-timer": "^1.0.3", "debug": "^4.3.4", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb-ns": "^2.4.2", "prop-types": "^15.7.2", @@ -46,8 +46,8 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/d3": "^3.5.x", diff --git a/packages/compass-settings/package.json b/packages/compass-settings/package.json index 3d5018f4a26..6f166f15a7f 100644 --- a/packages/compass-settings/package.json +++ b/packages/compass-settings/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.38.0", + "version": "0.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -49,21 +49,21 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/atlas-service": "^0.26.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-generative-ai": "^0.20.0", - "@mongodb-js/compass-logging": "^1.4.3", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "@mongodb-js/atlas-service": "^0.27.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-generative-ai": "^0.21.0", + "@mongodb-js/compass-logging": "^1.4.4", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/compass-settings/src/components/modal.tsx b/packages/compass-settings/src/components/modal.tsx index 38e7723b044..3879123dd96 100644 --- a/packages/compass-settings/src/components/modal.tsx +++ b/packages/compass-settings/src/components/modal.tsx @@ -34,7 +34,6 @@ type SettingsModalProps = { isAIFeatureEnabled: boolean; isOpen: boolean; isOIDCEnabled: boolean; - isProxySupportEnabled: boolean; selectedTab: SettingsTabId | undefined; onMount?: () => void; onClose: () => void; @@ -65,7 +64,6 @@ const settingsStyles = css( export const SettingsModal: React.FunctionComponent = ({ isAIFeatureEnabled, - isProxySupportEnabled, isOpen, selectedTab, onMount, @@ -86,6 +84,11 @@ export const SettingsModal: React.FunctionComponent = ({ { tabId: 'general', name: 'General', component: GeneralSettings }, { tabId: 'theme', name: 'Theme', component: ThemeSettings }, { tabId: 'privacy', name: 'Privacy', component: PrivacySettings }, + { + tabId: 'proxy', + name: 'Proxy Configuration', + component: ProxySettings, + }, ]; if ( @@ -108,14 +111,6 @@ export const SettingsModal: React.FunctionComponent = ({ }); } - if (isProxySupportEnabled) { - settings.push({ - tabId: 'proxy', - name: 'Proxy Configuration', - component: ProxySettings, - }); - } - if (useShouldShowFeaturePreviewSettings()) { settings.push({ tabId: 'preview', @@ -170,7 +165,6 @@ export default connect( state.settings.isModalOpen && state.settings.loadingState === 'ready', isAIFeatureEnabled: !!state.settings.settings.enableGenAIFeatures, isOIDCEnabled: !!state.settings.settings.enableOidc, - isProxySupportEnabled: !!state.settings.settings.enableProxySupport, hasChangedSettings: state.settings.updatedFields.length > 0, selectedTab: state.settings.tab, }; diff --git a/packages/compass-shell/package.json b/packages/compass-shell/package.json index c3146f3194b..15f1c29140f 100644 --- a/packages/compass-shell/package.json +++ b/packages/compass-shell/package.json @@ -6,7 +6,7 @@ "email": "compass@mongodb.com" }, "private": true, - "version": "3.37.0", + "version": "3.38.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -49,33 +49,32 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/compass-workspaces": "^0.19.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/compass-workspaces": "^0.20.0", "@mongosh/browser-repl": "^2.3.0", "@mongosh/logging": "^2.3.0", "@mongosh/node-runtime-worker-thread": "^2.3.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "react": "^17.0.2", "react-redux": "^8.1.3", "redux": "^4.2.1", "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", "depcheck": "^1.4.1", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-mocha": "^12.2.0", "enzyme": "^3.11.0", "eslint": "^7.25.0", diff --git a/packages/compass-shell/src/plugin.spec.tsx b/packages/compass-shell/src/plugin.spec.tsx index a4e98309ad7..7d9d4b6a652 100644 --- a/packages/compass-shell/src/plugin.spec.tsx +++ b/packages/compass-shell/src/plugin.spec.tsx @@ -1,90 +1,32 @@ -import sinon from 'sinon'; import React from 'react'; -import type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; import { expect } from 'chai'; - -import { CompassShell } from './components/compass-shell'; import { CompassShellPlugin } from './index'; -import { AppRegistryProvider } from 'hadron-app-registry'; -import { - ConnectionsManager, - ConnectionsManagerProvider, - ConnectionInfoProvider, -} from '@mongodb-js/compass-connections/provider'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { - ConnectionStorageProvider, - InMemoryConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; - -// Wait until a component is present that is rendered in a limited number -// of microtask queue iterations. In particular, this does *not* wait for the -// event loop itself to progress. -async function waitForAsyncComponent(wrapper, Component, attempts = 10) { - let current = 0; - let result; - while (current++ < attempts) { - wrapper.update(); - result = wrapper.find(Component); - // Return immediately if we found something - if (result.length > 0 && result.exists()) { - return result; - } - await new Promise((r) => setTimeout(r)); // wait a microtask queue iteration - } - return result; -} + cleanup, + renderWithActiveConnection, + screen, + waitFor, +} from '@mongodb-js/compass-connections/test'; describe('CompassShellPlugin', function () { - const dummyConnectionInfo = { - id: '1', - connectionOptions: { - connectionString: 'mongodb://localhost:27017', - }, - }; - - const fakeDataService = { - getMongoClientConnectionOptions() {}, - } as any; - - const connectionsManager = new ConnectionsManager({ - logger: createNoopLogger().log.unbound, - }); - - sinon.replace(connectionsManager, 'getDataServiceForConnection', () => { - return fakeDataService; - }); - - let wrapper: ReactWrapper | null; - afterEach(() => { - wrapper?.unmount(); - wrapper = null; + cleanup(); }); - it('returns a renderable plugin', async function () { - connectionsManager['connectionStatuses'].set('1', 'connected'); - wrapper = mount( - - {/* global */} - - {/* local */} - - - - - - - - - - ); - - const component = await waitForAsyncComponent(wrapper, CompassShell); - - expect(component?.exists()).to.equal(true); + // TODO(COMPASS-7906): remove + it.skip('returns a renderable plugin', async function () { + await renderWithActiveConnection(, undefined, { + connectFn() { + return { + getMongoClientConnectionOptions() { + return { url: '', options: {} }; + }, + }; + }, + }); + + await waitFor(() => { + expect(screen.getByTestId('shell-section')).to.exist; + }); }); }); diff --git a/packages/compass-shell/src/plugin.tsx b/packages/compass-shell/src/plugin.tsx index 24400fe298b..4ba46dc2218 100644 --- a/packages/compass-shell/src/plugin.tsx +++ b/packages/compass-shell/src/plugin.tsx @@ -30,7 +30,7 @@ type ShellPluginProps = { export function ShellPlugin(props: ShellPluginProps) { const multiConnectionsEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const ShellComponent = multiConnectionsEnabled ? TabShell : Shell; return ( diff --git a/packages/compass-sidebar/package.json b/packages/compass-sidebar/package.json index f5b51d7877d..65fb0725efb 100644 --- a/packages/compass-sidebar/package.json +++ b/packages/compass-sidebar/package.json @@ -11,7 +11,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "5.38.0", + "version": "5.39.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,22 +48,22 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connection-import-export": "^0.34.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-connections-navigation": "^1.37.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-maybe-protect-connection-string": "^0.24.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-form": "^1.36.0", - "@mongodb-js/connection-info": "^0.5.3", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connection-import-export": "^0.35.0", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-connections-navigation": "^1.38.0", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-maybe-protect-connection-string": "^0.25.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-form": "^1.37.0", + "@mongodb-js/connection-info": "^0.6.0", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", "mongodb": "^6.8.0", - "mongodb-instance-model": "^12.23.3", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2", "react-redux": "^8.1.3", @@ -71,13 +71,11 @@ "redux-thunk": "^2.4.2" }, "devDependencies": { - "@mongodb-js/connection-storage": "^0.17.0", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.5.0", "@types/chai": "^4.2.21", "@types/chai-dom": "^0.0.10", @@ -90,7 +88,7 @@ "electron-mocha": "^12.2.0", "eslint": "^7.25.0", "mocha": "^10.2.0", - "mongodb-data-service": "^22.22.3", + "mongodb-data-service": "^22.23.0", "nyc": "^15.1.0", "prettier": "^2.7.1", "react-dom": "^17.0.2", diff --git a/packages/compass-sidebar/src/components/connection-info-modal.tsx b/packages/compass-sidebar/src/components/connection-info-modal.tsx index 8b8895bb716..dea39d69ac3 100644 --- a/packages/compass-sidebar/src/components/connection-info-modal.tsx +++ b/packages/compass-sidebar/src/components/connection-info-modal.tsx @@ -257,7 +257,7 @@ const mapStateToProps = ( return { infos: getInfos({ instance: instance, - databases: databases.databases ?? [], + databases: databases?.databases ?? [], connectionInfo: connectionInfo, connectionOptions: connectionOptions || {}, }), diff --git a/packages/compass-sidebar/src/components/db-stats.tsx b/packages/compass-sidebar/src/components/db-stats.tsx index 13825ed466e..d9bc31fbb37 100644 --- a/packages/compass-sidebar/src/components/db-stats.tsx +++ b/packages/compass-sidebar/src/components/db-stats.tsx @@ -86,7 +86,7 @@ const mapStateToProps = ( { connectionId }: { connectionId: string } ) => ({ refreshingStatus: state.instance[connectionId]?.refreshingStatus ?? 'initial', - databases: state.databases[connectionId].databases, + databases: state.databases[connectionId]?.databases ?? [], }); const MappedDBStats = connect(mapStateToProps, {})(DBStats); diff --git a/packages/compass-sidebar/src/components/legacy/navigation-items.spec.tsx b/packages/compass-sidebar/src/components/legacy/navigation-items.spec.tsx index fa889808165..182a91d6d45 100644 --- a/packages/compass-sidebar/src/components/legacy/navigation-items.spec.tsx +++ b/packages/compass-sidebar/src/components/legacy/navigation-items.spec.tsx @@ -42,7 +42,8 @@ function renderNavigationItems( const createDatabaseText = 'Create database'; const refreshCTAText = 'Refresh databases'; -describe('NavigationItems [Component]', function () { +// TODO(COMPASS-7906): remove +describe.skip('NavigationItems [Component]', function () { afterEach(cleanup); describe('when rendered', function () { diff --git a/packages/compass-sidebar/src/components/legacy/navigation-items.tsx b/packages/compass-sidebar/src/components/legacy/navigation-items.tsx index 48cba01c9ef..b6ee65ad57a 100644 --- a/packages/compass-sidebar/src/components/legacy/navigation-items.tsx +++ b/packages/compass-sidebar/src/components/legacy/navigation-items.tsx @@ -384,12 +384,11 @@ const mapStateToProps = ( }: { connectionInfo: ConnectionInfo; readOnly: boolean } ) => { const connectionId = connectionInfo.id; - const totalCollectionsCount = state.databases[connectionId].databases.reduce( - (acc: number, db: { collectionsLength: number }) => { - return acc + db.collectionsLength; - }, - 0 - ); + const totalCollectionsCount = ( + state.databases[connectionId]?.databases ?? [] + ).reduce((acc: number, db: { collectionsLength: number }) => { + return acc + db.collectionsLength; + }, 0); const isReady = ['ready', 'refreshing'].includes( diff --git a/packages/compass-sidebar/src/components/legacy/sidebar.tsx b/packages/compass-sidebar/src/components/legacy/sidebar.tsx index 7a253b198f9..c022e7f6273 100644 --- a/packages/compass-sidebar/src/components/legacy/sidebar.tsx +++ b/packages/compass-sidebar/src/components/legacy/sidebar.tsx @@ -10,7 +10,10 @@ import { useToast, } from '@mongodb-js/compass-components'; import { SaveConnectionModal } from '@mongodb-js/connection-form'; -import { useConnections } from '@mongodb-js/compass-connections/provider'; +import { + useConnectionInfo, + useConnections, +} from '@mongodb-js/compass-connections/provider'; import SidebarTitle from './sidebar-title'; import NavigationItems from './navigation-items'; @@ -58,7 +61,7 @@ const navigationItemsContainerStyles = css({ export function Sidebar({ showConnectionInfo = true, activeWorkspace, - initialConnectionInfo, + connectionInfo, setConnectionIsCSFLEEnabled, isGenuine, csfleMode, @@ -66,7 +69,7 @@ export function Sidebar({ }: { showConnectionInfo?: boolean; activeWorkspace: WorkspaceTab | null; - initialConnectionInfo: ConnectionInfo; + connectionInfo: ConnectionInfo; setConnectionIsCSFLEEnabled: (connectionId: string, enabled: boolean) => void; isGenuine?: boolean; csfleMode?: 'enabled' | 'disabled' | 'unavailable'; @@ -83,12 +86,12 @@ export function Sidebar({ setIsFavoriteModalVisible(false); return saveEditedConnection({ - ...cloneDeep(initialConnectionInfo), + ...cloneDeep(connectionInfo), favorite: newFavoriteInfo, savedConnectionType: 'favorite', }); }, - [initialConnectionInfo, saveEditedConnection] + [connectionInfo, saveEditedConnection] ); const { openToast } = useToast('compass-connections'); @@ -119,7 +122,7 @@ export function Sidebar({ if (action === 'copy-connection-string') { void copyConnectionString( maybeProtectConnectionString( - initialConnectionInfo.connectionOptions.connectionString ?? '' + connectionInfo.connectionOptions.connectionString ?? '' ) ); return; @@ -137,14 +140,14 @@ export function Sidebar({ if (action === 'open-create-database') { onSidebarAction(action, ...rest, { - connectionId: initialConnectionInfo.id, + connectionId: connectionInfo.id, }); return; } if (action === 'refresh-databases') { onSidebarAction(action, ...rest, { - connectionId: initialConnectionInfo.id, + connectionId: connectionInfo.id, }); return; } @@ -152,11 +155,11 @@ export function Sidebar({ onSidebarAction(action, ...rest); }, [ - initialConnectionInfo.id, + connectionInfo.id, onSidebarAction, openToast, maybeProtectConnectionString, - initialConnectionInfo.connectionOptions.connectionString, + connectionInfo.connectionOptions.connectionString, ] ); @@ -175,16 +178,16 @@ export function Sidebar({ {showConnectionInfo && (
{ - showNonGenuineMongoDBWarningModal(initialConnectionInfo.id); + showNonGenuineMongoDBWarningModal(connectionInfo.id); }} />
setIsFavoriteModalVisible(false)} onSaveClicked={(favoriteInfo) => onClickSaveFavorite(favoriteInfo)} @@ -214,11 +217,11 @@ export function Sidebar({ csfleMode={csfleMode} onClose={() => setIsCSFLEModalVisible(false)} setConnectionIsCSFLEEnabled={(enabled) => - setConnectionIsCSFLEEnabled(initialConnectionInfo.id, enabled) + setConnectionIsCSFLEEnabled(connectionInfo.id, enabled) } /> setIsConnectionInfoModalVisible(false)} /> @@ -230,15 +233,14 @@ export function Sidebar({ const mapStateToProps = ( state: RootState, { - initialConnectionInfo, + connectionInfo, }: { - initialConnectionInfo: Partial & Pick; + connectionInfo: ConnectionInfo; } ) => { return { - isGenuine: - state.instance[initialConnectionInfo.id]?.genuineMongoDB.isGenuine, - csfleMode: state.instance[initialConnectionInfo.id]?.csfleMode, + isGenuine: state.instance[connectionInfo.id]?.genuineMongoDB.isGenuine, + csfleMode: state.instance[connectionInfo.id]?.csfleMode, }; }; @@ -256,4 +258,11 @@ const MappedSidebar = connect(mapStateToProps, { onSidebarAction, })(Sidebar); -export default MappedSidebar; +export default function SidebarWithConnectionInfo( + props: Omit, 'connectionInfo'> +) { + const connectionInfo = useConnectionInfo(); + return ( + + ); +} diff --git a/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx b/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx index 2d93e64399a..d17cf0c13b5 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/connections-navigation.tsx @@ -30,8 +30,8 @@ import { } from '@mongodb-js/connection-info'; import type { RootState, SidebarThunkAction } from '../../modules'; import { - ConnectionStatus, type useConnectionsWithStatus, + ConnectionStatus, } from '@mongodb-js/compass-connections/provider'; import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider'; import { diff --git a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx index 6849ce80c82..ba8312262ef 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/sidebar.spec.tsx @@ -1,53 +1,26 @@ import React from 'react'; import { expect } from 'chai'; import sinon from 'sinon'; +import type { RenderWithConnectionsHookResult } from '@mongodb-js/compass-connections/test'; import { - render, + renderPluginComponentWithConnections, screen, cleanup, waitFor, within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; + userEvent, +} from '@mongodb-js/compass-connections/test'; import MultipleConnectionSidebar from './sidebar'; -import type { ConnectionInfo } from '@mongodb-js/connection-info'; -import { - ConfirmationModalArea, - ToastArea, -} from '@mongodb-js/compass-components'; -import { - InMemoryConnectionStorage, - ConnectionStorageProvider, - type ConnectionStorage, -} from '@mongodb-js/connection-storage/provider'; -import type { DataService } from 'mongodb-data-service'; -import { - ConnectionsManagerProvider, - ConnectionsManager, - ConnectionStatus, -} from '@mongodb-js/compass-connections/provider'; -import { createSidebarStore } from '../../stores'; -import { Provider } from 'react-redux'; -import AppRegistry, { createActivateHelpers } from 'hadron-app-registry'; -import { - type PreferencesAccess, - createSandboxFromDefaultPreferences, -} from 'compass-preferences-model'; -import { PreferencesProvider } from 'compass-preferences-model/provider'; -import { - type WorkspacesService, - WorkspacesServiceProvider, -} from '@mongodb-js/compass-workspaces/provider'; import type { WorkspaceTab } from '@mongodb-js/compass-workspaces'; import { WorkspacesProvider } from '@mongodb-js/compass-workspaces'; -import type { MongoDBInstance } from '@mongodb-js/compass-app-stores/provider'; -import { - type MongoDBInstancesManager, - TestMongoDBInstanceManager, -} from '@mongodb-js/compass-app-stores/provider'; +import type { WorkspacesService } from '@mongodb-js/compass-workspaces/provider'; +import { WorkspacesServiceProvider } from '@mongodb-js/compass-workspaces/provider'; +import type { MongoDBInstancesManager } from '@mongodb-js/compass-app-stores/provider'; +import { TestMongoDBInstanceManager } from '@mongodb-js/compass-app-stores/provider'; import { ConnectionImportExportProvider } from '@mongodb-js/compass-connection-import-export'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; -import { TelemetryProvider } from '@mongodb-js/compass-telemetry/provider'; +import { CompassSidebarPlugin } from '../../index'; +import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; +import type AppRegistry from '../../../../hadron-app-registry/dist'; const savedFavoriteConnection: ConnectionInfo = { id: '12345', @@ -69,116 +42,118 @@ const savedRecentConnection: ConnectionInfo = { }; describe('Multiple Connections Sidebar Component', function () { - let preferences: PreferencesAccess; - - const globalAppRegistry = new AppRegistry(); - const emitSpy = sinon.spy(globalAppRegistry, 'emit'); - const activeWorkspace = { - type: 'Databases', - connectionId: savedFavoriteConnection.id, - } as WorkspaceTab; - const workspaceService: WorkspacesService = { - openCollectionsWorkspace: sinon.stub(), - openCollectionWorkspace: sinon.stub(), - openCollectionWorkspaceSubtab: sinon.stub(), - openDatabasesWorkspace: sinon.stub(), - openEditViewWorkspace: sinon.stub(), - openMyQueriesWorkspace: sinon.stub(), - openPerformanceWorkspace: sinon.stub(), - openShellWorkspace: sinon.stub(), - getActiveWorkspace() { - return activeWorkspace; - }, - }; - - let connectionStorage: ConnectionStorage; - let connectionsManager: ConnectionsManager; - let instancesManager: MongoDBInstancesManager; - let store: ReturnType['store']; - let deactivate: () => void; - let connectFn: sinon.SinonStub; + let appRegistry: sinon.SinonSpiedInstance; let track: sinon.SinonStub; + let connectionsStoreActions: sinon.SinonSpiedInstance< + RenderWithConnectionsHookResult['connectionsStore']['actions'] + >; + let workspace: sinon.SinonSpiedInstance; + let instancesManager: MongoDBInstancesManager; function doRender( activeWorkspace: WorkspaceTab | null = null, - { wrapper }: Parameters['1'] = {} + connections: ConnectionInfo[] = [savedFavoriteConnection] ) { - return render( - - - - - - null }]} - > - - - - - - - - - - - - - , - { wrapper } - ); - } - - beforeEach(async function () { - connectFn = sinon.stub(); - track = sinon.stub(); - instancesManager = new TestMongoDBInstanceManager(); - connectionsManager = new ConnectionsManager({ - logger: createNoopLogger().log.unbound, - __TEST_CONNECT_FN: connectFn, - }); - connectionStorage = new InMemoryConnectionStorage([ - savedFavoriteConnection, - savedRecentConnection, - ]); - preferences = await createSandboxFromDefaultPreferences(); - await preferences.savePreferences({ - enableNewMultipleConnectionSystem: true, + workspace = sinon.spy({ + openMyQueriesWorkspace: () => undefined, + openShellWorkspace: () => undefined, + openPerformanceWorkspace: () => undefined, + }) as any; + instancesManager = new TestMongoDBInstanceManager({ + _id: '1', + status: 'ready', + genuineMongoDB: { + dbType: 'local', + isGenuine: true, + }, + build: { + isEnterprise: true, + version: '7.0.1', + }, + dataLake: { + isDataLake: false, + version: '', + }, + topologyDescription: { + servers: [], + setName: '', + type: 'LoadBalanced', + }, + databasesStatus: 'ready', + databases: [ + { + _id: 'db_ready', + name: 'db_ready', + collectionsStatus: 'ready', + collections: [ + { + _id: 'coll_ready', + name: 'coll_ready', + type: 'collection', + }, + ], + }, + ] as any, }); - ({ store, deactivate } = createSidebarStore( + const result = renderPluginComponentWithConnections( + + null }, + { name: 'Performance', component: () => null }, + ]} + > + + + + + , + CompassSidebarPlugin.withMockServices({ instancesManager }), + {}, { - globalAppRegistry, - instancesManager, - logger: createNoopLogger(), - connectionsManager, - }, - createActivateHelpers() - )); - }); + preferences: { enableMultipleConnectionSystem: true }, + connections, + connectFn() { + return { + currentOp() { + return {}; + }, + top() { + return {}; + }, + getConnectionOptions() { + return {}; + }, + } as any; + }, + } + ); + track = result.track; + appRegistry = sinon.spy(result.globalAppRegistry); + connectionsStoreActions = sinon.spy(result.connectionsStore.actions); + return result; + } afterEach(function () { - deactivate(); cleanup(); sinon.restore(); }); describe('top level general navigation', function () { beforeEach(function () { - doRender(); + // These tests expect only one connection on the screen + doRender(undefined, [savedFavoriteConnection]); }); + it('should have settings button and it emits open-compass-settings on click', () => { const settingsBtn = screen.getByTitle('Compass Settings'); expect(settingsBtn).to.be.visible; userEvent.click(settingsBtn); - expect(emitSpy).to.have.been.calledWith('open-compass-settings'); + expect(appRegistry.emit).to.have.been.calledWith('open-compass-settings'); }); it('should have "My Queries" navigation item and it should the workspace on click', () => { @@ -187,7 +162,7 @@ describe('Multiple Connections Sidebar Component', function () { userEvent.click(navItem); - expect(workspaceService.openMyQueriesWorkspace).to.have.been.called; + expect(workspace.openMyQueriesWorkspace).to.have.been.called; }); it('should have a connections list with connection related actions', function () { @@ -201,33 +176,21 @@ describe('Multiple Connections Sidebar Component', function () { const addNewConnectionsBtn = screen.getByLabelText('Add new connection'); expect(addNewConnectionsBtn).to.be.visible; - - // import export connections actions behind Show actions are not visible - // because there is no provider - expect(() => screen.getByLabelText('Show actions')).to.throw; }); - context( - 'when there is ConnectionImportExportProvider available', - function () { - it('should show connection import export action in connection list header', function () { - cleanup(); - doRender(null, { wrapper: ConnectionImportExportProvider }); - expect(screen.getByLabelText('Show actions')).to.be.visible; - - userEvent.click(screen.getByLabelText('Show actions')); - expect(screen.getByText('Import connections')).to.be.visible; - expect(screen.getByText('Export connections')).to.be.visible; - }); - } - ); + it('should show connection import export action in connection list header', function () { + expect(screen.getByLabelText('Show actions')).to.be.visible; + + userEvent.click(screen.getByLabelText('Show actions')); + expect(screen.getByText('Import connections')).to.be.visible; + expect(screen.getByText('Export connections')).to.be.visible; + }); }); describe('connections list', function () { context('when there are no connections', function () { it('should display an empty state with a CTA to add new connection', function () { - connectionStorage = new InMemoryConnectionStorage([]); - doRender(); + doRender(undefined, []); expect(() => screen.getByRole('tree')).to.throw; @@ -248,24 +211,30 @@ describe('Multiple Connections Sidebar Component', function () { }); context('when there are some connections', function () { - const storedConnections: ConnectionInfo[] = [ - savedFavoriteConnection, - savedRecentConnection, - ]; - async function renderWithConnections( - connections: ConnectionInfo[] = storedConnections, - activeWorkspaceTabs: WorkspaceTab | null = null - ) { - cleanup(); - connectionStorage = new InMemoryConnectionStorage(connections); - doRender(activeWorkspaceTabs); - return await waitFor(() => screen.getByRole('tree')); - } + const renderAndWaitForNavigationTree = async ( + ...[activeTab, connections]: Parameters + ) => { + const result = doRender( + activeTab, + connections ?? [savedFavoriteConnection, savedRecentConnection] + ); + await waitFor(() => screen.getByRole('tree')); + return result; + }; + + const connectAndNotifyInstanceManager = async ( + connectionInfo: ConnectionInfo + ) => { + await connectionsStoreActions.connect(connectionInfo); + instancesManager.emit( + 'instance-started', + connectionInfo.id, + instancesManager.getMongoDBInstanceForConnection(connectionInfo.id) + ); + }; it('should not render the empty CTA and instead render the connections tree', async function () { - const navigationTree = await renderWithConnections(); - expect(navigationTree).to.be.visible; - + await renderAndWaitForNavigationTree(undefined); const connectionNavigationItems = screen.getAllByRole('treeitem'); expect(connectionNavigationItems).to.have.lengthOf(2); expect(connectionNavigationItems[0]).to.have.attribute( @@ -280,7 +249,7 @@ describe('Multiple Connections Sidebar Component', function () { context('on hover of connection navigation item', function () { it('should display actions for favorite item', async function () { - await renderWithConnections(); + await renderAndWaitForNavigationTree(); const favoriteItem = screen.getByTestId(savedFavoriteConnection.id); expect(favoriteItem).to.be.visible; @@ -311,7 +280,7 @@ describe('Multiple Connections Sidebar Component', function () { }); it('should display actions for non-favorite item', async function () { - await renderWithConnections(); + await renderAndWaitForNavigationTree(); const nonFavoriteItem = screen.getByTestId(savedRecentConnection.id); expect(nonFavoriteItem).to.be.visible; @@ -343,98 +312,14 @@ describe('Multiple Connections Sidebar Component', function () { }); context('when connected', function () { - const connectedInstance: MongoDBInstance = { - _id: '1', - status: 'ready', - genuineMongoDB: { - dbType: 'local', - isGenuine: true, - }, - build: { - isEnterprise: true, - version: '7.0.1', - }, - dataLake: { - isDataLake: false, - version: '', - }, - topologyDescription: { - servers: [], - setName: '', - type: 'standalone', - }, - isWritable: true, - env: '', - isAtlas: false, - isLocalAtlas: false, - databasesStatus: 'ready', - databases: [ - { - _id: 'db_ready', - name: 'db_ready', - collectionsLength: 1, - collectionsStatus: 'ready', - collections: [ - { - _id: 'coll_ready', - name: 'coll_ready', - type: 'collection', - }, - ], - }, - ] as any, - refresh: () => Promise.resolve(), - fetch: () => Promise.resolve(), - fetchDatabases: () => Promise.resolve(), - getNamespace: () => Promise.resolve(null), - on: () => {}, - removeListener: () => {}, - } as any; - - const connectedDataService: DataService = { - id: 1, - getConnectionOptions: () => { - return { - ...savedFavoriteConnection.connectionOptions, - }; - }, - currentOp: () => Promise.resolve({} as any), - top: () => Promise.resolve({} as any), - disconnect: () => {}, - } as any; - - const instances = new Map(); - - beforeEach(function () { - instances.set(savedFavoriteConnection.id, connectedInstance); - sinon - .stub(instancesManager, 'listMongoDBInstances') - .returns(instances); - sinon - .stub(connectionsManager, 'getDataServiceForConnection') - .returns(connectedDataService); - sinon.stub(connectionsManager, 'statusOf').callsFake((id) => { - if (id === savedFavoriteConnection.id) { - return ConnectionStatus.Connected; - } - return ConnectionStatus.Disconnected; - }); - ({ store, deactivate } = createSidebarStore( - { - globalAppRegistry, - instancesManager, - logger: createNoopLogger(), - connectionsManager, - }, - createActivateHelpers() - )); - }); - it('should render the connected connections expanded', async () => { - await renderWithConnections(storedConnections, { + await renderAndWaitForNavigationTree({ + id: '123', type: 'Databases', connectionId: savedFavoriteConnection.id, - } as WorkspaceTab); + }); + + await connectAndNotifyInstanceManager(savedFavoriteConnection); const connectedItem = screen.getByTestId(savedFavoriteConnection.id); expect(connectedItem).to.exist; @@ -443,17 +328,22 @@ describe('Multiple Connections Sidebar Component', function () { }); it('should display actions for connected item', async () => { - await renderWithConnections(storedConnections); + await renderAndWaitForNavigationTree(); + + await connectAndNotifyInstanceManager(savedFavoriteConnection); const connectedItem = screen.getByTestId(savedFavoriteConnection.id); + userEvent.hover( within(connectedItem).getByTestId('base-navigation-item') ); - expect(screen.getByLabelText('Create database')).to.be.visible; - expect(screen.getByLabelText('Open MongoDB shell')).to.be.visible; + expect(within(connectedItem).getByLabelText('Open MongoDB shell')).to + .be.visible; + expect(within(connectedItem).getByLabelText('Create database')).to.be + .visible; - userEvent.click(screen.getByLabelText('Show actions')); + userEvent.click(within(connectedItem).getByLabelText('Show actions')); expect(screen.getByText('View performance metrics')).to.be.visible; expect(screen.getByText('Show connection info')).to.be.visible; @@ -468,18 +358,15 @@ describe('Multiple Connections Sidebar Component', function () { context('and performing actions', function () { beforeEach(async function () { - await renderWithConnections(storedConnections, { + await renderAndWaitForNavigationTree({ + id: '1234', type: 'Databases', connectionId: savedFavoriteConnection.id, - } as WorkspaceTab); + }); + await connectAndNotifyInstanceManager(savedFavoriteConnection); }); - it('should open create database modal when clicked on create database action', async function () { - const emitSpy = sinon.stub(globalAppRegistry, 'emit'); - await renderWithConnections(storedConnections, { - type: 'Databases', - connectionId: savedFavoriteConnection.id, - } as WorkspaceTab); + it('should open create database modal when clicked on create database action', function () { const connectionItem = screen.getByTestId( savedFavoriteConnection.id ); @@ -491,9 +378,12 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByLabelText('Create database') ); - expect(emitSpy).to.have.been.calledWith('open-create-database', { - connectionId: savedFavoriteConnection.id, - }); + expect(appRegistry.emit).to.have.been.calledWith( + 'open-create-database', + { + connectionId: savedFavoriteConnection.id, + } + ); }); it('should open shell workspace when clicked on open shell action', async function () { @@ -506,7 +396,7 @@ describe('Multiple Connections Sidebar Component', function () { userEvent.click(screen.getByLabelText('Open MongoDB shell')); - expect(workspaceService.openShellWorkspace).to.have.been.calledWith( + expect(workspace.openShellWorkspace).to.have.been.calledWith( savedFavoriteConnection.id, { newTab: true } ); @@ -524,13 +414,15 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByTestId('base-navigation-item') ); - userEvent.click(screen.getByLabelText('Show actions')); + userEvent.click( + within(connectionItem).getByLabelText('Show actions') + ); userEvent.click(screen.getByText('View performance metrics')); - expect( - workspaceService.openPerformanceWorkspace - ).to.have.been.calledWith(savedFavoriteConnection.id); + expect(workspace.openPerformanceWorkspace).to.have.been.calledWith( + savedFavoriteConnection.id + ); }); it('should open connection info modal when clicked on show connection info action', function () { @@ -541,19 +433,16 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByTestId('base-navigation-item') ); - userEvent.click(screen.getByLabelText('Show actions')); + userEvent.click( + within(connectionItem).getByLabelText('Show actions') + ); userEvent.click(screen.getByText('Show connection info')); expect(screen.getByTestId('connection-info-modal')).to.be.visible; }); - it('should disconnect when clicked on disconnect action', async function () { - const disconnectSpy = sinon.spy( - connectionsManager, - 'closeConnection' - ); - await renderWithConnections(); + it('should disconnect when clicked on disconnect action', function () { const connectionItem = screen.getByTestId( savedFavoriteConnection.id ); @@ -561,16 +450,16 @@ describe('Multiple Connections Sidebar Component', function () { within(connectionItem).getByTestId('base-navigation-item') ); - userEvent.click(screen.getByLabelText('Show actions')); + userEvent.click( + within(connectionItem).getByLabelText('Show actions') + ); userEvent.click(screen.getByText('Disconnect')); - expect(disconnectSpy).to.be.calledWith(savedFavoriteConnection.id); + expect(connectionsStoreActions.disconnect).to.have.been.called; }); it('should connect when the user tries to expand an inactive connection', async function () { - const connectSpy = sinon.spy(connectionsManager, 'connect'); - await renderWithConnections(); const connectionItem = screen.getByTestId(savedRecentConnection.id); userEvent.click( @@ -578,7 +467,9 @@ describe('Multiple Connections Sidebar Component', function () { ); await waitFor(() => { - expect(connectSpy).to.be.calledWith(savedRecentConnection); + expect(connectionsStoreActions.connect).to.be.calledWith( + savedRecentConnection + ); }); }); @@ -619,8 +510,6 @@ describe('Multiple Connections Sidebar Component', function () { }); it('should unfavorite/favorite connection when clicked on favorite/unfavorite action', async function () { - const saveSpy = sinon.spy(connectionStorage, 'save'); - const connectionItem = screen.getByTestId( savedFavoriteConnection.id ); @@ -635,18 +524,13 @@ describe('Multiple Connections Sidebar Component', function () { userEvent.click(screen.getByText('Unfavorite')); await waitFor(() => { - expect(saveSpy).to.have.been.calledWithExactly({ - connectionInfo: { - ...savedFavoriteConnection, - savedConnectionType: 'recent', - }, - }); + expect( + connectionsStoreActions.toggleFavoritedConnectionStatus + ).to.have.been.calledWithExactly(savedFavoriteConnection.id); }); }); - it('should open a connection form when clicked on duplicate action', async function () { - const saveSpy = sinon.spy(connectionStorage, 'save'); - + it('should open a connection form when clicked on duplicate action', function () { const connectionItem = screen.getByTestId( savedFavoriteConnection.id ); @@ -660,11 +544,6 @@ describe('Multiple Connections Sidebar Component', function () { userEvent.click(screen.getByText('Duplicate')); - // Does not save the duplicate yet - await waitFor(() => { - expect(saveSpy).not.to.have.been.called; - }); - // We see the connect button in the form modal expect(screen.getByTestId('connect-button')).to.be.visible; @@ -675,12 +554,6 @@ describe('Multiple Connections Sidebar Component', function () { }); it('should disconnect and remove the connection when clicked on remove action', async function () { - const closeConnectionSpy = sinon.spy( - connectionsManager, - 'closeConnection' - ); - const deleteSpy = sinon.spy(connectionStorage, 'delete'); - const connectionItem = screen.getByTestId( savedFavoriteConnection.id ); @@ -695,15 +568,9 @@ describe('Multiple Connections Sidebar Component', function () { userEvent.click(screen.getByText('Remove')); await waitFor(() => { - expect(closeConnectionSpy).to.have.been.calledWithExactly( - savedFavoriteConnection.id - ); - }); - - await waitFor(() => { - expect(deleteSpy).to.have.been.calledWithExactly({ - ...savedFavoriteConnection, - }); + expect( + connectionsStoreActions.removeConnection + ).to.have.been.calledWithExactly(savedFavoriteConnection.id); }); await waitFor(() => { diff --git a/packages/compass-sidebar/src/components/multiple-connections/sidebar.tsx b/packages/compass-sidebar/src/components/multiple-connections/sidebar.tsx index dae57bd2720..7e7f83baf12 100644 --- a/packages/compass-sidebar/src/components/multiple-connections/sidebar.tsx +++ b/packages/compass-sidebar/src/components/multiple-connections/sidebar.tsx @@ -26,6 +26,7 @@ import CSFLEConnectionModal, { type CSFLEConnectionModalProps, } from '../csfle-connection-modal'; import { setConnectionIsCSFLEEnabled } from '../../modules/data-service'; +import { useGlobalAppRegistry } from 'hadron-app-registry'; const TOAST_TIMEOUT_MS = 5000; // 5 seconds. type MappedCsfleModalProps = { @@ -171,6 +172,12 @@ export function MultipleConnectionSidebar({ [csfleModalConnectionId, onConnectionCsfleModeChanged] ); + const globalAppRegistry = useGlobalAppRegistry(); + const openSettingsModal = useCallback( + (tab?: string) => globalAppRegistry.emit('open-compass-settings', tab), + [globalAppRegistry] + ); + return (
diff --git a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx index 183d84008df..f88616d19b9 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-default.spec.tsx @@ -172,6 +172,7 @@ describe('AuthenticationDefault Component', function () { { fieldName: 'username', message: 'username error', + fieldTab: 'general', }, ], updateConnectionFormField: updateConnectionFormFieldSpy, @@ -186,6 +187,7 @@ describe('AuthenticationDefault Component', function () { { fieldName: 'password', message: 'password error', + fieldTab: 'general', }, ], updateConnectionFormField: updateConnectionFormFieldSpy, diff --git a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx index ffbe3284c12..411cd2ff9c6 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-gssapi.spec.tsx @@ -118,7 +118,7 @@ describe('AuthenticationGssapi Component', function () { .getByTestId('gssapi-canonicalize-host-name-none') .closest('input'); - expect(radio.checked).to.be.true; + expect(radio?.checked).to.be.true; }); it('updates the form field with CANONICALIZE_HOST_NAME forward', function () { @@ -188,7 +188,7 @@ describe('AuthenticationGssapi Component', function () { it('allows to edit the password when enter password directly is enabled', function () { expect(screen.queryByTestId('gssapi-password-input')).to.not.exist; const checkbox = screen.getByTestId('gssapi-password-checkbox'); - expect(checkbox.closest('input').checked).to.be.false; + expect(checkbox.closest('input')?.checked).to.be.false; fireEvent.click(checkbox); @@ -220,15 +220,17 @@ describe('AuthenticationGssapi Component', function () { it('enables the checkbox and shows the password input', function () { const checkbox = screen.getByTestId('gssapi-password-checkbox'); - expect(checkbox.closest('input').checked).to.be.true; + expect(checkbox.closest('input')?.checked).to.be.true; const passwordInput = screen.queryByTestId('gssapi-password-input'); expect(passwordInput).to.exist; - expect(passwordInput.closest('input').value).to.equal('password'); + expect(passwordInput && passwordInput.closest('input')?.value).to.equal( + 'password' + ); }); it('resets the password when the checkbox is unchecked', function () { const checkbox = screen.getByTestId('gssapi-password-checkbox'); - expect(checkbox.closest('input').checked).to.be.true; + expect(checkbox.closest('input')?.checked).to.be.true; fireEvent.click(checkbox); expect(updateConnectionFormFieldSpy.callCount).to.equal(1); diff --git a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.spec.tsx index 5117c361731..4564f4d640d 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.spec.tsx @@ -15,7 +15,9 @@ import ConnectionForm from '../../../'; const deviceAuthFlowText = 'Enable Device Authentication Flow'; async function renderConnectionForm( - connectSpy, + connectSpy: ( + expected: ConnectionOptions | ((expected: ConnectionOptions) => void) + ) => Promise, { showOIDCDeviceAuthFlow }: { showOIDCDeviceAuthFlow: boolean } ) { render( @@ -26,10 +28,13 @@ async function renderConnectionForm( connectionString: 'mongodb://localhost:27017', }, }} - onConnectClicked={(connectionInfo) => { - connectSpy(connectionInfo.connectionOptions); + onSaveAndConnectClicked={(connectionInfo) => { + void connectSpy(connectionInfo.connectionOptions); }} preferences={{ enableOidc: true, showOIDCDeviceAuthFlow }} + onSaveClicked={() => { + return Promise.resolve(); + }} /> ); @@ -57,19 +62,22 @@ const openOptionsAccordion = () => fireEvent.click(screen.getByText('OIDC Options')); describe('Authentication OIDC Connection Form', function () { - let expectToConnectWith; + let expectToConnectWith: ( + expected: ConnectionOptions | ((expected: ConnectionOptions) => void) + ) => Promise; let connectSpy: sinon.SinonSpy; beforeEach(function () { connectSpy = sinon.spy(); expectToConnectWith = async ( - expected: ConnectionOptions | ((ConnectionOptions) => void) + expected: ConnectionOptions | ((expected: ConnectionOptions) => void) ): Promise => { connectSpy.resetHistory(); fireEvent.click(screen.getByTestId('connect-button')); try { await waitFor(() => expect(connectSpy).to.have.been.calledOnce); } catch (e) { + // this only finds something if it is a validation error const errors = screen.getByTestId( 'connection-error-summary' ).textContent; @@ -94,7 +102,7 @@ describe('Authentication OIDC Connection Form', function () { }); it('handles principal (username) changes', async function () { - fireEvent.change(screen.getAllByRole('textbox')[1], { + fireEvent.change(screen.getByTestId('connection-oidc-username-input'), { target: { value: 'goodSandwich' }, }); @@ -105,9 +113,12 @@ describe('Authentication OIDC Connection Form', function () { }); it('handles the auth redirect flow uri changes', async function () { - fireEvent.change(screen.getAllByRole('textbox')[2], { - target: { value: 'goodSandwiches' }, - }); + fireEvent.change( + screen.getByTestId('connection-oidc-auth-code-flow-redirect-uri-input'), + { + target: { value: 'goodSandwiches' }, + } + ); await expectToConnectWith({ connectionString: @@ -118,6 +129,27 @@ describe('Authentication OIDC Connection Form', function () { }); }); + it('handles the Use ID token instead of Access Token checkbox', async function () { + fireEvent.click(screen.getByText('Use ID token instead of Access Token')); + await expectToConnectWith({ + connectionString: + 'mongodb://localhost:27017/?authMechanism=MONGODB-OIDC&authSource=%24external', + oidc: { + passIdTokenAsAccessToken: true, + }, + }); + }); + + it('handles the Use ID token instead of Access Token checkbox on and off', async function () { + fireEvent.click(screen.getByText('Use ID token instead of Access Token')); + fireEvent.click(screen.getByText('Use ID token instead of Access Token')); + await expectToConnectWith({ + connectionString: + 'mongodb://localhost:27017/?authMechanism=MONGODB-OIDC&authSource=%24external', + oidc: {}, + }); + }); + it('handles the Consider Target Endpoint Trusted checkbox', async function () { fireEvent.click(screen.getByText('Consider Target Endpoint Trusted')); await expectToConnectWith({ diff --git a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx index 9ac91596774..97a805c29fd 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/authentication-tab/authentication-oidc.tsx @@ -5,6 +5,7 @@ import { Description, FormFieldContainer, Label, + Link, TextInput, } from '@mongodb-js/compass-components'; import type ConnectionStringUrl from 'mongodb-connection-string-url'; @@ -24,11 +25,13 @@ function AuthenticationOIDC({ updateConnectionFormField, errors, connectionOptions, + openSettingsModal, }: { connectionStringUrl: ConnectionStringUrl; errors: ConnectionFormError[]; updateConnectionFormField: UpdateConnectionFormField; connectionOptions: ConnectionOptions; + openSettingsModal?: (tab?: string) => void; }): React.ReactElement { const username = getConnectionStringUsername(connectionStringUrl); const usernameError = errorMessageByFieldName(errors, 'username'); @@ -51,6 +54,12 @@ function AuthenticationOIDC({ 'showOIDCDeviceAuthFlow' ); + const openProxySettings = useCallback( + () => openSettingsModal?.('proxy'), + [openSettingsModal] + ); + const showProxySettings = + useConnectionFormPreference('showProxySettings') && openSettingsModal; return ( <> @@ -72,7 +81,7 @@ function AuthenticationOIDC({ /> - + + + ) => { + if (checked) { + return handleFieldChanged('passIdTokenAsAccessToken', true); + } + + return handleFieldChanged( + 'passIdTokenAsAccessToken', + undefined + ); + }} + data-testid="oidc-pass-id-token-as-access-token" + id="oidc-pass-id-token-as-access-token" + label={ + <> + + + Use ID tokens instead of access tokens to work around + misconfigured or broken identity providers. This will only + work if the server is configured correspondingly. + + + } + checked={!!connectionOptions.oidc?.passIdTokenAsAccessToken} + /> + + + {showProxySettings && ( + + ) => { + return handleFieldChanged( + 'shareProxyWithConnection', + !checked + ); + }} + data-testid="oidc-use-application-level-proxy" + id="oidc-use-application-level-proxy" + label={ + <> + + + Use the{' '} + + application-level proxy settings + {' '} + for communicating with the identity provider. If not + chosen, the same proxy (if any) is used for connecting to + both the cluster and the identity provider. + + + } + checked={!connectionOptions.oidc?.shareProxyWithConnection} + /> + + )} + {showOIDCDeviceAuthFlow && ( void; }>; } @@ -90,11 +91,13 @@ function AuthenticationTab({ updateConnectionFormField, connectionStringUrl, connectionOptions, + openSettingsModal, }: { errors: ConnectionFormError[]; connectionStringUrl: ConnectionStringUrl; updateConnectionFormField: UpdateConnectionFormField; connectionOptions: ConnectionOptions; + openSettingsModal?: (tab?: string) => void; }): React.ReactElement { // enableOIDC is the feature flag, showOIDC is the connection form preference. const enableOIDC = !!useConnectionFormPreference('enableOidc'); @@ -173,6 +176,7 @@ function AuthenticationTab({ connectionStringUrl={connectionStringUrl} updateConnectionFormField={updateConnectionFormField} connectionOptions={connectionOptions} + openSettingsModal={openSettingsModal} /> diff --git a/packages/connection-form/src/components/advanced-options-tabs/csfle-tab/csfle-tab.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/csfle-tab/csfle-tab.spec.tsx index 589f2fa46ca..d9dba077a74 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/csfle-tab/csfle-tab.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/csfle-tab/csfle-tab.spec.tsx @@ -44,14 +44,16 @@ const setFileInputValue = (testId: string, value: string) => }); describe('In-Use Encryption', function () { - let expectToConnectWith; - let expectConnectionError; + let expectToConnectWith: ( + expected: ConnectionOptions | ((opts: ConnectionOptions) => void) + ) => Promise; + let expectConnectionError: (expectedErrorText: string) => Promise; beforeEach(async function () { const connectSpy = sinon.spy(); expectToConnectWith = async ( - expected: ConnectionOptions | ((ConnectionOptions) => void) + expected: ConnectionOptions | ((opts: ConnectionOptions) => void) ): Promise => { connectSpy.resetHistory(); fireEvent.click(screen.getByTestId('connect-button')); @@ -87,9 +89,12 @@ describe('In-Use Encryption', function () { connectionString: 'mongodb://localhost:27017', }, }} - onConnectClicked={(connectionInfo) => { + onSaveAndConnectClicked={(connectionInfo) => { connectSpy(connectionInfo.connectionOptions); }} + onSaveClicked={() => { + return Promise.resolve(); + }} /> ); @@ -192,6 +197,10 @@ describe('In-Use Encryption', function () { .getByTestId('csfle-kms-local-key') .closest('input')?.value; + if (!generatedLocalKey) { + throw new Error('expected generatedLocalKey'); + } + expect(generatedLocalKey).to.match(/^[a-zA-Z0-9+/-_=]{128}$/); await expectToConnectWith({ diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/app-proxy.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/app-proxy.tsx new file mode 100644 index 00000000000..91b38587dae --- /dev/null +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/app-proxy.tsx @@ -0,0 +1,22 @@ +import { Body, Link } from '@mongodb-js/compass-components'; +import React, { useCallback } from 'react'; + +export function AppProxy({ + openSettingsModal, +}: { + openSettingsModal?: (tab?: string) => void; +}): React.ReactElement { + const openProxySettings = useCallback(() => { + openSettingsModal?.('proxy'); + }, [openSettingsModal]); + + if (!openSettingsModal) return <>; + + return ( + + Use the{' '} + application-level proxy settings{' '} + for communicating with the cluster. + + ); +} diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.spec.tsx index 6430306ee0e..961aa0a863d 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.spec.tsx @@ -184,13 +184,13 @@ describe('SSHTunnelTab', function () { fireEvent.click(screen.getByTestId(`${tab}-tab-button`)); expect(updateConnectionFormFieldSpy).to.have.been.called; expect(updateConnectionFormFieldSpy.args[0][0]).to.deep.equal({ - type: 'remove-proxy-options', + type: 'remove-proxy-options-and-app-proxy', }); }); }); // eslint-disable-next-line mocha/no-setup-in-describe - ['none', 'socks'].forEach((tab) => { + for (const tab of ['none', 'socks', 'app-proxy']) { it(`removes sshTunnel when user clicks ${tab} tab`, function () { renderWithOptionsAndUrl( { @@ -207,10 +207,13 @@ describe('SSHTunnelTab', function () { fireEvent.click(screen.getByTestId(`${tab}-tab-button`)); expect(updateConnectionFormFieldSpy).to.have.been.called; expect(updateConnectionFormFieldSpy.args[0][0]).to.deep.equal({ - type: 'remove-ssh-options', + type: + tab === 'app-proxy' + ? 'remove-proxy-options-and-set-app-proxy' + : 'remove-ssh-options-and-app-proxy', }); }); - }); + } it('removes proxyOptions when user navigates from socks tab to none', function () { connectionStringUrl.searchParams.set('proxyHost', 'hello'); @@ -222,7 +225,7 @@ describe('SSHTunnelTab', function () { fireEvent.click(screen.getByTestId('none-tab-button')); expect(updateConnectionFormFieldSpy).to.have.been.calledOnce; expect(updateConnectionFormFieldSpy.args[0][0]).to.deep.equal({ - type: 'remove-proxy-options', + type: 'remove-proxy-options-and-app-proxy', }); }); @@ -242,7 +245,7 @@ describe('SSHTunnelTab', function () { fireEvent.click(screen.getByTestId('none-tab-button')); expect(updateConnectionFormFieldSpy).to.have.been.calledOnce; expect(updateConnectionFormFieldSpy.args[0][0]).to.deep.equal({ - type: 'remove-ssh-options', + type: 'remove-ssh-options-and-app-proxy', }); }); @@ -262,7 +265,7 @@ describe('SSHTunnelTab', function () { fireEvent.click(screen.getByTestId('none-tab-button')); expect(updateConnectionFormFieldSpy).to.have.been.calledOnce; expect(updateConnectionFormFieldSpy.args[0][0]).to.deep.equal({ - type: 'remove-ssh-options', + type: 'remove-ssh-options-and-app-proxy', }); }); }); diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.tsx index fe00ee1ebae..62ea137f0fa 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/proxy-and-ssh-tunnel-tab.tsx @@ -20,7 +20,9 @@ import type { import SshTunnelIdentity from './ssh-tunnel-identity'; import SshTunnelPassword from './ssh-tunnel-password'; import Socks from './socks'; +import { AppProxy } from './app-proxy'; import type { ConnectionFormError } from '../../../utils/validation'; +import { useConnectionFormPreference } from '../../../hooks/use-connect-form-preferences'; interface TabOption { id: string; @@ -31,10 +33,11 @@ interface TabOption { updateConnectionFormField: UpdateConnectionFormField; connectionStringUrl: ConnectionStringUrl; errors: ConnectionFormError[]; + openSettingsModal?: (tab?: string) => void; }>; } -const options: TabOption[] = [ +const tabOptions: TabOption[] = [ { title: 'None', id: 'none', @@ -83,6 +86,10 @@ const getSelectedTunnelType = ( return 'socks'; } + if (connectionOptions?.useApplicationLevelProxy) { + return 'app-proxy'; + } + if ( !connectionOptions || !connectionOptions?.sshTunnel || @@ -106,17 +113,30 @@ function ProxyAndSshTunnelTab({ updateConnectionFormField, errors, connectionStringUrl, + openSettingsModal, }: { errors: ConnectionFormError[]; connectionStringUrl: ConnectionStringUrl; updateConnectionFormField: UpdateConnectionFormField; connectionOptions?: ConnectionOptions; + openSettingsModal?: (tab?: string) => void; }): React.ReactElement { const selectedTunnelType: TunnelType = getSelectedTunnelType( connectionStringUrl, connectionOptions ); + const options = [...tabOptions]; + const showProxySettings = useConnectionFormPreference('showProxySettings'); + if (showProxySettings) { + options.push({ + title: 'Application-level Proxy', + id: 'app-proxy', + type: 'app-proxy', + component: AppProxy, + }); + } + const selectedOptionIndex = options.findIndex((x) => x.type === selectedTunnelType) ?? 0; const [selectedOption, setSelectedOption] = useState( @@ -125,18 +145,30 @@ function ProxyAndSshTunnelTab({ const handleOptionChanged = useCallback( (oldType: TunnelType, newType: TunnelType) => { - let type: 'remove-proxy-options' | 'remove-ssh-options'; + let type: + | 'remove-proxy-options-and-app-proxy' + | 'remove-ssh-options-and-app-proxy' + | 'remove-proxy-options-and-set-app-proxy' + | 'remove-app-proxy'; switch (newType) { case 'socks': - type = 'remove-ssh-options'; + type = 'remove-ssh-options-and-app-proxy'; break; case 'ssh-identity': case 'ssh-password': - type = 'remove-proxy-options'; + type = 'remove-proxy-options-and-app-proxy'; + break; + case 'app-proxy': + type = 'remove-proxy-options-and-set-app-proxy'; break; + case 'none': default: type = - oldType === 'socks' ? 'remove-proxy-options' : 'remove-ssh-options'; + oldType === 'socks' + ? 'remove-proxy-options-and-app-proxy' + : oldType === 'app-proxy' + ? 'remove-app-proxy' + : 'remove-ssh-options-and-app-proxy'; break; } updateConnectionFormField({ type }); @@ -195,6 +227,7 @@ function ProxyAndSshTunnelTab({ sshTunnelOptions={connectionOptions.sshTunnel} updateConnectionFormField={updateConnectionFormField} connectionStringUrl={connectionStringUrl} + openSettingsModal={openSettingsModal} /> )} diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.spec.tsx index 3b23ebce5a3..aafe97c9254 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/socks.spec.tsx @@ -22,20 +22,23 @@ const formFields = [ key: 'proxyPassword', value: 'password', }, -]; +] as const; const proxyParams = { proxyHost: 'hello-world.com', - proxyPort: 1080, + proxyPort: '1080', proxyUsername: 'cosmo', proxyPassword: 'kramer', -}; +} as const; const connectionStringUrl = new ConnectionStringUrl( 'mongodb+srv://0ranges:p!neapp1es@localhost/' ); for (const key in proxyParams) { - connectionStringUrl.searchParams.set(key, proxyParams[key]); + connectionStringUrl.searchParams.set( + key, + proxyParams[key as keyof typeof proxyParams] + ); } describe('TunnelSocks', function () { diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-identity.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-identity.spec.tsx index b7270c442e7..2035e02abed 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-identity.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-identity.spec.tsx @@ -8,7 +8,10 @@ import SSHTunnelIdentity from './ssh-tunnel-identity'; import type { ConnectionFormError } from '../../../utils/validation'; import { errorMessageByFieldName } from '../../../utils/validation'; -const formFields = [ +const formFields: { + key: keyof SSHConnectionOptions; + value: string; +}[] = [ { key: 'host', value: 'host', @@ -61,7 +64,7 @@ describe('SSHTunnelIdentity', function () { if (key !== 'identityKeyFile') { expect(el.getAttribute('value'), `renders ${key} value`).to.equal( - sshTunnelOptions[key].toString() + sshTunnelOptions[key]?.toString() ); } }); @@ -92,14 +95,17 @@ describe('SSHTunnelIdentity', function () { { fieldName: 'sshHostname', message: 'Invalid host', + fieldTab: 'authentication', }, { fieldName: 'sshUsername', message: 'Invalid username', + fieldTab: 'authentication', }, { fieldName: 'sshIdentityKeyFile', message: 'Invalid file', + fieldTab: 'authentication', }, ]; @@ -112,17 +118,23 @@ describe('SSHTunnelIdentity', function () { ); expect( - screen.getByText(errorMessageByFieldName(errors, 'sshHostname')), + screen.getByText( + errorMessageByFieldName(errors, 'sshHostname') as string + ), 'renders sshHostname field error' ).to.exist; expect( - screen.getByText(errorMessageByFieldName(errors, 'sshUsername')), + screen.getByText( + errorMessageByFieldName(errors, 'sshUsername') as string + ), 'renders sshUsername field error' ).to.exist; expect( - screen.getByText(errorMessageByFieldName(errors, 'sshIdentityKeyFile')), + screen.getByText( + errorMessageByFieldName(errors, 'sshIdentityKeyFile') as string + ), 'renders sshIdentityKeyFile field error' ).to.exist; }); diff --git a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-password.spec.tsx b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-password.spec.tsx index e542b46a5d7..6a03e01c89d 100644 --- a/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-password.spec.tsx +++ b/packages/connection-form/src/components/advanced-options-tabs/ssh-tunnel-tab/ssh-tunnel-password.spec.tsx @@ -25,14 +25,14 @@ const formFields = [ key: 'password', value: 'password', }, -]; +] as const; const sshTunnelOptions: SSHConnectionOptions = { host: 'old host', port: 22, username: 'old username', password: 'old password', -}; +} as const; describe('SSHTunnelPassword', function () { let updateConnectionFormFieldSpy: sinon.SinonSpy; @@ -54,7 +54,7 @@ describe('SSHTunnelPassword', function () { const el = screen.getByTestId(key); expect(el, `renders ${key} field`).to.exist; expect(el.getAttribute('value'), `renders ${key} value`).to.equal( - sshTunnelOptions[key].toString() + sshTunnelOptions[key]?.toString() ); }); }); @@ -97,17 +97,23 @@ describe('SSHTunnelPassword', function () { ); expect( - screen.getByText(errorMessageByFieldName(errors, 'sshHostname')), + screen.getByText( + errorMessageByFieldName(errors, 'sshHostname') as string + ), 'renders sshHostname field error' ).to.exist; expect( - screen.getByText(errorMessageByFieldName(errors, 'sshUsername')), + screen.getByText( + errorMessageByFieldName(errors, 'sshUsername') as string + ), 'renders sshUsername field error' ).to.exist; expect( - screen.getByText(errorMessageByFieldName(errors, 'sshPassword')), + screen.getByText( + errorMessageByFieldName(errors, 'sshPassword') as string + ), 'renders sshPassword field error' ).to.exist; }); diff --git a/packages/connection-form/src/components/connection-form.spec.tsx b/packages/connection-form/src/components/connection-form.spec.tsx index 42c61f90cfb..24d0feb9bd4 100644 --- a/packages/connection-form/src/components/connection-form.spec.tsx +++ b/packages/connection-form/src/components/connection-form.spec.tsx @@ -38,7 +38,6 @@ describe('ConnectionForm Component', function () { return render( { - /* */ - }} connectionErrorMessage="connection error" initialConnectionInfo={{ id: 'test', @@ -328,10 +327,10 @@ describe('ConnectionForm Component', function () { expect(screen.getByText('connection error')).to.be.visible; }); - it('should show a Save & Connect button when there is no existing connection', function () { + // TODO(COMPASS-7762) + it.skip('should show a Save & Connect button when there is no existing connection', function () { render( expect(connectionString.value).to.equal('')); @@ -433,15 +433,17 @@ describe('ConnectionForm Component', function () { const personalizationName = screen.getByTestId( 'personalization-name-input' - ); + ) as HTMLInputElement; expect(personalizationName.value).to.equal('myserver:27017'); }); it('should not sync with the href of the connection string when it has been edited', async function () { - const connectionString = screen.getByTestId('connectionString'); + const connectionString = screen.getByTestId( + 'connectionString' + ) as HTMLInputElement; const personalizationName = screen.getByTestId( 'personalization-name-input' - ); + ) as HTMLInputElement; userEvent.clear(personalizationName); userEvent.clear(connectionString); diff --git a/packages/connection-form/src/components/connection-form.tsx b/packages/connection-form/src/components/connection-form.tsx index 4b659dd3626..a0896967c72 100644 --- a/packages/connection-form/src/components/connection-form.tsx +++ b/packages/connection-form/src/components/connection-form.tsx @@ -343,6 +343,7 @@ type ConnectionFormPropsWithoutPreferences = { onSaveAndConnectClicked?: (connectionInfo: ConnectionInfo) => void; onSaveClicked: (connectionInfo: ConnectionInfo) => Promise; onAdvancedOptionsToggle?: (newState: boolean) => void; + openSettingsModal?: (tab?: string) => void; }; export type ConnectionFormProps = ConnectionFormPropsWithoutPreferences & { @@ -357,11 +358,12 @@ function ConnectionForm({ onSaveClicked, onCancel, onAdvancedOptionsToggle, + openSettingsModal, }: ConnectionFormPropsWithoutPreferences): React.ReactElement { const [advancedOpen, setAdvancedOpen] = useState(false); const isDarkMode = useDarkMode(); const isMultiConnectionEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const onAdvancedChange = useCallback( @@ -455,6 +457,8 @@ function ConnectionForm({ (action: 'saveAndConnect' | 'connect') => { // TODO(COMPASS-7906): cleanup const updatedConnectionOptions = cloneDeep(connectionOptions); + // TODO: this method throws on malformed connection strings instead of + // returning errors const formErrors = validateConnectionOptionsErrors( updatedConnectionOptions ); @@ -609,6 +613,7 @@ function ConnectionForm({ disabled={!!connectionStringInvalidError} updateConnectionFormField={updateConnectionFormField} connectionOptions={connectionOptions} + openSettingsModal={openSettingsModal} /> )} diff --git a/packages/connection-form/src/components/connection-string-input.spec.tsx b/packages/connection-form/src/components/connection-string-input.spec.tsx index 1cfeda49c73..8d7a7e51de3 100644 --- a/packages/connection-form/src/components/connection-string-input.spec.tsx +++ b/packages/connection-form/src/components/connection-string-input.spec.tsx @@ -222,7 +222,9 @@ describe('ConnectionStringInput Component', function () { screen.getByRole('switch').click(); // Click confirm on the modal that opens. - const confirmButton = screen.getByText('Confirm').closest('button'); + const confirmButton = screen + .getByText('Confirm') + .closest('button') as HTMLButtonElement; fireEvent( confirmButton, new MouseEvent('click', { @@ -248,7 +250,9 @@ describe('ConnectionStringInput Component', function () { screen.getByRole('switch').click(); // Click cancel on the modal that opens. - const cancelButton = screen.getByText('Cancel').closest('button'); + const cancelButton = screen + .getByText('Cancel') + .closest('button') as HTMLButtonElement; fireEvent( cancelButton, new MouseEvent('click', { diff --git a/packages/connection-form/src/components/save-connection-modal.spec.tsx b/packages/connection-form/src/components/save-connection-modal.spec.tsx index fd3e1f0f5ff..7c66ee95950 100644 --- a/packages/connection-form/src/components/save-connection-modal.spec.tsx +++ b/packages/connection-form/src/components/save-connection-modal.spec.tsx @@ -7,7 +7,7 @@ import SaveConnectionModal from './save-connection-modal'; describe('SaveConnectionModal Component', function () { let onSaveSpy: sinon.SinonSpy; - let onCancelSpy; + let onCancelSpy: sinon.SinonSpy; beforeEach(function () { onSaveSpy = sinon.spy(); diff --git a/packages/connection-form/src/hooks/use-connect-form-preferences.tsx b/packages/connection-form/src/hooks/use-connect-form-preferences.tsx index 0b55b3067af..0a2367841bf 100644 --- a/packages/connection-form/src/hooks/use-connect-form-preferences.tsx +++ b/packages/connection-form/src/hooks/use-connect-form-preferences.tsx @@ -13,6 +13,7 @@ export type ConnectionFormPreferences = { showOIDCAuth: boolean; showKerberosAuth: boolean; showCSFLE: boolean; + showProxySettings: boolean; }; const defaultPreferences = { @@ -27,6 +28,7 @@ const defaultPreferences = { showOIDCAuth: true, showKerberosAuth: true, showCSFLE: true, + showProxySettings: true, }; export const ConnectionFormPreferencesContext = createContext< diff --git a/packages/connection-form/src/hooks/use-connect-form.spec.ts b/packages/connection-form/src/hooks/use-connect-form.spec.ts index f9d06d7d567..c8d7c63afc0 100644 --- a/packages/connection-form/src/hooks/use-connect-form.spec.ts +++ b/packages/connection-form/src/hooks/use-connect-form.spec.ts @@ -104,6 +104,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -141,6 +147,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -180,6 +192,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -215,6 +233,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -248,6 +272,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -284,6 +314,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -327,6 +363,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -365,6 +407,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -403,6 +451,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -439,6 +493,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -481,6 +541,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -523,6 +589,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -566,6 +638,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -611,6 +689,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -652,6 +736,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); }); @@ -699,6 +789,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); @@ -720,6 +816,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -740,6 +842,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -766,6 +874,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -788,6 +902,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -815,6 +935,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -833,6 +959,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -853,6 +985,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -873,6 +1011,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -893,6 +1037,12 @@ describe('use-connect-form hook', function () { }, { connectionString: connectionStringUrl.toString(), + }, + { + name: 'does not matter', + color: 'color1', + isFavorite: false, + isNameDirty: false, } ); expect( @@ -915,7 +1065,7 @@ describe('use-connect-form hook', function () { color: 'color4', }, savedConnectionType: 'favorite', - }; + } as const; it('should inherit the initial information from the connection information', function () { const { result } = renderHook(() => @@ -945,8 +1095,8 @@ describe('use-connect-form hook', function () { initialState.current[0].personalizationOptions ); - expect(result.personalizationOptions.name).to.equal('turtles'); - expect(result.personalizationOptions.isNameDirty).to.be.true; + expect(result.personalizationOptions?.name).to.equal('turtles'); + expect(result.personalizationOptions?.isNameDirty).to.be.true; }); }); @@ -985,8 +1135,8 @@ describe('use-connect-form hook', function () { initialState.current[0].personalizationOptions ); - expect(result.personalizationOptions.name).to.equal('localhost:27019'); - expect(result.personalizationOptions.isNameDirty).to.be.false; + expect(result.personalizationOptions?.name).to.equal('localhost:27019'); + expect(result.personalizationOptions?.isNameDirty).to.be.false; }); it('should not be inferred when the name is dirty', function () { @@ -1007,8 +1157,8 @@ describe('use-connect-form hook', function () { } ); - expect(result.personalizationOptions.name).to.equal('webscale'); - expect(result.personalizationOptions.isNameDirty).to.be.true; + expect(result.personalizationOptions?.name).to.equal('webscale'); + expect(result.personalizationOptions?.isNameDirty).to.be.true; }); it('should support empty names', function () { @@ -1029,8 +1179,8 @@ describe('use-connect-form hook', function () { } ); - expect(result.personalizationOptions.name).to.equal(''); - expect(result.personalizationOptions.isNameDirty).to.be.true; + expect(result.personalizationOptions?.name).to.equal(''); + expect(result.personalizationOptions?.isNameDirty).to.be.true; }); }); }); diff --git a/packages/connection-form/src/hooks/use-connect-form.ts b/packages/connection-form/src/hooks/use-connect-form.ts index fbb0deae1c0..678b36b6f20 100644 --- a/packages/connection-form/src/hooks/use-connect-form.ts +++ b/packages/connection-form/src/hooks/use-connect-form.ts @@ -179,10 +179,16 @@ type ConnectionFormFieldActions = value: string; } | { - type: 'remove-ssh-options'; + type: 'remove-ssh-options-and-app-proxy'; } | { - type: 'remove-proxy-options'; + type: 'remove-proxy-options-and-app-proxy'; + } + | { + type: 'remove-proxy-options-and-set-app-proxy'; + } + | { + type: 'remove-app-proxy'; } | UpdateCsfleStoreCredentialsAction | UpdateCsfleAction @@ -606,7 +612,8 @@ export function handleConnectionFormFieldUpdate( }, }; } - case 'remove-proxy-options': { + case 'remove-proxy-options-and-app-proxy': + case 'remove-proxy-options-and-set-app-proxy': { const proxyOptions: (keyof ProxyOptions)[] = [ 'proxyHost', 'proxyPort', @@ -618,14 +625,25 @@ export function handleConnectionFormFieldUpdate( connectionOptions: { ...currentConnectionOptions, connectionString: parsedConnectionStringUrl.toString(), + useApplicationLevelProxy: + action.type === 'remove-proxy-options-and-set-app-proxy', }, }; } - case 'remove-ssh-options': { + case 'remove-ssh-options-and-app-proxy': { return { connectionOptions: { ...currentConnectionOptions, sshTunnel: undefined, + useApplicationLevelProxy: false, + }, + }; + } + case 'remove-app-proxy': { + return { + connectionOptions: { + ...currentConnectionOptions, + useApplicationLevelProxy: false, }, }; } diff --git a/packages/connection-form/src/hooks/use-connection-color.spec.tsx b/packages/connection-form/src/hooks/use-connection-color.spec.tsx index 29111be74ec..ebf137b18c6 100644 --- a/packages/connection-form/src/hooks/use-connection-color.spec.tsx +++ b/packages/connection-form/src/hooks/use-connection-color.spec.tsx @@ -27,13 +27,14 @@ describe('useConnectionColor', function () { it('converts a color code to hex', function () { for (const colorCode of CONNECTION_COLOR_CODES) { const { container } = render(); - expect(container.firstChild.textContent).to.match( + expect(container.firstChild?.textContent).to.match( /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ ); } }); - it('converts legacy colors', function () { + // TODO(COMPASS-7906): remove + it.skip('converts legacy colors', function () { const legacyColors = { '#5fc86e': 'color1', '#326fde': 'color2', @@ -54,8 +55,9 @@ describe('useConnectionColor', function () { const { container: container2 } = render( ); - expect(container1.firstChild.textContent).to.equal( - container2.firstChild.textContent + expect(container1.firstChild?.textContent).to.be.not.be.undefined; + expect(container1.firstChild?.textContent).to.equal( + container2.firstChild?.textContent ); } }); @@ -64,12 +66,12 @@ describe('useConnectionColor', function () { const { container } = render( ); - expect(container.firstChild.textContent).to.be.empty; + expect(container.firstChild?.textContent).to.be.empty; }); it('does not convert an unknown hex code', function () { const { container } = render(); - expect(container.firstChild.textContent).to.be.empty; + expect(container.firstChild?.textContent).to.be.empty; }); describe('connection color names', function () { diff --git a/packages/connection-form/src/hooks/use-connection-color.ts b/packages/connection-form/src/hooks/use-connection-color.ts index 8c9354fb956..7a5b232b291 100644 --- a/packages/connection-form/src/hooks/use-connection-color.ts +++ b/packages/connection-form/src/hooks/use-connection-color.ts @@ -189,7 +189,7 @@ export function useConnectionColor(): { ); const isMultiConnectionEnabled = usePreference( - 'enableNewMultipleConnectionSystem' + 'enableMultipleConnectionSystem' ); const connectionColorCodes = () => { diff --git a/packages/connection-form/src/utils/check-for-invalid-character-in-host.spec.ts b/packages/connection-form/src/utils/check-for-invalid-character-in-host.spec.ts index 68e7e692544..a444dd053e4 100644 --- a/packages/connection-form/src/utils/check-for-invalid-character-in-host.spec.ts +++ b/packages/connection-form/src/utils/check-for-invalid-character-in-host.spec.ts @@ -11,7 +11,7 @@ describe('#checkForInvalidCharacterInHost', function () { let errorThrown; try { checkForInvalidCharacterInHost('aaAA@@aa', false); - } catch (e) { + } catch (e: any) { // Expected to throw. errorThrown = e.message; } @@ -23,7 +23,7 @@ describe('#checkForInvalidCharacterInHost', function () { let errorThrown; try { checkForInvalidCharacterInHost('localhost,,', true); - } catch (e) { + } catch (e: any) { // Expected to throw. errorThrown = e.message; } @@ -35,7 +35,7 @@ describe('#checkForInvalidCharacterInHost', function () { let errorThrown; try { checkForInvalidCharacterInHost('localhost:222', true); - } catch (e) { + } catch (e: any) { // Expected to throw. errorThrown = e.message; } diff --git a/packages/connection-form/src/utils/connection-ssh-handler.spec.ts b/packages/connection-form/src/utils/connection-ssh-handler.spec.ts index 82606ae0aa3..3d6d4234a96 100644 --- a/packages/connection-form/src/utils/connection-ssh-handler.spec.ts +++ b/packages/connection-form/src/utils/connection-ssh-handler.spec.ts @@ -20,16 +20,16 @@ describe('#handleUpdateSshOptions', function () { expect(response.connectionOptions.connectionString).to.equal( connectionString ); - expect(response.connectionOptions.sshTunnel.host).to.equal('localhost'); - expect(response.connectionOptions.sshTunnel.port).to.equal(22); - expect(response.connectionOptions.sshTunnel.username).to.equal(''); - expect(response.connectionOptions.sshTunnel.password).to.equal(undefined); - expect(response.connectionOptions.sshTunnel.identityKeyFile).to.equal( - undefined - ); - expect(response.connectionOptions.sshTunnel.identityKeyPassphrase).to.equal( + expect(response.connectionOptions.sshTunnel?.host).to.equal('localhost'); + expect(response.connectionOptions.sshTunnel?.port).to.equal(22); + expect(response.connectionOptions.sshTunnel?.username).to.equal(''); + expect(response.connectionOptions.sshTunnel?.password).to.equal(undefined); + expect(response.connectionOptions.sshTunnel?.identityKeyFile).to.equal( undefined ); + expect( + response.connectionOptions.sshTunnel?.identityKeyPassphrase + ).to.equal(undefined); }); it('should handle tab update with initial options', function () { @@ -52,15 +52,15 @@ describe('#handleUpdateSshOptions', function () { expect(response.connectionOptions.connectionString).to.equal( connectionString ); - expect(response.connectionOptions.sshTunnel.host).to.equal('localhosted'); - expect(response.connectionOptions.sshTunnel.port).to.equal(22); - expect(response.connectionOptions.sshTunnel.username).to.equal('root'); - expect(response.connectionOptions.sshTunnel.password).to.equal(undefined); - expect(response.connectionOptions.sshTunnel.identityKeyFile).to.equal( - undefined - ); - expect(response.connectionOptions.sshTunnel.identityKeyPassphrase).to.equal( + expect(response.connectionOptions.sshTunnel?.host).to.equal('localhosted'); + expect(response.connectionOptions.sshTunnel?.port).to.equal(22); + expect(response.connectionOptions.sshTunnel?.username).to.equal('root'); + expect(response.connectionOptions.sshTunnel?.password).to.equal(undefined); + expect(response.connectionOptions.sshTunnel?.identityKeyFile).to.equal( undefined ); + expect( + response.connectionOptions.sshTunnel?.identityKeyPassphrase + ).to.equal(undefined); }); }); diff --git a/packages/connection-form/src/utils/connection-ssh-handler.ts b/packages/connection-form/src/utils/connection-ssh-handler.ts index 3a58d2d9638..61fe3f2d5d6 100644 --- a/packages/connection-form/src/utils/connection-ssh-handler.ts +++ b/packages/connection-form/src/utils/connection-ssh-handler.ts @@ -4,7 +4,12 @@ import { defaultSshPort } from '../constants/default-connection'; export type SSHConnectionOptions = NonNullable; -export type TunnelType = 'none' | 'ssh-password' | 'ssh-identity' | 'socks'; +export type TunnelType = + | 'none' + | 'ssh-password' + | 'ssh-identity' + | 'socks' + | 'app-proxy'; export interface UpdateSshOptions { type: 'update-ssh-options'; diff --git a/packages/connection-form/src/utils/connection-string-helpers.spec.ts b/packages/connection-form/src/utils/connection-string-helpers.spec.ts index 55f5671fb50..dff395cf392 100644 --- a/packages/connection-form/src/utils/connection-string-helpers.spec.ts +++ b/packages/connection-form/src/utils/connection-string-helpers.spec.ts @@ -38,10 +38,10 @@ describe('connection-string-helpers', function () { 'mongodb://outerspace:27099?directConnection=true' ); - expect(connectionString.toString()).to.equal( + expect(connectionString?.toString()).to.equal( 'mongodb://outerspace:27099/?directConnection=true' ); - expect(connectionString.hosts[0]).to.equal('outerspace:27099'); + expect(connectionString?.hosts[0]).to.equal('outerspace:27099'); }); it('should return without an error when successfully parsed', function () { @@ -59,7 +59,7 @@ describe('connection-string-helpers', function () { ); expect(connectionString).to.equal(undefined); - expect(error.message).to.equal( + expect(error?.message).to.equal( 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' ); }); @@ -69,7 +69,7 @@ describe('connection-string-helpers', function () { 'mongos://pineapple:27099/?directConnection=true' ); - expect(connectionString.href).to.equal( + expect(connectionString?.href).to.equal( 'mongos://pineapple:27099/?directConnection=true' ); expect(error).to.equal(undefined); diff --git a/packages/connection-form/src/utils/csfle-handler.spec.ts b/packages/connection-form/src/utils/csfle-handler.spec.ts index 4b5eeb5d9bc..79772cbdcb7 100644 --- a/packages/connection-form/src/utils/csfle-handler.spec.ts +++ b/packages/connection-form/src/utils/csfle-handler.spec.ts @@ -262,6 +262,9 @@ describe('csfle-handler', function () { const obj = textToEncryptedFieldConfig( encryptedFieldConfigToText(exampleObject) ); + if (!obj) { + throw new Error('expected obj'); + } expect(obj).to.deep.equal({ ...exampleObject, '$compass.error': null, diff --git a/packages/connection-info/package.json b/packages/connection-info/package.json index fb10b24d3ef..fc3d6d5e392 100644 --- a/packages/connection-info/package.json +++ b/packages/connection-info/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.5.3", + "version": "0.6.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -25,6 +25,7 @@ "main": "dist/index.js", "compass:main": "src/index.ts", "exports": { + "types": "./dist/index.d.ts", "import": "./dist/.esm-wrapper.mjs", "require": "./dist/index.js" }, @@ -53,11 +54,11 @@ "lodash": "^4.17.21", "mongodb": "^6.8.0", "mongodb-connection-string-url": "^3.0.1", - "mongodb-data-service": "^22.22.3" + "mongodb-data-service": "^22.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/connection-info/src/connection-info.ts b/packages/connection-info/src/connection-info.ts index e0d0c260058..a237afd8fa4 100644 --- a/packages/connection-info/src/connection-info.ts +++ b/packages/connection-info/src/connection-info.ts @@ -32,11 +32,9 @@ export interface ConnectionInfo { /** * Saved connection type. Legacy favorite connections will be mapped as - * 'favorite'. 'autoConnectInfo' type is to identify when a particular - * connection info is resolved by ConnectionStorage.getAutoConnectInfo and - * they are also never saved to disk. + * 'favorite' */ - savedConnectionType?: 'favorite' | 'recent' | 'autoConnectInfo'; + savedConnectionType?: 'favorite' | 'recent'; /** * The options used to connect @@ -60,9 +58,3 @@ export interface ConnectionFavoriteOptions { */ color?: string; } - -export type ConnectionStatus = - | 'connected' - | 'disconnected' - | 'connecting' - | 'failed'; diff --git a/packages/connection-info/src/connection-title.ts b/packages/connection-info/src/connection-title.ts index 0283774ccdb..cf7aaf39466 100644 --- a/packages/connection-info/src/connection-title.ts +++ b/packages/connection-info/src/connection-title.ts @@ -1,22 +1,39 @@ import ConnectionString from 'mongodb-connection-string-url'; import type { ConnectionInfo } from './connection-info'; -export function getConnectionTitle( - info: Pick -): string { - if (info.atlasMetadata?.clusterName) { - return info.atlasMetadata.clusterName; - } +type ConnectionInfoForTitle = Pick< + ConnectionInfo, + 'favorite' | 'connectionOptions' | 'atlasMetadata' +>; + +const ConnectionInfoTitleCache = new WeakMap(); - if (info.favorite?.name) { - return info.favorite.name; +/** + * Returns the title for connection info. It is called often in various parts of + * the application, including render methods, so to save some runtime, the + * result is cached based on the connection info value by referece + */ +export function getConnectionTitle(info: ConnectionInfoForTitle): string { + let title = ConnectionInfoTitleCache.get(info); + + if (title) { + return title; } - try { - const url = new ConnectionString(info.connectionOptions.connectionString); - return url.hosts.join(','); - } catch (e) { - // When parsing a connection for its title fails we default the title. - return info.connectionOptions.connectionString || 'Connection'; + if (info.atlasMetadata?.clusterName) { + title = info.atlasMetadata.clusterName; + } else if (info.favorite?.name) { + title = info.favorite.name; + } else { + try { + const url = new ConnectionString(info.connectionOptions.connectionString); + title = url.hosts.join(','); + } catch (e) { + // When parsing a connection for its title fails we default the title. + title = info.connectionOptions.connectionString || 'Connection'; + } } + + ConnectionInfoTitleCache.set(info, title); + return title; } diff --git a/packages/connection-info/src/index.ts b/packages/connection-info/src/index.ts index b985554d5a5..040796b5b17 100644 --- a/packages/connection-info/src/index.ts +++ b/packages/connection-info/src/index.ts @@ -3,7 +3,6 @@ export { extractSecrets, mergeSecrets } from './connection-secrets'; export type { ConnectionInfo, ConnectionFavoriteOptions, - ConnectionStatus, AtlasClusterMetadata, } from './connection-info'; export { getConnectionTitle } from './connection-title'; diff --git a/packages/connection-storage/package.json b/packages/connection-storage/package.json index 985d6bdc39f..74968322a8e 100644 --- a/packages/connection-storage/package.json +++ b/packages/connection-storage/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.17.0", + "version": "0.18.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -56,24 +56,24 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-user-data": "^0.3.3", - "@mongodb-js/compass-utils": "^0.6.9", - "@mongodb-js/connection-info": "^0.5.3", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-user-data": "^0.3.4", + "@mongodb-js/compass-utils": "^0.6.10", + "@mongodb-js/connection-info": "^0.6.0", "bson": "^6.7.0", - "compass-preferences-model": "^2.26.0", - "electron": "^29.4.5", - "hadron-app-registry": "^9.2.2", - "hadron-ipc": "^3.2.20", + "compass-preferences-model": "^2.27.0", + "electron": "^30.4.0", + "hadron-app-registry": "^9.2.3", + "hadron-ipc": "^3.2.21", "keytar": "^7.9.0", "lodash": "^4.17.21", "mongodb-connection-string-url": "^3.0.1", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/connection-storage/src/auto-connect.spec.ts b/packages/connection-storage/src/auto-connect.spec.ts index 452ffa20233..d7b19f7323d 100644 --- a/packages/connection-storage/src/auto-connect.spec.ts +++ b/packages/connection-storage/src/auto-connect.spec.ts @@ -12,6 +12,7 @@ import type { AutoConnectPreferences, ConnectionStorage, } from './connection-storage'; +import { omit } from 'lodash'; let connectionStorage: ReturnType; @@ -19,7 +20,9 @@ const createGetAutoConnectInfoFnWithConnections = async ( connectPreferences: Record = {}, connections: ConnectionInfo[] = [], exportOptions: ExportConnectionOptions = {} -): Promise['getAutoConnectInfo']> => { +): Promise< + () => ReturnType['getAutoConnectInfo']> +> => { const tmpDir = await fs.mkdtemp( path.join(os.tmpdir(), 'connection-storage-tests') ); @@ -98,10 +101,7 @@ describe('auto connection argument parsing', function () { [connectionInfo] ); const info = await fn?.(); - expect(info).to.deep.equal({ - ...connectionInfo, - savedConnectionType: 'autoConnectInfo', - }); + expect(omit(info, 'savedConnectionType')).to.deep.equal(connectionInfo); }); it('rejects a multi-connection file if one has been specified without an id', async function () { @@ -175,10 +175,9 @@ describe('auto connection argument parsing', function () { ] ); const info = await fn?.(); - expect(info).to.deep.equal({ + expect(omit(info, 'savedConnectionType')).to.deep.equal({ ...connectionInfo, id: '9036dd5f-719b-46d1-b812-7e6348e1e9c9', - savedConnectionType: 'autoConnectInfo', }); }); @@ -237,10 +236,7 @@ describe('auto connection argument parsing', function () { [connectionInfo] ); const info = await fn?.(); - expect(info).to.deep.equal({ - ...connectionInfo, - savedConnectionType: 'autoConnectInfo', - }); + expect(omit(info, 'savedConnectionType')).to.deep.equal(connectionInfo); }); it('applies username and password if requested', async function () { diff --git a/packages/connection-storage/src/compass-main-connection-storage.spec.ts b/packages/connection-storage/src/compass-main-connection-storage.spec.ts index c7ed59e02e9..53bb7aa7365 100644 --- a/packages/connection-storage/src/compass-main-connection-storage.spec.ts +++ b/packages/connection-storage/src/compass-main-connection-storage.spec.ts @@ -4,7 +4,7 @@ import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { UUID } from 'bson'; -import { omit, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; import { initCompassMainConnectionStorage, @@ -786,6 +786,36 @@ describe('ConnectionStorage', function () { expect(JSON.parse(content).connectionInfo.id).to.be.equal(id); }); + it('saves a connection with all ConnectionOptions set', async function () { + const id = new UUID().toString(); + const connectionOptions: Required = { + connectionString: 'mongodb://localhost:27017/', + sshTunnel: { host: 'localhost', port: 2222, username: 'foobar' }, + useApplicationLevelProxy: true, + oidc: {}, + fleOptions: { storeCredentials: false }, + lookup: () => ({} as any), + }; + await connectionStorage.save({ + connectionInfo: { + id, + connectionOptions, + }, + }); + delete (connectionOptions as any).lookup; // intentionally not stored + + const content = await fs.readFile( + getConnectionFilePath(tmpDir, id), + 'utf-8' + ); + expect( + JSON.parse(content).connectionInfo.connectionOptions + ).to.deep.equal(connectionOptions); + expect( + (await connectionStorage.load({ id }))?.connectionOptions + ).to.deep.equal(connectionOptions); + }); + it('saves a connection with arbitrary authMechanism', async function () { const id = new UUID().toString(); await connectionStorage.save({ @@ -1302,7 +1332,6 @@ describe('ConnectionStorage', function () { expect(info?.connectionOptions).to.deep.equal( autoConnectInfo.connectionOptions ); - expect(info?.savedConnectionType).to.equal('autoConnectInfo'); }); it('should return autoConnectInfo when a file with positional argument is provided', async function () { @@ -1315,7 +1344,6 @@ describe('ConnectionStorage', function () { expect(info?.connectionOptions).to.deep.equal( autoConnectInfo.connectionOptions ); - expect(info?.savedConnectionType).to.equal('autoConnectInfo'); }); it('should return autoConnectInfo when a mongodb url is provided', async function () { @@ -1326,36 +1354,9 @@ describe('ConnectionStorage', function () { expect(info?.connectionOptions.connectionString).to.equal( 'mongodb://localhost:2021/' ); - expect(info?.savedConnectionType).to.equal('autoConnectInfo'); }); - it('should cache autoConnectInfo and subsequent calls should return the same autoConnectInfo', async function () { - const infoFromUrl = await connectionStorage.getAutoConnectInfo({ - shouldAutoConnect: true, - positionalArguments: ['mongodb://localhost:2021/'], - }); - - expect( - await connectionStorage.getAutoConnectInfo({ - shouldAutoConnect: true, - positionalArguments: ['mongodb://localhost:2021/'], - }) - ).to.deep.equal(infoFromUrl); - - const infoFromFile = await connectionStorage.getAutoConnectInfo({ - shouldAutoConnect: true, - file: getExportedConnectionsFilePath(tmpDir), - }); - - expect( - await connectionStorage.getAutoConnectInfo({ - shouldAutoConnect: true, - file: getExportedConnectionsFilePath(tmpDir), - }) - ).to.deep.equal(infoFromFile); - }); - - context('when autoConnectInfo is available and cached', function () { + context('when autoConnectInfo is available', function () { beforeEach(async function () { await connectionStorage.getAutoConnectInfo({ shouldAutoConnect: true, @@ -1363,24 +1364,6 @@ describe('ConnectionStorage', function () { }); }); - it('loadAll returns the disk connections alongside the autoConnectInfo', async function () { - const allConnections = await connectionStorage.loadAll(); - expect( - allConnections.find(({ id }) => { - return id === autoConnectInfo.id; - }) - ).to.not.be.undefined; - }); - - it('load returns the autoConnectInfo when there is a match', async function () { - const connection = await connectionStorage.load({ - id: autoConnectInfo.id, - }); - expect(omit(connection, 'savedConnectionType')).to.deep.equal( - autoConnectInfo - ); - }); - it('save ignores the call when the passed connectionInfo matches autoConnectInfo', async function () { await connectionStorage.save({ connectionInfo: { diff --git a/packages/connection-storage/src/compass-main-connection-storage.ts b/packages/connection-storage/src/compass-main-connection-storage.ts index 3a6233f4a75..ce27ccc5466 100644 --- a/packages/connection-storage/src/compass-main-connection-storage.ts +++ b/packages/connection-storage/src/compass-main-connection-storage.ts @@ -1,7 +1,6 @@ import { type HadronIpcMain, ipcMain } from 'hadron-ipc'; import keytar from 'keytar'; import { safeStorage } from 'electron'; -import { UUID } from 'bson'; import fsPromises from 'fs/promises'; import ConnectionString from 'mongodb-connection-string-url'; @@ -77,6 +76,7 @@ const ConnectionSchema: z.Schema = z useSystemCA: z.boolean().optional(), // Unused but may be present in legacy files oidc: z.any().optional(), fleOptions: z.any().optional(), + useApplicationLevelProxy: z.boolean().optional(), }), }) .optional(), @@ -87,7 +87,6 @@ const ConnectionSchema: z.Schema = z class CompassMainConnectionStorage implements ConnectionStorage { private readonly userData: UserData; - private autoConnectInfo: ConnectionInfo | undefined | null = null; private readonly version = 1; private readonly maxAllowedRecentConnections = 10; @@ -125,13 +124,12 @@ class CompassMainConnectionStorage implements ConnectionStorage { const connections = await this.getConnections(); const loadedConnections = connections // Ignore legacy connections and make sure connection has a connection string. - .filter((x) => x.connectionInfo?.connectionOptions?.connectionString) + .filter((x) => { + return x.connectionInfo?.connectionOptions?.connectionString; + }) .map((connection) => this.mapStoredConnectionToConnectionInfo(connection) ); - if (this.autoConnectInfo) { - return [this.autoConnectInfo, ...loadedConnections]; - } return loadedConnections; } catch (err) { log.error( @@ -155,10 +153,6 @@ class CompassMainConnectionStorage implements ConnectionStorage { if (!id) { return undefined; } - if (id === this.autoConnectInfo?.id) { - return this.autoConnectInfo; - } - const connections = await this.loadAll(); return connections.find((connection) => id === connection.id); } @@ -171,18 +165,6 @@ class CompassMainConnectionStorage implements ConnectionStorage { signal?: AbortSignal; }): Promise { throwIfAborted(signal); - if ( - connectionInfo.id === this.autoConnectInfo?.id || - connectionInfo.savedConnectionType === 'autoConnectInfo' - ) { - log.warn( - mongoLogId(1_001_000_311), - 'Connection Storage', - 'Attempted to save autoConnectInfo, ignoring the call' - ); - return; - } - try { // While saving connections, we also save `_id` property // in order to support the downgrade of Compass to a version @@ -233,15 +215,6 @@ class CompassMainConnectionStorage implements ConnectionStorage { if (!id) { return; } - if (id === this.autoConnectInfo?.id) { - log.warn( - mongoLogId(1_001_000_312), - 'Connection Storage', - 'Attempted to save autoConnectInfo, ignoring the call' - ); - return; - } - try { await this.userData.delete(id); } catch (err) { @@ -269,11 +242,7 @@ class CompassMainConnectionStorage implements ConnectionStorage { shouldAutoConnect, } = autoConnectPreferences; - if (!shouldAutoConnect) return (this.autoConnectInfo = undefined); - - if (this.autoConnectInfo !== null) { - return this.autoConnectInfo; - } + if (!shouldAutoConnect) return undefined; const getConnectionStringFromArgs = (args?: string[]) => args?.[0]; @@ -324,29 +293,24 @@ class CompassMainConnectionStorage implements ConnectionStorage { `Could not find connection with id '${id}' in connection file '${file}'` ); } - return (this.autoConnectInfo = applyUsernameAndPassword( - { - ...connectionInfo, - savedConnectionType: 'autoConnectInfo', - }, - { - username, - password, - } - )); + return applyUsernameAndPassword(connectionInfo, { + username, + password, + }); } else { const connectionString = getConnectionStringFromArgs(positionalArguments); if (!connectionString) { throw new Error('Could not find a connection string'); } - return (this.autoConnectInfo = applyUsernameAndPassword( + return applyUsernameAndPassword( { connectionOptions: { connectionString }, - id: new UUID().toString(), - savedConnectionType: 'autoConnectInfo', + // Same connection id if we're not loading it from disk where id + // should exist already either in file or positional args + id: 'autoconnection', }, { username, password } - )); + ); } } @@ -545,21 +509,6 @@ class CompassMainConnectionStorage implements ConnectionStorage { } } - on(): ConnectionStorage { - // noop - return this; - } - - off(): ConnectionStorage { - // noop - return this; - } - - emit(): boolean { - // noop - return false; - } - // This method is only called when compass tries to migrate connections to a new version. // In e2e-tests we do not migrate any connections as all the connections are created // in the new format, so keychain is not triggered (using keytar) and hence test is not blocked. diff --git a/packages/connection-storage/src/compass-renderer-connection-storage.spec.ts b/packages/connection-storage/src/compass-renderer-connection-storage.spec.ts deleted file mode 100644 index 20d29fb2999..00000000000 --- a/packages/connection-storage/src/compass-renderer-connection-storage.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import sinon from 'sinon'; -import { - CompassRendererConnectionStorage, - type ConnectionStorageIPCInterface, - type ConnectionStorageIPCRenderer, -} from './compass-renderer-connection-storage'; -import { TEST_CONNECTION_INFO } from '@mongodb-js/compass-connections/provider'; -import { expect } from 'chai'; - -describe('CompassRendererConnectionStorage', function () { - describe('getAutoConnectInfo', function () { - it('should emit ConnectionChanged event after first fetch of autoConnectInfo', async function () { - const getAutoConnectInfo = sinon.stub().resolves(TEST_CONNECTION_INFO); - const ipcStub: ConnectionStorageIPCRenderer = < - ConnectionStorageIPCRenderer - >{ - createInvoke() { - return { - getAutoConnectInfo, - } as unknown as ConnectionStorageIPCInterface; - }, - async call() {}, - }; - const storage = new CompassRendererConnectionStorage(ipcStub); - const connectionsChangedStub = sinon.stub(); - storage.on('ConnectionsChanged', connectionsChangedStub); - // emits the event - await storage.getAutoConnectInfo(); - // does nothing - await storage.getAutoConnectInfo(); - expect(connectionsChangedStub).to.be.calledOnce; - }); - - it('should not return an autoConnectInfo once it has been requested already', async function () { - const getAutoConnectInfo = sinon.stub().resolves(TEST_CONNECTION_INFO); - const ipcStub: ConnectionStorageIPCRenderer = < - ConnectionStorageIPCRenderer - >{ - createInvoke() { - return { - getAutoConnectInfo, - } as unknown as ConnectionStorageIPCInterface; - }, - async call() {}, - }; - const storage = new CompassRendererConnectionStorage(ipcStub); - expect(await storage.getAutoConnectInfo()).to.deep.equal( - TEST_CONNECTION_INFO - ); - expect(await storage.getAutoConnectInfo()).to.be.undefined; - }); - }); -}); diff --git a/packages/connection-storage/src/compass-renderer-connection-storage.ts b/packages/connection-storage/src/compass-renderer-connection-storage.ts index 07e3fa31d27..d12f32e3a68 100644 --- a/packages/connection-storage/src/compass-renderer-connection-storage.ts +++ b/packages/connection-storage/src/compass-renderer-connection-storage.ts @@ -1,12 +1,8 @@ -import { EventEmitter } from 'events'; import type { HadronIpcRenderer } from 'hadron-ipc'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; -import { - type ConnectionStorageEvent, - type ConnectionStorageEventListeners, - type ConnectionStorage, - type AutoConnectPreferences, - ConnectionStorageEvents, +import type { + AutoConnectPreferences, + ConnectionStorage, } from './connection-storage'; import type { ExportConnectionOptions, @@ -22,26 +18,9 @@ export type ConnectionStorageIPCRenderer = Pick< 'createInvoke' | 'call' >; -class CompassRendererConnectionStorage - extends EventEmitter - implements ConnectionStorage -{ +class CompassRendererConnectionStorage implements ConnectionStorage { private _ipc: ConnectionStorageIPCInterface | undefined; - /** - * TODO(COMPASS-7858): We would like to avoid a situation where in the same - * render process there are multiple places asking for auto connect info and - * potentially trying to auto connect ("accidentally"). So we have this little - * state here tracking if the auto connect info has already been requested - * once and if yes the getAutoConnectInfo won't return anything for subsequent - * calls. - */ - private hasAlreadyRequestedAutoConnectInfo = false; - constructor( - private readonly ipcRenderer?: ConnectionStorageIPCRenderer, - private readonly getInitialAutoConnectPreferences?: () => Promise - ) { - super(); - } + constructor(private readonly ipcRenderer?: ConnectionStorageIPCRenderer) {} get ipc() { const ipc = @@ -92,7 +71,6 @@ class CompassRendererConnectionStorage signal?: AbortSignal | undefined; }): Promise { await this.ipc.save(options); - this.emit(ConnectionStorageEvents.ConnectionsChanged); } async delete(options: { @@ -100,24 +78,12 @@ class CompassRendererConnectionStorage signal?: AbortSignal | undefined; }): Promise { await this.ipc.delete(options); - this.emit(ConnectionStorageEvents.ConnectionsChanged); } async getAutoConnectInfo( - autoConnectPreferences?: AutoConnectPreferences + autoConnectPreferences: AutoConnectPreferences ): Promise { - if (this.hasAlreadyRequestedAutoConnectInfo) { - return; - } - this.hasAlreadyRequestedAutoConnectInfo = true; - const autoConnectInfo = await this.ipc.getAutoConnectInfo( - autoConnectPreferences ?? - (await this.getInitialAutoConnectPreferences?.()) ?? { - shouldAutoConnect: false, - } - ); - this.emit(ConnectionStorageEvents.ConnectionsChanged); - return autoConnectInfo; + return await this.ipc.getAutoConnectInfo(autoConnectPreferences); } getLegacyConnections( @@ -151,42 +117,6 @@ class CompassRendererConnectionStorage signal?: AbortSignal | undefined; }): Promise { await this.ipc.importConnections(args); - this.emit(ConnectionStorageEvents.ConnectionsChanged); - } - - on( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): this { - return super.on(eventName, listener); - } - - off( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): this { - return super.off(eventName, listener); - } - - once( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): this { - return super.once(eventName, listener); - } - - removeListener( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): this { - return super.removeListener(eventName, listener); - } - - emit( - eventName: T, - ...args: Parameters - ): boolean { - return super.emit(eventName, ...args); } } diff --git a/packages/connection-storage/src/connection-storage.ts b/packages/connection-storage/src/connection-storage.ts index 58e090ccb51..023e12b9478 100644 --- a/packages/connection-storage/src/connection-storage.ts +++ b/packages/connection-storage/src/connection-storage.ts @@ -10,17 +10,6 @@ import type { AllPreferences } from 'compass-preferences-model'; export type { ConnectionInfo, AtlasClusterMetadata }; -export const ConnectionStorageEvents = { - ConnectionsChanged: 'ConnectionsChanged', -} as const; - -export type ConnectionStorageEvent = - typeof ConnectionStorageEvents[keyof typeof ConnectionStorageEvents]; - -export type ConnectionStorageEventListeners = { - [ConnectionStorageEvents.ConnectionsChanged]: () => void; -}; - export type AutoConnectPreferences = Partial< Pick< AllPreferences, @@ -34,21 +23,6 @@ export type AutoConnectPreferences = Partial< > & { shouldAutoConnect: boolean }; export interface ConnectionStorage { - on( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): ConnectionStorage; - - off( - eventName: T, - listener: ConnectionStorageEventListeners[T] - ): ConnectionStorage; - - emit( - eventName: T, - ...args: Parameters - ): boolean; - loadAll(options?: { signal?: AbortSignal }): Promise; load(options: { @@ -64,7 +38,7 @@ export interface ConnectionStorage { delete?(options: { id: string; signal?: AbortSignal }): Promise; getAutoConnectInfo?( - autoConnectPreferences?: AutoConnectPreferences + autoConnectPreferences: AutoConnectPreferences ): Promise; getLegacyConnections?(options?: { diff --git a/packages/connection-storage/src/in-memory-connection-storage.ts b/packages/connection-storage/src/in-memory-connection-storage.ts index 8e8835b2c54..49b618a5312 100644 --- a/packages/connection-storage/src/in-memory-connection-storage.ts +++ b/packages/connection-storage/src/in-memory-connection-storage.ts @@ -1,23 +1,10 @@ -import { EventEmitter } from 'events'; import { type ConnectionInfo } from '@mongodb-js/connection-info'; -import { - ConnectionStorageEvents, - type ConnectionStorage, -} from './connection-storage'; +import { type ConnectionStorage } from './connection-storage'; -export class InMemoryConnectionStorage - extends EventEmitter - implements ConnectionStorage -{ +export class InMemoryConnectionStorage implements ConnectionStorage { private connections: ConnectionInfo[]; - private legacyConnections: { name: string }[]; - constructor( - connections: ConnectionInfo[] = [], - legacyConnections: { name: string }[] = [] - ) { - super(); + constructor(connections: ConnectionInfo[] = []) { this.connections = [...connections]; - this.legacyConnections = [...legacyConnections]; } static async createAsync( @@ -54,7 +41,6 @@ export class InMemoryConnectionStorage save({ connectionInfo }: { connectionInfo: ConnectionInfo }): Promise { this._save({ connectionInfo }); - this.emit(ConnectionStorageEvents.ConnectionsChanged); return Promise.resolve(); } @@ -65,16 +51,11 @@ export class InMemoryConnectionStorage if (existingConnectionIdx !== -1) { this.connections.splice(existingConnectionIdx, 1); } - this.emit(ConnectionStorageEvents.ConnectionsChanged); return Promise.resolve(); } - getAutoConnectInfo(): Promise { - return Promise.resolve(undefined); - } - getLegacyConnections(): Promise<{ name: string }[]> { - return Promise.resolve(this.legacyConnections); + return Promise.resolve([]); } importConnections(): Promise { diff --git a/packages/connection-storage/src/provider.ts b/packages/connection-storage/src/provider.ts index 58f1eb88932..0cb29c87359 100644 --- a/packages/connection-storage/src/provider.ts +++ b/packages/connection-storage/src/provider.ts @@ -4,19 +4,12 @@ import { type ConnectionStorage, type ConnectionInfo, type AtlasClusterMetadata, - type AutoConnectPreferences, - ConnectionStorageEvents, } from './connection-storage'; import { InMemoryConnectionStorage } from './in-memory-connection-storage'; -export { ConnectionStorageEvents, InMemoryConnectionStorage }; +export { InMemoryConnectionStorage }; -export type { - ConnectionStorage, - ConnectionInfo, - AtlasClusterMetadata, - AutoConnectPreferences, -}; +export type { ConnectionStorage, ConnectionInfo, AtlasClusterMetadata }; export const ConnectionStorageContext = createContext( null diff --git a/packages/connection-storage/src/renderer.ts b/packages/connection-storage/src/renderer.ts index 2c0e80d7261..07da58764fb 100644 --- a/packages/connection-storage/src/renderer.ts +++ b/packages/connection-storage/src/renderer.ts @@ -3,4 +3,3 @@ export type { AtlasClusterMetadata, } from '@mongodb-js/connection-info'; export * from './compass-renderer-connection-storage'; -export type { AutoConnectPreferences } from './connection-storage'; diff --git a/packages/data-service/package.json b/packages/data-service/package.json index 1dcbba09622..b9c45a27aed 100644 --- a/packages/data-service/package.json +++ b/packages/data-service/package.json @@ -7,7 +7,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "22.22.3", + "version": "22.23.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -51,11 +51,12 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-utils": "^0.6.9", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-utils": "^0.6.10", "@mongodb-js/devtools-connect": "^3.2.5", - "@mongodb-js/ssh-tunnel": "^2.3.3", + "@mongodb-js/devtools-proxy-support": "^0.3.6", "bson": "^6.7.0", + "compass-preferences-model": "^2.27.0", "lodash": "^4.17.21", "mongodb": "^6.8.0", "mongodb-build-info": "^1.7.2", @@ -63,11 +64,11 @@ "mongodb-ns": "^2.4.2" }, "devDependencies": { - "@mongodb-js/compass-test-server": "^0.1.19", - "@mongodb-js/devtools-docker-test-envs": "^1.3.2", - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/oidc-plugin": "^1.0.0", + "@mongodb-js/compass-test-server": "^0.1.20", + "@mongodb-js/devtools-docker-test-envs": "^1.3.3", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", + "@mongodb-js/oidc-plugin": "^1.1.1", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/lodash": "^4.14.188", @@ -78,6 +79,7 @@ "eslint": "^7.25.0", "kerberos": "^2.1.1", "mocha": "^10.2.0", + "mongodb-log-writer": "^1.4.2", "nyc": "^15.1.0", "prettier": "^2.7.1", "sinon": "^9.2.3", @@ -85,6 +87,6 @@ "typescript": "^5.0.4" }, "optionalDependencies": { - "mongodb-client-encryption": "6.0.0" + "mongodb-client-encryption": "~6.0.1" } } diff --git a/packages/data-service/src/connect-mongo-client.spec.ts b/packages/data-service/src/connect-mongo-client.spec.ts index b44dd3c0e60..a61e930c03b 100644 --- a/packages/data-service/src/connect-mongo-client.spec.ts +++ b/packages/data-service/src/connect-mongo-client.spec.ts @@ -71,10 +71,12 @@ describe('connectMongoClient', function () { authMechanismProperties: {}, oidc: { allowedFlows: options.oidc?.allowedFlows, + customHttpOptions: options.oidc?.customHttpOptions, signal: undefined, }, autoEncryption: undefined, parentHandle: options.parentHandle, + applyProxyToOIDC: false, ...defaultOptions, }); expect(await (options.oidc?.allowedFlows as any)()).to.deep.equal([ @@ -118,9 +120,11 @@ describe('connectMongoClient', function () { authMechanismProperties: {}, oidc: { allowedFlows: options.oidc?.allowedFlows, + customHttpOptions: options.oidc?.customHttpOptions, signal: undefined, }, parentHandle: options.parentHandle, + applyProxyToOIDC: false, ...defaultOptions, }); expect(await (options.oidc?.allowedFlows as any)()).to.deep.equal([ @@ -153,10 +157,12 @@ describe('connectMongoClient', function () { authMechanismProperties: {}, oidc: { allowedFlows: options.oidc?.allowedFlows, + customHttpOptions: options.oidc?.customHttpOptions, signal: undefined, }, autoEncryption: undefined, parentHandle: options.parentHandle, + applyProxyToOIDC: false, ...defaultOptions, }); expect(await (options.oidc?.allowedFlows as any)()).to.deep.equal([ diff --git a/packages/data-service/src/connect-mongo-client.ts b/packages/data-service/src/connect-mongo-client.ts index 745c6b7f1c0..5a10c735225 100644 --- a/packages/data-service/src/connect-mongo-client.ts +++ b/packages/data-service/src/connect-mongo-client.ts @@ -5,7 +5,12 @@ import type { DevtoolsConnectOptions, DevtoolsConnectionState, } from '@mongodb-js/devtools-connect'; -import type SSHTunnel from '@mongodb-js/ssh-tunnel'; +import { + createSocks5Tunnel, + hookLogger as hookProxyLogger, + createAgent, +} from '@mongodb-js/devtools-proxy-support'; +import type { Tunnel } from '@mongodb-js/devtools-proxy-support'; import EventEmitter from 'events'; import ConnectionString from 'mongodb-connection-string-url'; import _ from 'lodash'; @@ -13,10 +18,10 @@ import _ from 'lodash'; import { redactConnectionOptions, redactConnectionString } from './redact'; import type { ConnectionOptions } from './connection-options'; import { - forceCloseTunnel, - openSshTunnel, + getCurrentApplicationProxyOptions, + getTunnelOptions, waitForTunnelError, -} from './ssh-tunnel'; +} from './ssh-tunnel-helpers'; import { runCommand } from './run-command'; import type { UnboundDataServiceImplLogger } from './logger'; import { debug as _debug } from './logger'; @@ -64,12 +69,21 @@ export function prepareOIDCOptions( connectionOptions: Readonly, signal?: AbortSignal, reauthenticationHandler?: ReauthenticationHandler -): Required> { +): Required< + Pick< + DevtoolsConnectOptions, + 'oidc' | 'authMechanismProperties' | 'applyProxyToOIDC' + > +> { const options: Required< - Pick + Pick< + DevtoolsConnectOptions, + 'oidc' | 'authMechanismProperties' | 'applyProxyToOIDC' + > > = { oidc: { ...connectionOptions.oidc }, authMechanismProperties: {}, + applyProxyToOIDC: false, }; const allowedFlows = connectionOptions.oidc?.allowedFlows ?? ['auth-code']; @@ -90,6 +104,14 @@ export function prepareOIDCOptions( matchingAllowedHosts(connectionOptions); } + if (connectionOptions.oidc?.shareProxyWithConnection) { + options.applyProxyToOIDC = true; + } else { + options.oidc.customHttpOptions = { + agent: createAgent(getCurrentApplicationProxyOptions()), + }; + } + options.oidc.signal = signal; return options; @@ -115,7 +137,7 @@ export async function connectMongoClientDataService({ [ metadataClient: CloneableMongoClient, crudClient: CloneableMongoClient, - sshTunnel: SSHTunnel | undefined, + tunnel: Tunnel | undefined, connectionState: DevtoolsConnectionState, options: { url: string; options: DevtoolsConnectOptions } ] @@ -166,17 +188,25 @@ export async function connectMongoClientDataService({ // // If connectionOptions.sshTunnel is not defined, the tunnel // will also be undefined. - const [tunnel, socks5Options] = await openSshTunnel( - connectionOptions.sshTunnel, - logger + const tunnel = createSocks5Tunnel( + getTunnelOptions(connectionOptions), + 'generate-credentials', + 'mongodb://' ); + // TODO: Not urgent, but it might be helpful to properly implement redaction + // and then actually log this to the log file, it's been helpful for debugging + // e2e tests for sure + // console.log({tunnel, tunnelOptions: getTunnelOptions(connectionOptions), connectionOptions, oidcOptions}) + if (tunnel && logger) + hookProxyLogger(tunnel.logger, logger, 'compass-tunnel'); const tunnelForwardingErrors: Error[] = []; tunnel?.on('forwardingError', (err: Error) => tunnelForwardingErrors.push(err) ); + await tunnel?.listen(); - if (socks5Options) { - Object.assign(options, socks5Options); + if (tunnel?.config) { + Object.assign(options, tunnel.config); } class CompassMongoClient extends MongoClient { constructor(url: string, options?: MongoClientOptions) { @@ -287,7 +317,7 @@ export async function connectMongoClientDataService({ debug('connection error', err); debug('force shutting down ssh tunnel ...'); await Promise.all([ - forceCloseTunnel(tunnel, logger), + tunnel?.close(), crudClient?.close(), metadataClient?.close(), ]).catch(() => { diff --git a/packages/data-service/src/connect.spec.ts b/packages/data-service/src/connect.spec.ts index 0243b6cb235..7c739823320 100644 --- a/packages/data-service/src/connect.spec.ts +++ b/packages/data-service/src/connect.spec.ts @@ -7,14 +7,18 @@ import ConnectionStringUrl from 'mongodb-connection-string-url'; import path from 'path'; import os from 'os'; import type { MongoClientOptions } from 'mongodb'; +import { UUID } from 'mongodb'; import connect from './connect'; import type { ConnectionOptions } from './connection-options'; import type DataService from './data-service'; import { redactConnectionOptions } from './redact'; import { runCommand } from './run-command'; +import { MongoLogWriter } from 'mongodb-log-writer'; const IS_CI = process.env.EVERGREEN_BUILD_VARIANT || process.env.CI === 'true'; +const SHOULD_DEBUG = + IS_CI || process.env.DEBUG?.includes('data-service-connect'); const SHOULD_RUN_DOCKER_TESTS = process.env.COMPASS_RUN_DOCKER_TESTS === 'true'; @@ -637,7 +641,12 @@ async function connectAndGetAuthInfo(connectionOptions: ConnectionOptions) { let dataService: DataService | undefined; try { - dataService = await connect({ connectionOptions }); + dataService = await connect({ + connectionOptions, + logger: SHOULD_DEBUG + ? new MongoLogWriter(new UUID().toHexString(), null, process.stderr) + : undefined, + }); const connectionStatus = await runCommand( dataService['_database']('admin', 'META'), { connectionStatus: 1 } diff --git a/packages/data-service/src/connection-options.ts b/packages/data-service/src/connection-options.ts index fda27ec5861..cc284f32fa5 100644 --- a/packages/data-service/src/connection-options.ts +++ b/packages/data-service/src/connection-options.ts @@ -10,6 +10,10 @@ export type OIDCOptions = Omit< // to match the connection string hosts, including possible SRV "sibling" domains. enableUntrustedEndpoints?: boolean; + // Set either devtools-connect's applyProxyToOIDC flag or create a custom Agent + // based on application-level HTTP settings. Defaults to 'false', i.e. app-level proxying. + shareProxyWithConnection?: boolean; + allowedFlows?: ExtractArrayEntryType< NonNullable['allowedFlows'] >[]; @@ -26,6 +30,12 @@ export interface ConnectionOptions { */ sshTunnel?: ConnectionSshOptions; + /** + * Alternative to Socks5 proxying / SSH tunnel: If set, inherit Compass's application-level + * proxy settings. + */ + useApplicationLevelProxy?: boolean; + /** * If present the connection should use OIDC authentication. */ diff --git a/packages/data-service/src/data-service.ts b/packages/data-service/src/data-service.ts index b124a171855..6d733295d62 100644 --- a/packages/data-service/src/data-service.ts +++ b/packages/data-service/src/data-service.ts @@ -1,4 +1,4 @@ -import type SshTunnel from '@mongodb-js/ssh-tunnel'; +import type { Tunnel } from '@mongodb-js/devtools-proxy-support'; import { EventEmitter } from 'events'; import { ExplainVerbosity, ClientEncryption } from 'mongodb'; import type { @@ -942,7 +942,7 @@ class DataServiceImpl extends WithLogContext implements DataService { private _useCRUDClient = true; private _csfleCollectionTracker?: CSFLECollectionTracker; - private _tunnel?: SshTunnel; + private _tunnel?: Tunnel; private _state?: DevtoolsConnectionState; private _reauthenticationHandlers = new Set(); @@ -1033,7 +1033,8 @@ class DataServiceImpl extends WithLogContext implements DataService { this._mongoClientConnectionOptions, 'options.oidc.notifyDeviceFlow', 'options.oidc.signal', - 'options.oidc.allowedFlows' + 'options.oidc.allowedFlows', + 'options.oidc.customHttpOptions.agent' ); } diff --git a/packages/data-service/src/index.ts b/packages/data-service/src/index.ts index 86c5667f336..12d41777988 100644 --- a/packages/data-service/src/index.ts +++ b/packages/data-service/src/index.ts @@ -29,3 +29,4 @@ export type { SearchIndex, SearchIndexStatus, } from './search-index-detail-helper'; +export type { InstanceDetails } from './instance-detail-helper'; diff --git a/packages/data-service/src/redact.spec.ts b/packages/data-service/src/redact.spec.ts deleted file mode 100644 index 555fdd05e01..00000000000 --- a/packages/data-service/src/redact.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable mocha/no-mocha-arrows */ -import type { SshTunnelConfig } from '@mongodb-js/ssh-tunnel'; -import assert from 'assert'; -import { redactSshTunnelOptions } from './redact'; - -describe('redact', () => { - describe('redactSshTunnelOptions', function () { - let baseOptions: Partial; - - beforeEach(function () { - baseOptions = { - readyTimeout: 10, - keepaliveInterval: 10, - srcAddr: 'srcAddr', - dstPort: 22, - dstAddr: 'dstAddr', - localPort: 22222, - localAddr: 'localAddr', - host: 'host', - port: 27017, - username: 'username', - }; - }); - - // eslint-disable-next-line mocha/no-setup-in-describe - ['password', 'privateKey', 'passphrase'].forEach((key) => { - it(`redacts '${key}'`, function () { - assert.deepStrictEqual( - redactSshTunnelOptions({ - ...baseOptions, - [key]: 'secret', - } as SshTunnelConfig), - { - ...baseOptions, - [key]: '', - } - ); - }); - }); - }); -}); diff --git a/packages/data-service/src/redact.ts b/packages/data-service/src/redact.ts index 140e31f29d9..12b4b613a3f 100644 --- a/packages/data-service/src/redact.ts +++ b/packages/data-service/src/redact.ts @@ -1,4 +1,3 @@ -import type { SshTunnelConfig } from '@mongodb-js/ssh-tunnel'; import type { ConnectionOptions } from './connection-options'; import { redactConnectionString } from 'mongodb-connection-string-url'; export { redactConnectionString }; @@ -29,23 +28,3 @@ export function redactConnectionOptions( return redacted; } - -export function redactSshTunnelOptions>( - options: T -): T { - const redacted = { ...options }; - - if (redacted.password) { - redacted.password = ''; - } - - if (redacted.privateKey) { - redacted.privateKey = ''; - } - - if (redacted.passphrase) { - redacted.passphrase = ''; - } - - return redacted; -} diff --git a/packages/data-service/src/ssh-tunnel-helpers.ts b/packages/data-service/src/ssh-tunnel-helpers.ts new file mode 100644 index 00000000000..1b688f4190d --- /dev/null +++ b/packages/data-service/src/ssh-tunnel-helpers.ts @@ -0,0 +1,52 @@ +import type { ConnectionOptions } from './connection-options'; +import type { + DevtoolsProxyOptions, + Tunnel, +} from '@mongodb-js/devtools-proxy-support'; +import { + defaultPreferencesInstance, + proxyPreferenceToProxyOptions, +} from 'compass-preferences-model'; + +export async function waitForTunnelError( + tunnel: Tunnel | undefined +): Promise { + return new Promise((_, reject) => { + tunnel?.on('error', reject); + }); +} + +export function getCurrentApplicationProxyOptions() { + return proxyPreferenceToProxyOptions( + defaultPreferencesInstance.getPreferences().proxy + ); +} + +export function getTunnelOptions( + connectionOptions: ConnectionOptions +): DevtoolsProxyOptions { + if (connectionOptions.useApplicationLevelProxy) { + return getCurrentApplicationProxyOptions(); + } + if (connectionOptions.sshTunnel) { + const { + host, + port, + username, + password, + identityKeyFile, + identityKeyPassphrase, + } = connectionOptions.sshTunnel; + return { + proxy: `ssh://${ + username + ? encodeURIComponent(username) + + (password ? ':' + encodeURIComponent(password) : '') + + '@' + : '' + }${encodeURIComponent(host)}:${encodeURIComponent(+port || 22)}`, + sshOptions: { identityKeyFile, identityKeyPassphrase }, + }; + } + return {}; +} diff --git a/packages/data-service/src/ssh-tunnel.ts b/packages/data-service/src/ssh-tunnel.ts deleted file mode 100644 index 5c165068ffc..00000000000 --- a/packages/data-service/src/ssh-tunnel.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { EventEmitter, once } from 'events'; -import fs from 'fs'; -import crypto from 'crypto'; -import { promisify } from 'util'; -import SSHTunnel from '@mongodb-js/ssh-tunnel'; -import type { MongoClientOptions } from 'mongodb'; -import type { ConnectionSshOptions } from './connection-options'; -import { redactSshTunnelOptions } from './redact'; -import type { UnboundDataServiceImplLogger } from './logger'; -import { debug as _debug, mongoLogId } from './logger'; - -const debug = _debug.extend('ssh-tunnel'); - -const randomBytes = promisify(crypto.randomBytes); - -type Socks5Options = Pick< - MongoClientOptions, - 'proxyHost' | 'proxyPort' | 'proxyUsername' | 'proxyPassword' ->; - -export async function openSshTunnel( - sshTunnelOptions: ConnectionSshOptions | undefined, - logger?: UnboundDataServiceImplLogger -): Promise<[SSHTunnel | undefined, Socks5Options | undefined]> { - if (!sshTunnelOptions) { - return [undefined, undefined]; - } - - const credentialsSource = await randomBytes(64); - const socks5Username = credentialsSource.slice(0, 32).toString('base64'); - const socks5Password = credentialsSource.slice(32).toString('base64'); - - const tunnelConstructorOptions = { - readyTimeout: 20000, - forwardTimeout: 20000, - keepaliveInterval: 20000, - localPort: 0, // let the OS pick a port - localAddr: '127.0.0.1', - socks5Username: socks5Username, - socks5Password: socks5Password, - host: sshTunnelOptions.host, - port: sshTunnelOptions.port, - username: sshTunnelOptions.username, - password: sshTunnelOptions.password, - privateKey: sshTunnelOptions.identityKeyFile - ? await fs.promises.readFile(sshTunnelOptions.identityKeyFile) - : undefined, - passphrase: sshTunnelOptions.identityKeyPassphrase, - }; - - const redactedTunnelOptions = redactSshTunnelOptions( - tunnelConstructorOptions - ); - - logger?.info( - 'COMPASS-DATA-SERVICE', - mongoLogId(1_001_000_006), - 'SSHTunnel', - 'Creating SSH tunnel', - redactedTunnelOptions - ); - - debug('creating ssh tunnel with options', redactedTunnelOptions); - - const tunnel = new SSHTunnel(tunnelConstructorOptions); - - debug('ssh tunnel listen ...'); - await tunnel.listen(); - debug('ssh tunnel opened'); - - logger?.info( - 'COMPASS-DATA-SERVICE', - mongoLogId(1_001_000_007), - 'SSHTunnel', - 'SSH tunnel opened' - ); - - return [ - tunnel, - { - proxyHost: 'localhost', - proxyPort: tunnel.config.localPort, - proxyUsername: socks5Username, - proxyPassword: socks5Password, - }, - ]; -} - -export async function forceCloseTunnel( - tunnelToClose?: SSHTunnel | void, - logger?: UnboundDataServiceImplLogger -): Promise { - if (tunnelToClose) { - logger?.info( - 'COMPASS-DATA-SERVICE', - mongoLogId(1_001_000_008), - 'SSHTunnel', - 'Closing SSH tunnel' - ); - try { - await tunnelToClose.close(); - debug('ssh tunnel stopped'); - } catch (err) { - debug('ssh tunnel stopped with error', err); - } - } -} - -export async function waitForTunnelError( - tunnel: SSHTunnel | void -): Promise { - const [error] = await once(tunnel || new EventEmitter(), 'error'); - throw error; -} diff --git a/packages/database-model/package.json b/packages/database-model/package.json index 8c9c964e508..cd31b334b08 100644 --- a/packages/database-model/package.json +++ b/packages/database-model/package.json @@ -2,7 +2,7 @@ "name": "mongodb-database-model", "description": "MongoDB database model", "author": "Lucas Hrabovsky ", - "version": "2.22.3", + "version": "2.23.0", "bugs": { "url": "https://jira.mongodb.org/projects/COMPASS/issues", "email": "compass@mongodb.com" @@ -30,11 +30,11 @@ "dependencies": { "ampersand-collection": "^2.0.2", "ampersand-model": "^8.0.1", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", diff --git a/packages/databases-collections-list/package.json b/packages/databases-collections-list/package.json index e86a297842b..88b1f4e8260 100644 --- a/packages/databases-collections-list/package.json +++ b/packages/databases-collections-list/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.35.0", + "version": "1.36.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -48,18 +48,18 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/connection-info": "^0.5.3", - "compass-preferences-model": "^2.26.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/connection-info": "^0.6.0", + "compass-preferences-model": "^2.27.0", "mongodb-ns": "^2.4.2", "react": "^17.0.2" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/databases-collections-list/src/items-grid.tsx b/packages/databases-collections-list/src/items-grid.tsx index 3ab1371d7b0..12cb9d1cee6 100644 --- a/packages/databases-collections-list/src/items-grid.tsx +++ b/packages/databases-collections-list/src/items-grid.tsx @@ -160,12 +160,12 @@ const GridControls: React.FunctionComponent<{ openShellWorkspace, } = useOpenWorkspace(); const track = useTelemetry(); - const { enableShell, enableNewMultipleConnectionSystem } = usePreferences([ + const { enableShell, enableMultipleConnectionSystem } = usePreferences([ 'enableShell', - 'enableNewMultipleConnectionSystem', + 'enableMultipleConnectionSystem', ]); - const showOpenShellButton = enableShell && enableNewMultipleConnectionSystem; + const showOpenShellButton = enableShell && enableMultipleConnectionSystem; const breadcrumbs = useMemo(() => { const { database } = toNS(namespace ?? ''); diff --git a/packages/databases-collections/package.json b/packages/databases-collections/package.json index f170f33d265..6f810621c0b 100644 --- a/packages/databases-collections/package.json +++ b/packages/databases-collections/package.json @@ -2,7 +2,7 @@ "name": "@mongodb-js/compass-databases-collections", "description": "Plugin for viewing the list of, creating, and dropping databases and collections", "private": true, - "version": "1.37.0", + "version": "1.38.0", "license": "SSPL", "homepage": "https://github.com/mongodb-js/compass", "bugs": { @@ -42,8 +42,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", @@ -60,21 +60,21 @@ "typescript": "^5.0.4" }, "dependencies": { - "@mongodb-js/compass-app-stores": "^7.24.0", - "@mongodb-js/compass-components": "^1.29.0", - "@mongodb-js/compass-connections": "^1.38.0", - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-logging": "^1.4.3", - "@mongodb-js/compass-telemetry": "^1.1.3", - "@mongodb-js/compass-workspaces": "^0.19.0", - "@mongodb-js/databases-collections-list": "^1.35.0", - "@mongodb-js/my-queries-storage": "^0.15.0", - "compass-preferences-model": "^2.26.0", - "hadron-app-registry": "^9.2.2", + "@mongodb-js/compass-app-stores": "^7.25.0", + "@mongodb-js/compass-components": "^1.29.1", + "@mongodb-js/compass-connections": "^1.39.0", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-logging": "^1.4.4", + "@mongodb-js/compass-telemetry": "^1.1.4", + "@mongodb-js/compass-workspaces": "^0.20.0", + "@mongodb-js/databases-collections-list": "^1.36.0", + "@mongodb-js/my-queries-storage": "^0.15.1", + "compass-preferences-model": "^2.27.0", + "hadron-app-registry": "^9.2.3", "lodash": "^4.17.21", - "mongodb-collection-model": "^5.22.3", - "mongodb-database-model": "^2.22.3", - "mongodb-instance-model": "^12.23.3", + "mongodb-collection-model": "^5.23.0", + "mongodb-database-model": "^2.23.0", + "mongodb-instance-model": "^12.24.0", "mongodb-ns": "^2.4.2", "mongodb-query-parser": "^4.2.0", "prop-types": "^15.7.2", diff --git a/packages/databases-collections/src/stores/create-namespace.spec.tsx b/packages/databases-collections/src/stores/create-namespace.spec.tsx index d2ab46278e2..12915fae656 100644 --- a/packages/databases-collections/src/stores/create-namespace.spec.tsx +++ b/packages/databases-collections/src/stores/create-namespace.spec.tsx @@ -1,50 +1,30 @@ import React from 'react'; import Sinon from 'sinon'; import { CreateNamespacePlugin } from '../index'; -import AppRegistry from 'hadron-app-registry'; -import { - render, - cleanup, - screen, - waitForElementToBeRemoved, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import type AppRegistry from 'hadron-app-registry'; import { expect } from 'chai'; -import { - ConnectionsManager, - type DataService, -} from '@mongodb-js/compass-connections/provider'; -import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; +import { type DataService } from '@mongodb-js/compass-connections/provider'; import { type MongoDBInstance, TestMongoDBInstanceManager, } from '@mongodb-js/compass-app-stores/provider'; +import { + renderWithConnections, + cleanup, + screen, + waitForElementToBeRemoved, + userEvent, + createDefaultConnectionInfo, + waitFor, +} from '@mongodb-js/compass-connections/test'; + +const mockConnections = [ + { ...createDefaultConnectionInfo(), id: '1' }, + { ...createDefaultConnectionInfo(), id: '2' }, +]; describe('CreateNamespacePlugin', function () { const sandbox = Sinon.createSandbox(); - const appRegistry = sandbox.spy(new AppRegistry()); - const dataService1 = { - createCollection() { - return Promise.resolve({}); - }, - createDataKey() { - return Promise.resolve({}); - }, - configuredKMSProviders() { - return Promise.resolve([]); - }, - } as unknown as DataService; - const dataService2 = { - createCollection() { - return Promise.resolve({}); - }, - createDataKey() { - return Promise.resolve({}); - }, - configuredKMSProviders() { - return Promise.resolve([]); - }, - } as unknown as DataService; const instance1 = { on: sandbox.stub(), off: sandbox.stub(), @@ -62,22 +42,10 @@ describe('CreateNamespacePlugin', function () { const workspaces = { openCollectionWorkspace() {}, }; + let appRegistry: AppRegistry; + let getDataService: (id: string) => DataService; - beforeEach(function () { - const connectionsManager = new ConnectionsManager({ - logger: createNoopLogger().log.unbound, - }); - sandbox - .stub(connectionsManager, 'getDataServiceForConnection') - .callsFake((id) => { - if (id === '1') { - return dataService1; - } else if (id === '2') { - return dataService2; - } - throw new Error('unknown id provided'); - }); - + beforeEach(async function () { const instancesManager = new TestMongoDBInstanceManager(); sandbox .stub(instancesManager, 'getMongoDBInstanceForConnection') @@ -90,12 +58,30 @@ describe('CreateNamespacePlugin', function () { }) as () => MongoDBInstance); const Plugin = CreateNamespacePlugin.withMockServices({ - globalAppRegistry: appRegistry, - connectionsManager, instancesManager, workspaces: workspaces as any, }); - render(); + const result = renderWithConnections(, { + connections: mockConnections, + connectFn() { + return { + createCollection() { + return Promise.resolve({} as any); + }, + createDataKey() { + return Promise.resolve({}); + }, + configuredKMSProviders() { + return []; + }, + }; + }, + }); + appRegistry = result.globalAppRegistry; + getDataService = result.getDataServiceForConnection; + for (const connectionInfo of mockConnections) { + await result.connectionsStore.actions.connect(connectionInfo); + } }); afterEach(function () { @@ -105,9 +91,14 @@ describe('CreateNamespacePlugin', function () { }); it('should dismiss the modal not do anything when modal is dismissed', async function () { - const createCollectionSpy = sandbox.spy(dataService1, 'createCollection'); + const createCollectionSpy = sandbox.spy( + getDataService('1'), + 'createCollection' + ); appRegistry.emit('open-create-database', { connectionId: '1' }); - expect(screen.getByRole('heading', { name: 'Create Database' })).to.exist; + await waitFor(() => { + expect(screen.getByRole('heading', { name: 'Create Database' })).to.exist; + }); userEvent.click(screen.getByRole('button', { name: 'Cancel' })); await waitForElementToBeRemoved( @@ -126,7 +117,10 @@ describe('CreateNamespacePlugin', function () { it('should handle create database flow on `open-create-database` event', async function () { const emitSpy = sandbox.spy(appRegistry, 'emit'); - const createCollectionSpy = sandbox.spy(dataService1, 'createCollection'); + const createCollectionSpy = sandbox.spy( + getDataService('1'), + 'createCollection' + ); const openCollectionWorkspaceSpy = sandbox.spy( workspaces, 'openCollectionWorkspace' @@ -178,7 +172,10 @@ describe('CreateNamespacePlugin', function () { it('should handle create collection flow on `open-create-collection` event', async function () { const emitSpy = sandbox.spy(appRegistry, 'emit'); - const createCollectionSpy = sandbox.spy(dataService2, 'createCollection'); + const createCollectionSpy = sandbox.spy( + getDataService('2'), + 'createCollection' + ); const openCollectionWorkspaceSpy = sandbox.spy( workspaces, 'openCollectionWorkspace' diff --git a/packages/databases-collections/src/stores/create-namespace.ts b/packages/databases-collections/src/stores/create-namespace.ts index 1fbbece17b9..45193fe971f 100644 --- a/packages/databases-collections/src/stores/create-namespace.ts +++ b/packages/databases-collections/src/stores/create-namespace.ts @@ -1,8 +1,6 @@ import type AppRegistry from 'hadron-app-registry'; import { - ConnectionsManagerEvents, type ConnectionsManager, - type DataService, type ConnectionRepositoryAccess, } from '@mongodb-js/compass-connections/provider'; import type { MongoDBInstance } from 'mongodb-instance-model'; @@ -87,13 +85,9 @@ export function activatePlugin( }); }; - const onDataServiceProvided = ( - connectionId: string, - dataService: Pick< - DataService, - 'createCollection' | 'createDataKey' | 'configuredKMSProviders' - > - ) => { + const onDataServiceProvided = (connectionId: string) => { + const dataService = + connectionsManager.getDataServiceForConnection(connectionId); store.dispatch( kmsProvidersRetrieved(connectionId, dataService.configuredKMSProviders()) ); @@ -103,16 +97,8 @@ export function activatePlugin( connectionId, instance, ] of instancesManager.listMongoDBInstances()) { - const dataService = - connectionsManager.getDataServiceForConnection(connectionId); onInstanceProvided(connectionId, instance); - onDataServiceProvided( - connectionId, - dataService as Pick< - DataService, - 'createCollection' | 'createDataKey' | 'configuredKMSProviders' - > - ); + onDataServiceProvided(connectionId); } on( @@ -120,11 +106,7 @@ export function activatePlugin( MongoDBInstancesManagerEvents.InstanceCreated, onInstanceProvided ); - on( - connectionsManager, - ConnectionsManagerEvents.ConnectionAttemptSuccessful, - onDataServiceProvided - ); + on(connectionsManager, 'connected', onDataServiceProvided); on( globalAppRegistry, diff --git a/packages/databases-collections/src/stores/drop-namespace.spec.tsx b/packages/databases-collections/src/stores/drop-namespace.spec.tsx index f9b51d56014..d8a9207f403 100644 --- a/packages/databases-collections/src/stores/drop-namespace.spec.tsx +++ b/packages/databases-collections/src/stores/drop-namespace.spec.tsx @@ -1,31 +1,48 @@ import React from 'react'; import Sinon from 'sinon'; import { DropNamespacePlugin } from '../index'; -import AppRegistry from 'hadron-app-registry'; +import type AppRegistry from 'hadron-app-registry'; import toNS from 'mongodb-ns'; -import { render, cleanup, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { expect } from 'chai'; -import { createNoopTrack } from '@mongodb-js/compass-telemetry/provider'; +import { + renderWithConnections, + cleanup, + screen, + waitFor, + userEvent, + createDefaultConnectionInfo, +} from '@mongodb-js/compass-connections/test'; +import type { DataService } from '@mongodb-js/compass-connections/provider'; + +const mockConnectionInfo = createDefaultConnectionInfo(); describe('DropNamespacePlugin', function () { const sandbox = Sinon.createSandbox(); - const appRegistry = sandbox.spy(new AppRegistry()); - const dataService = { - dropDatabase: sandbox.stub().resolves(true), - dropCollection: sandbox.stub().resolves(true), - }; - const connectionsManager = { - getDataServiceForConnection: sandbox.stub().returns(dataService), - } as any; - - beforeEach(function () { - const Plugin = DropNamespacePlugin.withMockServices({ - globalAppRegistry: appRegistry, - connectionsManager, - track: createNoopTrack(), - }); - render(); + let appRegistry: Sinon.SinonSpiedInstance; + let dataService: Sinon.SinonSpiedInstance; + + beforeEach(async function () { + const result = renderWithConnections( + , + { + connections: [mockConnectionInfo], + connectFn() { + return { + dropDatabase() { + return Promise.resolve(true); + }, + dropCollection() { + return Promise.resolve(true); + }, + }; + }, + } + ); + await result.connectionsStore.actions.connect(mockConnectionInfo); + appRegistry = sandbox.spy(result.globalAppRegistry); + dataService = sandbox.spy( + result.getDataServiceForConnection(mockConnectionInfo.id) + ); }); afterEach(function () { @@ -35,7 +52,7 @@ describe('DropNamespacePlugin', function () { it('should ask for confirmation and delete collection on `open-drop-collection` event', async function () { appRegistry.emit('open-drop-collection', toNS('test.to-drop'), { - connectionId: 'TEST', + connectionId: mockConnectionInfo.id, }); expect( @@ -66,13 +83,13 @@ describe('DropNamespacePlugin', function () { expect(appRegistry.emit).to.have.been.calledWithExactly( 'collection-dropped', 'test.to-drop', - { connectionId: 'TEST' } + { connectionId: mockConnectionInfo.id } ); }); it('should ask for confirmation and delete database on `open-drop-database` event', async function () { appRegistry.emit('open-drop-database', 'db-to-drop', { - connectionId: 'TEST', + connectionId: mockConnectionInfo.id, }); expect( @@ -101,7 +118,7 @@ describe('DropNamespacePlugin', function () { expect(appRegistry.emit).to.have.been.calledWithExactly( 'database-dropped', 'db-to-drop', - { connectionId: 'TEST' } + { connectionId: mockConnectionInfo.id } ); }); }); diff --git a/packages/explain-plan-helper/package.json b/packages/explain-plan-helper/package.json index 2de69f655b8..400ec36f6d8 100644 --- a/packages/explain-plan-helper/package.json +++ b/packages/explain-plan-helper/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.1.15", + "version": "1.2.0", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -50,11 +50,11 @@ }, "dependencies": { "@mongodb-js/shell-bson-parser": "^1.1.0", - "mongodb-explain-compat": "^3.0.4" + "mongodb-explain-compat": "^3.1.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/hadron-app-registry/package.json b/packages/hadron-app-registry/package.json index a003d818a43..43f3720d087 100644 --- a/packages/hadron-app-registry/package.json +++ b/packages/hadron-app-registry/package.json @@ -7,7 +7,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "9.2.2", + "version": "9.2.3", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -50,8 +50,8 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@testing-library/react": "^12.1.5", diff --git a/packages/hadron-app-registry/src/app-registry.ts b/packages/hadron-app-registry/src/app-registry.ts index 72e84ac7c2d..e3313fb500d 100644 --- a/packages/hadron-app-registry/src/app-registry.ts +++ b/packages/hadron-app-registry/src/app-registry.ts @@ -1,9 +1,12 @@ -import type { Store as RefluxStore } from 'reflux'; import type { Store as ReduxStore } from 'redux'; import EventEmitter from 'eventemitter3'; import type { ReactReduxContext } from 'react-redux'; -export type Store = (ReduxStore | Partial) & { +// This type is very generic on purpose so that registerPlugin function generic +// type can derive it automatically based on the passed activate function. This +// is helpful when using useActivate hook to get the stricter type of returned +// activate store elsewhere in the code +export type Store = any & { onActivated?: (appRegistry: AppRegistry) => void; }; diff --git a/packages/hadron-app-registry/src/index.ts b/packages/hadron-app-registry/src/index.ts index d57268e8c3b..3dbc0835869 100644 --- a/packages/hadron-app-registry/src/index.ts +++ b/packages/hadron-app-registry/src/index.ts @@ -17,4 +17,5 @@ export { createServiceLocator, createServiceProvider, } from './register-plugin'; +export type { Plugin as HadronPlugin } from './app-registry'; export default AppRegistry; diff --git a/packages/hadron-app-registry/src/react-context.tsx b/packages/hadron-app-registry/src/react-context.tsx index 4ebaead76ba..d18cda6e14a 100644 --- a/packages/hadron-app-registry/src/react-context.tsx +++ b/packages/hadron-app-registry/src/react-context.tsx @@ -65,6 +65,10 @@ export function GlobalAppRegistryProvider({ ); } +export function useIsTopLevelProvider() { + return useContext(LocalAppRegistryContext) === null; +} + export function AppRegistryProvider({ children, ...props @@ -76,7 +80,7 @@ export function AppRegistryProvider({ } = initialPropsRef.current; const globalAppRegistry = useGlobalAppRegistry(); - const isTopLevelProvider = useContext(LocalAppRegistryContext) === null; + const isTopLevelProvider = useIsTopLevelProvider(); const [localAppRegistry] = useState(() => { return ( initialLocalAppRegistry ?? diff --git a/packages/hadron-app-registry/src/register-plugin.tsx b/packages/hadron-app-registry/src/register-plugin.tsx index 01ef66df12f..5810b768769 100644 --- a/packages/hadron-app-registry/src/register-plugin.tsx +++ b/packages/hadron-app-registry/src/register-plugin.tsx @@ -8,6 +8,7 @@ import { AppRegistryProvider, useGlobalAppRegistry, useLocalAppRegistry, + useIsTopLevelProvider, } from './react-context'; class ActivateHelpersImpl { @@ -126,7 +127,11 @@ type Services unknown>> = { [SvcName in keyof S]: ReturnType; }; -export type HadronPluginConfig unknown>> = { +export type HadronPluginConfig< + T, + S extends Record unknown>, + A extends Plugin +> = { name: string; component: React.ComponentType; /** @@ -139,7 +144,7 @@ export type HadronPluginConfig unknown>> = { initialProps: T, services: Registries & Services, helpers: ActivateHelpers - ) => Plugin; + ) => A; }; type MockOptions = { @@ -149,17 +154,20 @@ type MockOptions = { disableChildPluginRendering: boolean; }; -const defaultMockOptions = { +const DEFAULT_MOCK_OPTIONS = { pluginName: '$$root', mockedEnvironment: false, mockServices: {}, disableChildPluginRendering: false, }; -const MockOptionsContext = React.createContext(defaultMockOptions); +const MockOptionsContext = React.createContext(null); -const useMockOption = (key: T): MockOptions[T] => { - return useContext(MockOptionsContext)[key]; +const useMockOption = ( + key: T, + defaultMockOptions = DEFAULT_MOCK_OPTIONS +): MockOptions[T] => { + return useContext(MockOptionsContext)?.[key] ?? defaultMockOptions[key]; }; let serviceLocationInProgress = false; @@ -218,14 +226,19 @@ function isServiceLocator(val: any): boolean { return Object.prototype.hasOwnProperty.call(val, kLocator); } -function useHadronPluginActivate unknown>>( - config: HadronPluginConfig, +function useHadronPluginActivate< + T, + S extends Record unknown>, + A extends Plugin +>( + config: HadronPluginConfig, services: S | undefined, - props: T + props: T, + mockOptions?: MockOptions ) { const registryName = `${config.name}.Plugin`; - const isMockedEnvironment = useMockOption('mockedEnvironment'); - const mockServices = useMockOption('mockServices'); + const isMockedEnvironment = useMockOption('mockedEnvironment', mockOptions); + const mockServices = useMockOption('mockServices', mockOptions); const globalAppRegistry = useGlobalAppRegistry(); const localAppRegistry = useLocalAppRegistry(); @@ -295,7 +308,8 @@ function useHadronPluginActivate unknown>>( export type HadronPluginComponent< T, - S extends Record unknown> + S extends Record unknown>, + A extends Plugin > = React.FunctionComponent & { displayName: string; @@ -317,7 +331,7 @@ export type HadronPluginComponent< * return (pluginVisible && ) * } */ - useActivate(props: T): void; + useActivate(props: T): A; /** * Convenience method for testing: allows to override services and app @@ -337,7 +351,7 @@ export type HadronPluginComponent< withMockServices( mocks: Partial>, options?: Partial> - ): React.FunctionComponent; + ): HadronPluginComponent; }; /** @@ -395,8 +409,12 @@ export type HadronPluginComponent< */ export function registerHadronPlugin< T, - S extends Record unknown> ->(config: HadronPluginConfig, services?: S): HadronPluginComponent { + S extends Record unknown>, + A extends Plugin +>( + config: HadronPluginConfig, + services?: S +): HadronPluginComponent { const Component = config.component; const Plugin = (props: React.PropsWithChildren) => { const isMockedEnvironment = useMockOption('mockedEnvironment'); @@ -441,13 +459,13 @@ export function registerHadronPlugin< }; return Object.assign(Plugin, { displayName: config.name, - useActivate: (props: T) => { - useHadronPluginActivate(config, services, props); + useActivate: (props: T): A => { + return useHadronPluginActivate(config, services, props) as A; }, withMockServices( mocks: Partial> = {}, options?: Partial> - ): React.FunctionComponent { + ): HadronPluginComponent { const { // In case globalAppRegistry mock is not provided, we use the one // created in scope so that plugins don't leak their events and @@ -461,23 +479,51 @@ export function registerHadronPlugin< // These services will be passed to the plugin `activate` method second // argument const mockOptions = { - ...defaultMockOptions, + ...DEFAULT_MOCK_OPTIONS, mockedEnvironment: true, pluginName: config.name, mockServices, ...options, }; - return function MockPluginWithContext(props: T) { + function MockPluginWithContext(props: T) { + const isTopLevelProvider = useIsTopLevelProvider(); + const hasCustomAppRegistries = + !!mockServices.localAppRegistry || !!mockServices.globalAppRegistry; + // Only render these providers if there are no providers for app + // registry set up higher in the rendering tree or if user explicitly + // passed some mocks here. In other cases we're probably be interferring + // with some custom providers setup by the test code + const shouldRenderRegistryProviders = + isTopLevelProvider || hasCustomAppRegistries; + return ( - - - - - + {shouldRenderRegistryProviders ? ( + + + + + + ) : ( + + )} ); - }; + } + return Object.assign(MockPluginWithContext, { + displayName: config.name, + useActivate: (props: T): A => { + return useHadronPluginActivate( + config, + services, + props, + mockOptions + ) as A; + }, + withMockServices() { + return MockPluginWithContext as any; + }, + }); }, }); } diff --git a/packages/hadron-build/cli.js b/packages/hadron-build/cli.js index f05b8f627b7..b2018ad4cbc 100755 --- a/packages/hadron-build/cli.js +++ b/packages/hadron-build/cli.js @@ -11,7 +11,6 @@ const yargs = require('yargs') .command(require('./commands/info')) .command(require('./commands/upload')) .command(require('./commands/download')) - .command(require('./commands/verify')) .demand(1, 'Please specify a command.') .strict() .env() diff --git a/packages/hadron-build/commands/info.js b/packages/hadron-build/commands/info.js index 3ff2109e25d..7465ca13519 100644 --- a/packages/hadron-build/commands/info.js +++ b/packages/hadron-build/commands/info.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); const Target = require('../lib/target'); -const verifyDistro = require('../lib/distro'); const Table = require('cli-table'); const yaml = require('js-yaml'); const inspect = require('util').inspect; @@ -74,8 +73,6 @@ const toTable = (target) => { }; exports.handler = (argv) => { - verifyDistro(argv); - let target = new Target(argv.dir, { version: argv.version, platform: argv.platform, diff --git a/packages/hadron-build/commands/release.js b/packages/hadron-build/commands/release.js index 2a317b0991f..97233bde742 100644 --- a/packages/hadron-build/commands/release.js +++ b/packages/hadron-build/commands/release.js @@ -15,7 +15,6 @@ */ const Target = require('../lib/target'); -const verifyDistro = require('../lib/distro'); const cli = require('mongodb-js-cli')('hadron-build:release'); const util = require('util'); const format = util.format; @@ -30,8 +29,6 @@ const run = require('./../lib/run'); const rebuild = require('@electron/rebuild').rebuild; const { signArchive } = require('./../lib/signtool'); -const verify = require('./verify'); - exports.command = 'release'; exports.describe = ':shipit:'; @@ -514,8 +511,6 @@ exports.builder = { } }; -_.assign(exports.builder, verify.builder); - /** * @param {any} argv Parsed command arguments @@ -525,8 +520,6 @@ _.assign(exports.builder, verify.builder); exports.run = async (argv, done) => { cli.argv = argv; - verifyDistro(argv); - const target = new Target(argv.dir); cli.debug(`Building distribution: ${target.distribution}`); @@ -551,7 +544,6 @@ exports.run = async (argv, done) => { const noAsar = process.env.NO_ASAR === 'true' || argv.no_asar; const tasks = _.flatten([ - () => verify.tasks(argv), task('copy npmrc from root', ({ dir }, done) => { fs.cp( path.resolve(dir, '..', '..', '.npmrc'), diff --git a/packages/hadron-build/commands/verify.js b/packages/hadron-build/commands/verify.js deleted file mode 100644 index 476569ea98f..00000000000 --- a/packages/hadron-build/commands/verify.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -/** - * Wouldn't it be great if you or a CI system were notified properly - * that you aren't using the right version of node.js or npm? - * - * @see https://github.com/atom/atom/blob/master/script/utils/verify-requirements.js - */ - -const semver = require('semver'); -const cli = require('mongodb-js-cli')('hadron-build:verify'); -const { promisify } = require('util'); -const run = require('./../lib/run'); -const runAsync = promisify(run); - -exports.command = 'verify [options]'; -exports.describe = 'Verify the current environment meets the app\'s requirements.'; - -exports.builder = { - nodejs_version: { - describe: 'What version of node.js is required for this app?', - default: '^7.4.0' - }, - npm_version: { - describe: 'What version of npm is required for this app?', - default: '^4.0.0' - } -}; - -exports.tasks = (argv) => { - return exports.checkNpmAndNodejsVersions(argv); -}; - -exports.handler = (argv) => { - exports.tasks(argv).catch((err) => cli.abortIfError(err)); -}; - -exports.checkNpmAndNodejsVersions = async(opts) => { - const expectNodeVersion = opts.nodejs_version; - const expectNpmVersion = opts.npm_version; - const args = ['version', '--json', '--loglevel', 'error']; - const stdout = await runAsync('npm', args, {env: process.env, shell: true}); - const versions = JSON.parse(stdout); - - if (!semver.satisfies(versions.node, expectNodeVersion)) { - return new Error(`Your current node.js (v${versions.node}) ` + - `does not satisfy the version required by this project (v${expectNodeVersion}).`); - } else if (!semver.satisfies(versions.npm, expectNpmVersion)) { - return new Error(`Your current npm (v${versions.npm}) ` + - `does not meet the requirement ${expectNpmVersion}.`); - } - - return versions; -}; diff --git a/packages/hadron-build/index.js b/packages/hadron-build/index.js index d811147af8d..e76cf08e3d4 100644 --- a/packages/hadron-build/index.js +++ b/packages/hadron-build/index.js @@ -4,6 +4,5 @@ exports = function() {}; exports.release = require('./commands/release'); exports.upload = require('./commands/upload'); exports.download = require('./commands/download'); -exports.verify = require('./commands/verify'); module.exports = exports; diff --git a/packages/hadron-build/lib/distro.js b/packages/hadron-build/lib/distro.js deleted file mode 100644 index e5196ccf89b..00000000000 --- a/packages/hadron-build/lib/distro.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; -/** - * Verify the distribution is passed as an argument. - * - * @param {Object} argv - The arguments. - */ -const verifyDistribution = (argv) => { - if (argv._ && argv._[1]) { - process.env.HADRON_DISTRIBUTION = argv._[1]; - } -}; - -module.exports = verifyDistribution; diff --git a/packages/hadron-build/lib/target.js b/packages/hadron-build/lib/target.js index fd5638b46db..e716adc35b8 100644 --- a/packages/hadron-build/lib/target.js +++ b/packages/hadron-build/lib/target.js @@ -101,12 +101,27 @@ class Target { this.pkg = pkg; const distributions = pkg.config.hadron.distributions; + const distribution = opts.distribution ?? process.env.HADRON_DISTRIBUTION; + + if (!distribution) { + throw new Error( + 'You need to explicitly set HADRON_DISTRIBUTION or pass `distribution` option to Target constructor before building Compass' + ); + } + + if (!supportedDistributions.includes(distribution)) { + throw new Error( + `Unknown distribution "${distribution}". Available distributions: ${supportedDistributions.join( + ', ' + )}` + ); + } _.defaults(opts, { version: process.env.HADRON_APP_VERSION }, pkg, { platform: process.platform, arch: process.arch, sign: true, - distribution: process.env.HADRON_DISTRIBUTION || distributions.default + distribution, }); this.distribution = opts.distribution; @@ -196,14 +211,7 @@ class Target { arch: this.arch, electronVersion: this.electronVersion, sign: null, - afterExtract: [(buildPath, electronVersion, platform, arch, done) => { - // TODO(https://github.com/electron/electron/issues/43076): electron - // releases are pointing to a wrong version of ffmpeg codecs right now - // (platform mismatch), there is a fix in progress and we should switch - // asap when it's available, for now just use the ffmpeg from an older - // version - ffmpegAfterExtract(buildPath, '29.4.3', platform, arch, done) - }] + afterExtract: [ffmpegAfterExtract] }; validateBuildConfig(this.platform, this.pkg.config.hadron.build[this.platform]); diff --git a/packages/hadron-build/package.json b/packages/hadron-build/package.json index 8eccbd65926..abf41f0557f 100644 --- a/packages/hadron-build/package.json +++ b/packages/hadron-build/package.json @@ -1,7 +1,7 @@ { "name": "hadron-build", "description": "Tooling for Hadron apps like Compass", - "version": "25.5.7", + "version": "25.5.8", "scripts": { "check": "npm run lint && npm run depcheck", "test": "mocha -R spec", @@ -32,7 +32,7 @@ "debug": "^4.3.4", "del": "^2.0.2", "download": "^8.0.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "electron-packager": "^15.5.1", "electron-packager-plugin-non-proprietary-codecs-ffmpeg": "^1.0.2", "flatnest": "^1.0.0", @@ -46,7 +46,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "mongodb-js-cli": "^0.0.3", - "node-abi": "^3.65.0", + "node-abi": "^3.67.0", "normalize-package-data": "^2.3.5", "parse-github-repo-url": "^1.3.0", "semver": "^7.6.2", @@ -57,7 +57,7 @@ "zip-folder": "^1.0.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "chai": "^4.2.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", diff --git a/packages/hadron-build/test/fixtures/hadron-app/package.json b/packages/hadron-build/test/fixtures/hadron-app/package.json index 2ebf3eda63c..ae56155f2c6 100644 --- a/packages/hadron-build/test/fixtures/hadron-app/package.json +++ b/packages/hadron-build/test/fixtures/hadron-app/package.json @@ -42,6 +42,9 @@ "productName": "MongoDB Compass Enterprise super long test name", "plugins": [], "styles": [ "index" ] + }, + "foo-bar": { + "name": "foo-bar" } }, "endpoint": "https://hadron-app.herokuapp.com", diff --git a/packages/hadron-build/test/index.test.js b/packages/hadron-build/test/index.test.js index 714d24f7928..14af4320d14 100644 --- a/packages/hadron-build/test/index.test.js +++ b/packages/hadron-build/test/index.test.js @@ -14,13 +14,6 @@ describe('hadron-build', () => { expect(hadronBuild).to.be.a('function'); }); - describe('::release', () => { - it('should include options from commands::verify', () => { - expect(commands.release.builder).to.have.property('nodejs_version'); - expect(commands.release.builder).to.have.property('npm_version'); - }); - }); - describe('::test', () => { const DEFAULT_ARGS = { _: [], @@ -84,23 +77,4 @@ describe('hadron-build', () => { it('should spawn electron-mocha'); }); }); - - describe('::verify', () => { - it('should have a `nodejs_version` option', () => { - expect(commands.verify.builder).to.have.property('nodejs_version'); - }); - - it('should have a `npm_version` option', () => { - expect(commands.verify.builder).to.have.property('npm_version'); - }); - - it('should use `engines.node` for the default `nodejs_version` option'); - - it('should use `engines.npm` for the default `npm_version` option'); - - describe('::handler', () => { - it('should check the environment\'s npm installation'); - it('should check the environment\'s node.js installation'); - }); - }); }); diff --git a/packages/hadron-build/test/target.test.js b/packages/hadron-build/test/target.test.js index 740c77f14d8..666d4cd0086 100644 --- a/packages/hadron-build/test/target.test.js +++ b/packages/hadron-build/test/target.test.js @@ -39,7 +39,7 @@ describe('target', () => { it('allows to override distribution config with env vars', () => { Object.assign(process.env, { - HADRON_DISTRIBUTION: 'my-custom-distribution', + HADRON_DISTRIBUTION: 'compass-isolated', HADRON_PRODUCT: 'compass-compass', HADRON_PRODUCT_NAME: 'MongoDB Compass My Awesome Edition', HADRON_READONLY: 'true', @@ -49,7 +49,7 @@ describe('target', () => { const target = new Target(path.join(__dirname, 'fixtures', 'hadron-app')); - expect(target).to.have.property('distribution', 'my-custom-distribution'); + expect(target).to.have.property('distribution', 'compass-isolated'); expect(target).to.have.property('name', 'compass-compass'); expect(target).to.have.property( 'productName', diff --git a/packages/hadron-document/package.json b/packages/hadron-document/package.json index 4a861c43d98..7dfcea594c6 100644 --- a/packages/hadron-document/package.json +++ b/packages/hadron-document/package.json @@ -7,7 +7,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "8.6.0", + "version": "8.6.1", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -52,8 +52,8 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "chai": "^4.2.0", diff --git a/packages/hadron-ipc/package.json b/packages/hadron-ipc/package.json index 2ddb0107baf..7cdcfe40c03 100644 --- a/packages/hadron-ipc/package.json +++ b/packages/hadron-ipc/package.json @@ -1,7 +1,7 @@ { "name": "hadron-ipc", "description": "Simplified IPC for electron apps.", - "version": "3.2.20", + "version": "3.2.21", "author": { "name": "MongoDB Inc", "email": "compass@mongodb.com" @@ -50,8 +50,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -69,7 +69,7 @@ }, "dependencies": { "debug": "^4.3.4", - "electron": "^29.4.5", + "electron": "^30.4.0", "is-electron-renderer": "^2.0.1" } } diff --git a/packages/instance-model/package.json b/packages/instance-model/package.json index 5da0e444e3e..d6022315efd 100644 --- a/packages/instance-model/package.json +++ b/packages/instance-model/package.json @@ -2,7 +2,7 @@ "name": "mongodb-instance-model", "description": "MongoDB instance model", "author": "Lucas Hrabovsky ", - "version": "12.23.3", + "version": "12.24.0", "bugs": { "url": "https://jira.mongodb.org/projects/COMPASS/issues", "email": "compass@mongodb.com" @@ -29,12 +29,12 @@ }, "dependencies": { "ampersand-model": "^8.0.1", - "mongodb-collection-model": "^5.22.3", - "mongodb-data-service": "^22.22.3", - "mongodb-database-model": "^2.22.3" + "mongodb-collection-model": "^5.23.0", + "mongodb-data-service": "^22.23.0", + "mongodb-database-model": "^2.23.0" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "chai": "^4.3.4", "depcheck": "^1.4.1", diff --git a/packages/mongodb-explain-compat/package.json b/packages/mongodb-explain-compat/package.json index 8c5c9a8e82c..e9cfbb30b87 100644 --- a/packages/mongodb-explain-compat/package.json +++ b/packages/mongodb-explain-compat/package.json @@ -1,6 +1,6 @@ { "name": "mongodb-explain-compat", - "version": "3.0.4", + "version": "3.1.0", "description": "Convert mongodb SBE explain output to 4.4 explain output", "keywords": [ "mongodb", diff --git a/packages/mongodb-query-util/package.json b/packages/mongodb-query-util/package.json index 868774b7b47..be00cf94136 100644 --- a/packages/mongodb-query-util/package.json +++ b/packages/mongodb-query-util/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "2.2.5", + "version": "2.2.6", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -50,8 +50,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", diff --git a/packages/my-queries-storage/package.json b/packages/my-queries-storage/package.json index 9096d992ab4..a22dce52819 100644 --- a/packages/my-queries-storage/package.json +++ b/packages/my-queries-storage/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.15.0", + "version": "0.15.1", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -55,8 +55,8 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/chai": "^4.2.21", @@ -73,10 +73,10 @@ "typescript": "^5.0.4" }, "dependencies": { - "@mongodb-js/compass-editor": "^0.29.0", - "@mongodb-js/compass-user-data": "^0.3.3", + "@mongodb-js/compass-editor": "^0.29.1", + "@mongodb-js/compass-user-data": "^0.3.4", "bson": "^6.7.0", - "hadron-app-registry": "^9.2.2", + "hadron-app-registry": "^9.2.3", "react": "^17.0.2" } } diff --git a/packages/reflux-state-mixin/package.json b/packages/reflux-state-mixin/package.json index 61733721e63..708d26319c9 100644 --- a/packages/reflux-state-mixin/package.json +++ b/packages/reflux-state-mixin/package.json @@ -13,7 +13,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -53,8 +53,8 @@ "reflux": "^0.4.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", + "@mongodb-js/eslint-config-compass": "^1.1.5", + "@mongodb-js/mocha-config-compass": "^1.4.0", "@mongodb-js/prettier-config-compass": "^1.0.2", "@mongodb-js/tsconfig-compass": "^1.0.4", "@types/mocha": "^9.0.0", diff --git a/packages/ssh-tunnel/.depcheckrc b/packages/ssh-tunnel/.depcheckrc deleted file mode 100644 index fe20b38ee8a..00000000000 --- a/packages/ssh-tunnel/.depcheckrc +++ /dev/null @@ -1,6 +0,0 @@ -ignores: - - "@mongodb-js/prettier-config-compass" - - "@mongodb-js/tsconfig-compass" - - "@types/chai" - - "@types/sinon-chai" - - "sinon" diff --git a/packages/ssh-tunnel/.eslintignore b/packages/ssh-tunnel/.eslintignore deleted file mode 100644 index 85a8a75e68c..00000000000 --- a/packages/ssh-tunnel/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -.nyc-output -dist diff --git a/packages/ssh-tunnel/.eslintrc.js b/packages/ssh-tunnel/.eslintrc.js deleted file mode 100644 index 9c3ab95632f..00000000000 --- a/packages/ssh-tunnel/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; -module.exports = { - root: true, - extends: ['@mongodb-js/eslint-config-compass'], - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig-lint.json'], - }, -}; diff --git a/packages/ssh-tunnel/.mocharc.js b/packages/ssh-tunnel/.mocharc.js deleted file mode 100644 index e7eaccd61fa..00000000000 --- a/packages/ssh-tunnel/.mocharc.js +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -module.exports = require('@mongodb-js/mocha-config-compass'); diff --git a/packages/ssh-tunnel/.prettierignore b/packages/ssh-tunnel/.prettierignore deleted file mode 100644 index 85a8a75e68c..00000000000 --- a/packages/ssh-tunnel/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -.nyc-output -dist diff --git a/packages/ssh-tunnel/.prettierrc.json b/packages/ssh-tunnel/.prettierrc.json deleted file mode 100644 index 18853d1532e..00000000000 --- a/packages/ssh-tunnel/.prettierrc.json +++ /dev/null @@ -1 +0,0 @@ -"@mongodb-js/prettier-config-compass" diff --git a/packages/ssh-tunnel/package.json b/packages/ssh-tunnel/package.json deleted file mode 100644 index 86e4857aa62..00000000000 --- a/packages/ssh-tunnel/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "@mongodb-js/ssh-tunnel", - "description": "Yet another ssh tunnel based on ssh2", - "author": { - "name": "MongoDB Inc", - "email": "compass@mongodb.com" - }, - "publishConfig": { - "access": "public" - }, - "bugs": { - "url": "https://jira.mongodb.org/projects/COMPASS/issues", - "email": "compass@mongodb.com" - }, - "homepage": "https://github.com/mongodb-js/compass", - "version": "2.3.3", - "repository": { - "type": "git", - "url": "https://github.com/mongodb-js/compass.git" - }, - "files": [ - "dist" - ], - "license": "Apache-2.0", - "main": "dist/index.js", - "compass:main": "src/index.ts", - "exports": { - "import": "./dist/.esm-wrapper.mjs", - "require": "./dist/index.js" - }, - "compass:exports": { - ".": "./src/index.ts" - }, - "types": "./dist/index.d.ts", - "scripts": { - "bootstrap": "npm run compile", - "prepublishOnly": "npm run compile && compass-scripts check-exports-exist", - "compile": "tsc -p tsconfig.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", - "eslint": "eslint", - "prettier": "prettier", - "lint": "npm run eslint . && npm run prettier -- --check .", - "depcheck": "compass-scripts check-peer-deps && depcheck", - "check": "npm run lint && npm run depcheck", - "check-ci": "npm run check", - "test": "mocha", - "test-cov": "nyc --compact=false --produce-source-map=false -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", - "test-watch": "npm run test -- --watch", - "test-ci": "npm run test-cov", - "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." - }, - "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", - "@mongodb-js/mocha-config-compass": "^1.3.10", - "@mongodb-js/prettier-config-compass": "^1.0.2", - "@mongodb-js/tsconfig-compass": "^1.0.4", - "@types/chai": "^4.2.21", - "@types/chai-as-promised": "^7.1.4", - "@types/mocha": "^9.0.0", - "@types/node-fetch": "^2.6.11", - "@types/sinon-chai": "^3.2.5", - "@types/ssh2": "^1.11.8", - "chai": "^4.3.4", - "chai-as-promised": "^7.1.1", - "depcheck": "^1.4.1", - "eslint": "^7.25.0", - "gen-esm-wrapper": "^1.1.0", - "mocha": "^10.2.0", - "node-fetch": "^2.7.0", - "nyc": "^15.1.0", - "prettier": "^2.7.1", - "sinon": "^9.2.3", - "socks": "^2.7.3", - "typescript": "^5.0.4" - }, - "dependencies": { - "@mongodb-js/compass-logging": "^1.4.3", - "socksv5": "0.0.6", - "ssh2": "^1.12.0" - } -} diff --git a/packages/ssh-tunnel/src/index.spec.ts b/packages/ssh-tunnel/src/index.spec.ts deleted file mode 100644 index a77fe97d987..00000000000 --- a/packages/ssh-tunnel/src/index.spec.ts +++ /dev/null @@ -1,420 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -import { once } from 'events'; -import type { ServerConfig } from 'ssh2'; -import { Server as SSHServer } from 'ssh2'; -import type { Server as HttpServer } from 'http'; -import { createServer, Agent as HttpAgent } from 'http'; -import { promisify } from 'util'; -import { readFileSync } from 'fs'; -import path from 'path'; -import { Socket } from 'net'; -import fetch, { FetchError } from 'node-fetch'; -import { expect } from 'chai'; -import { SocksClient } from 'socks'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import type { SshTunnelConfig } from './index'; -import SSHTunnel from './index'; -import sinon from 'sinon'; - -chai.use(chaiAsPromised); - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -let sshServer: SSHServer, httpServer: HttpServer, sshTunnel: SSHTunnel; - -function createTestHttpServer(): Promise { - return new Promise((resolve) => { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - httpServer = createServer(async (req, res) => { - if (req.url === '/error') { - res.statusCode = 500; - res.end('Error'); - } else if (req.url === '/wait') { - await sleep(5000); - res.end('Waited 5000ms'); - } else { - res.end('Hello from http server'); - } - }); - httpServer.listen(0, 'localhost', () => { - resolve(); - }); - }); -} - -async function stopTestHttpServer() { - try { - await promisify(httpServer.close.bind(httpServer))(); - (httpServer as unknown) = null; - } catch { - // noop - } -} - -function createTestSshServer( - config: Partial = {} -): Promise { - return new Promise((resolve) => { - const key = path.resolve(__dirname, '..', 'test', 'fixtures', 'rsa'); - sshServer = new SSHServer( - { - hostKeys: [readFileSync(key)], - ...config, - }, - (client) => { - client - .on('authentication', (ctx) => { - ctx.accept(); - }) - .on('ready', () => { - client.on('tcpip', (accept, _reject, { destPort, destIP }) => { - const channel = accept(); - const connection = new Socket(); - channel.pipe(connection).pipe(channel); - connection.connect(destPort, destIP); - }); - }); - } - ); - sshServer.listen(0, 'localhost', () => resolve()); - }); -} - -async function stopTestSshServer() { - try { - await promisify(sshServer.close.bind(sshServer))(); - (sshServer as unknown) = null; - } catch { - // noop - } -} - -async function createTestSshTunnel(config: Partial = {}) { - sshTunnel = new SSHTunnel({ - username: 'user', - port: sshServer.address().port, - localPort: 0, - ...config, - }); - sinon.spy(sshTunnel.sshClient, 'connect'); - await sshTunnel.listen(); -} - -async function stopTestSshTunnel() { - try { - await sshTunnel.close(); - (sshTunnel as unknown) = null; - } catch { - // noop - } -} - -function breakSshTunnelConnection() { - const promise = once(sshTunnel.sshClient, 'close'); - sshTunnel.sshClient.end(); - return promise; -} - -interface Socks5ProxyOptions { - proxyHost: string; - proxyPort: number; - proxyUsername?: string; - proxyPassword?: string; -} - -class Socks5HttpAgent extends HttpAgent { - options: Socks5ProxyOptions; - - constructor(options: Socks5ProxyOptions) { - super(); - this.options = options; - } - - createConnection(options, callback) { - void SocksClient.createConnection( - { - destination: { - host: options.host, - port: +options.port, - }, - proxy: { - host: this.options.proxyHost, - port: this.options.proxyPort, - type: 5, - userId: this.options.proxyUsername, - password: this.options.proxyPassword, - }, - command: 'connect', - }, - (err, info) => { - if (err) { - callback(err); - } else { - callback(null, info.socket); - } - } - ); - } -} - -async function httpFetchWithSocks5( - httpUrl: string, - options: Socks5ProxyOptions -): ReturnType { - const agent = new Socks5HttpAgent(options); - return await fetch(httpUrl, { agent }); -} - -/** - * @securityTest SSH Tunnel Support Testing - * - * We ensure that, when the application opens an SSH Tunnel in response to a user - * request to do so, it does so securely. For example, we verify that no other application - * is able to use the same tunnel. - */ -describe('SSHTunnel', function () { - beforeEach(async function () { - await createTestSshServer(); - await createTestHttpServer(); - }); - - afterEach(async function () { - await stopTestSshTunnel(); - await stopTestSshServer(); - await stopTestHttpServer(); - }); - - it('should be main export', function () { - expect(new SSHTunnel()).to.be.instanceof(SSHTunnel); - }); - - it('creates a tunnel that allows to request remote server through an ssh server', async function () { - await createTestSshTunnel(); - - const res = await httpFetchWithSocks5( - `http://localhost:${httpServer.address().port}/`, - { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - } - ); - expect(await res.text()).to.equal('Hello from http server'); - }); - - it('creates a tunnel that passes through requests when auth matches', async function () { - await createTestSshTunnel({ - socks5Username: 'cat', - socks5Password: 'meow', - }); - - const res = await httpFetchWithSocks5( - `http://localhost:${httpServer.address().port}/`, - { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - proxyUsername: 'cat', - proxyPassword: 'meow', - } - ); - expect(await res.text()).to.equal('Hello from http server'); - }); - - it('creates a tunnel that rejects requests when auth mismatches', async function () { - await createTestSshTunnel({ - socks5Username: 'cat', - socks5Password: 'meow', - }); - - try { - await httpFetchWithSocks5( - `http://localhost:${httpServer.address().port}/`, - { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - proxyUsername: 'cow', - proxyPassword: 'moo', - } - ); - expect.fail('missed exception'); - } catch (err) { - expect(err.message).to.match(/Socks5 Authentication failed/); - } - }); - - it('closes any connections on tunnel close', async function () { - await createTestSshTunnel(); - - try { - await Promise.all([ - httpFetchWithSocks5( - `http://localhost:${httpServer.address().port}/wait`, - { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - } - ), - (async () => { - await sleep(500); - await sshTunnel.close(); - })(), - ]); - expect.fail('missed exception'); - } catch (err) { - expect(err.message).to.match(/socket hang up/); - } - }); - - it('fails on listen call if ssh server is not available', async function () { - try { - await createTestSshTunnel({ - host: 'nonexistent-ssh-server.test', - port: 4242, - }); - expect.fail('missed exception'); - } catch (err) { - expect(err.message).to.equal( - 'getaddrinfo ENOTFOUND nonexistent-ssh-server.test' - ); - } - }); - - it('stops http server if encountered an error connecting to ssh server', async function () { - try { - await createTestSshTunnel({ - host: 'nonexistent-ssh-server.test', - port: 4242, - }); - expect.fail('missed exception'); - } catch { - expect(sshTunnel.server.address()).to.equal(null); - } - }); - - it('does not reconnect if the tunnel is already connected', async function () { - await createTestSshTunnel(); - - const address = `http://localhost:${httpServer.address().port}/`; - const options = { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - }; - const expected = 'Hello from http server'; - - const res1 = await httpFetchWithSocks5(address, options); - expect(await res1.text()).to.equal(expected); - - const res2 = await httpFetchWithSocks5(address, options); - expect(await res2.text()).to.equal(expected); - - expect(sshTunnel.sshClient.connect.callCount).to.equal(1); - }); - - it('reconnects tunnel if it got accidentally disconnected', async function () { - await createTestSshTunnel(); - - await breakSshTunnelConnection(); - - const address = `http://localhost:${httpServer.address().port}/`; - const options = { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - }; - const expected = 'Hello from http server'; - - const res1 = await httpFetchWithSocks5(address, options); - expect(await res1.text()).to.equal(expected); - - const res2 = await httpFetchWithSocks5(address, options); - expect(await res2.text()).to.equal(expected); - - expect(sshTunnel.sshClient.connect.callCount).to.equal(2); - }); - - it('reuses the connection promise if a request comes in before the tunnel connects', async function () { - await createTestSshTunnel(); - - await breakSshTunnelConnection(); - - const address = `http://localhost:${httpServer.address().port}/`; - const options = { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - }; - const expected = 'Hello from http server'; - - const [res1, res2] = await Promise.all([ - httpFetchWithSocks5(address, options), - httpFetchWithSocks5(address, options), - ]); - expect(await res1.text()).to.equal(expected); - expect(await res2.text()).to.equal(expected); - - expect(sshTunnel.sshClient.connect.callCount).to.equal(2); - }); - - it('does not reconnect the tunnel after it was deliberately closed', async function () { - await createTestSshTunnel(); - - // NOTE: normally you'd call close(), but that also closes the server so - // you'd get a different error first. Trying to trigger the race condition - // where the request made it to the socks5 server in time. - await sshTunnel.closeSshClient(); - - const remotePort = httpServer.address().port; - const address = `http://localhost:${remotePort}/`; - - const options = { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - }; - - const promise = httpFetchWithSocks5(address, options); - - await expect(promise).to.be.rejectedWith( - FetchError, - `request to http://localhost:${remotePort}/ failed, reason: Socket closed` - ); - - expect(sshTunnel.sshClient.connect.callCount).to.equal(1); - }); - - it('reconnects if the ssh connection times out while we try and open the channel', async function () { - await createTestSshTunnel(); - - const forwardOut = sshTunnel.forwardOut; - sinon - .stub(sshTunnel, 'forwardOut') - .callsFake(async function ( - srcAddr: string, - srcPort: number, - dstAddr: string, - dstPort: number - ) { - await breakSshTunnelConnection(); - const promise = forwardOut.call( - this, - srcAddr, - srcPort, - dstAddr, - dstPort - ); - sshTunnel.forwardOut.restore(); - return promise; - }); - - const address = `http://localhost:${httpServer.address().port}/`; - const options = { - proxyHost: sshTunnel.config.localAddr, - proxyPort: sshTunnel.config.localPort, - }; - const expected = 'Hello from http server'; - - const res = await httpFetchWithSocks5(address, options); - expect(await res.text()).to.equal(expected); - - expect(sshTunnel.sshClient.connect.callCount).to.equal(2); - }); -}); diff --git a/packages/ssh-tunnel/src/index.ts b/packages/ssh-tunnel/src/index.ts deleted file mode 100644 index 467efcb5673..00000000000 --- a/packages/ssh-tunnel/src/index.ts +++ /dev/null @@ -1,339 +0,0 @@ -import { promisify } from 'util'; -import { EventEmitter, once } from 'events'; -import type { Socket } from 'net'; -import type { ClientChannel, ConnectConfig } from 'ssh2'; -import { Client as SshClient } from 'ssh2'; -import { createLogger } from '@mongodb-js/compass-logging'; - -// The socksv5 module is not bundle-able by itself, so we get the -// subpackages directly -import socks5Server from 'socksv5/lib/server'; -import socks5AuthNone from 'socksv5/lib/auth/None'; -import socks5AuthUserPassword from 'socksv5/lib/auth/UserPassword'; - -const { log, mongoLogId, debug } = createLogger('COMPASS-SSH-TUNNEL'); - -type LocalProxyServerConfig = { - localAddr: string; - localPort: number; - socks5Username?: string; - socks5Password?: string; -}; - -type ErrorWithOrigin = Error & { origin?: string }; - -export type SshTunnelConfig = ConnectConfig & LocalProxyServerConfig; - -function getConnectConfig(config: Partial): ConnectConfig { - const { - // Doing it the other way around would be too much - /* eslint-disable @typescript-eslint/no-unused-vars */ - localAddr, - localPort, - socks5Password, - socks5Username, - /* eslint-enable @typescript-eslint/no-unused-vars */ - ...connectConfig - } = config; - - return connectConfig; -} - -function getSshTunnelConfig(config: Partial): SshTunnelConfig { - return { - localAddr: '127.0.0.1', - localPort: 0, - socks5Username: undefined, - socks5Password: undefined, - ...config, - }; -} - -let idCounter = 0; -export class SshTunnel extends EventEmitter { - private connected = false; - private closed = false; - private connectingPromise?: Promise; - private connections: Set = new Set(); - private server: any; - private rawConfig: SshTunnelConfig; - private sshClient: SshClient; - private serverListen: (port?: number, host?: string) => Promise; - private serverClose: () => Promise; - private forwardOut: ( - srcIP: string, - srcPort: number, - dstIP: string, - dstPort: number - ) => Promise; - private logCtx = `tunnel-${idCounter++}`; - - constructor(config: Partial = {}) { - super(); - - this.rawConfig = getSshTunnelConfig(config); - - this.sshClient = new SshClient(); - - this.sshClient.on('close', () => { - log.info(mongoLogId(1_001_000_252), this.logCtx, 'sshClient closed'); - this.connected = false; - }); - - this.forwardOut = promisify(this.sshClient.forwardOut.bind(this.sshClient)); - - this.server = socks5Server.createServer(this.socks5Request.bind(this)); - - if (this.rawConfig.socks5Username) { - this.server.useAuth( - socks5AuthUserPassword( - (user: string, pass: string, cb: (success: boolean) => void) => { - const success = - this.rawConfig.socks5Username === user && - this.rawConfig.socks5Password === pass; - log.info( - mongoLogId(1_001_000_253), - this.logCtx, - 'Validated auth parameters', - { success } - ); - queueMicrotask(() => cb(success)); - } - ) - ); - } else { - log.info(mongoLogId(1_001_000_254), this.logCtx, 'Skipping auth setup'); - this.server.useAuth(socks5AuthNone()); - } - - this.serverListen = promisify(this.server.listen.bind(this.server)); - this.serverClose = promisify(this.server.close.bind(this.server)); - - for (const eventName of ['close', 'error', 'listening'] as const) { - this.server.on(eventName, this.emit.bind(this, eventName)); - } - } - - get config(): SshTunnelConfig { - const serverAddress = this.server.address(); - - return { - ...this.rawConfig, - localPort: - (typeof serverAddress !== 'string' && serverAddress?.port) || - this.rawConfig.localPort, - }; - } - - async listen(): Promise { - const { localPort, localAddr } = this.rawConfig; - - log.info( - mongoLogId(1_001_000_255), - this.logCtx, - 'Listening for Socks5 connections', - { localAddr, localPort } - ); - await this.serverListen(localPort, localAddr); - - await this.connectSsh(); - } - - async close(): Promise { - log.info(mongoLogId(1_001_000_256), this.logCtx, 'Closing SSH tunnel'); - const [maybeError] = await Promise.all([ - // If we catch anything, just return the error instead of throwing, we - // want to await on closing the connections before re-throwing server - // close error - this.serverClose().catch((e) => e), - this.closeSshClient(), - this.closeOpenConnections(), - ]); - - if (maybeError) { - throw maybeError; - } - } - - private async connectSsh(): Promise { - if (this.connected) { - debug('already connected'); - return; - } - - if (this.connectingPromise) { - debug('reusing connectingPromise'); - return this.connectingPromise; - } - - if (this.closed) { - // A socks5 request could come in after we deliberately closed the connection. Don't reconnect in that case. - throw new Error('Disconnected.'); - } - - log.info( - mongoLogId(1_001_000_257), - this.logCtx, - 'Establishing new SSH connection' - ); - - this.connectingPromise = Promise.race([ - once(this.sshClient, 'error').then(([err]) => { - throw err; - }), - (() => { - const waitForReady = once(this.sshClient, 'ready').then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function - this.sshClient.connect(getConnectConfig(this.rawConfig)); - return waitForReady; - })(), - ]); - - try { - await this.connectingPromise; - } catch (err) { - this.emit('forwardingError', err); - log.error( - mongoLogId(1_001_000_258), - this.logCtx, - 'Failed to establish new SSH connection', - { error: (err as any)?.stack ?? String(err) } - ); - delete this.connectingPromise; - await this.serverClose(); - throw err; - } - - delete this.connectingPromise; - this.connected = true; - log.info( - mongoLogId(1_001_000_259), - this.logCtx, - 'Finished establishing new SSH connection' - ); - } - - private async closeSshClient() { - if (!this.connected) { - return; - } - - // don't automatically reconnect if another request comes in - this.closed = true; - - const promise = once(this.sshClient, 'close'); - this.sshClient.end(); - return promise; - } - - private async closeOpenConnections() { - const waitForClose: Promise[] = []; - this.connections.forEach((socket) => { - waitForClose.push(once(socket, 'close')); - socket.destroy(); - }); - await Promise.all(waitForClose); - this.connections.clear(); - } - - private async socks5Request( - info: any, - accept: (intercept: true) => Socket, - deny: () => void - ): Promise { - const { srcAddr, srcPort, dstAddr, dstPort } = info; - const logMetadata = { srcAddr, srcPort, dstAddr, dstPort }; - log.info( - mongoLogId(1_001_000_260), - this.logCtx, - 'Received Socks5 fowarding request', - { - ...logMetadata, - } - ); - let socket: Socket | null = null; - - try { - await this.connectSsh(); - - let channel; - try { - channel = await this.forwardOut(srcAddr, srcPort, dstAddr, dstPort); - } catch (err) { - if ((err as Error).message === 'Not connected') { - this.connected = false; - log.error( - mongoLogId(1_001_000_261), - this.logCtx, - 'Error forwarding Socks5 request, retrying', - { - ...logMetadata, - error: (err as Error).stack, - } - ); - await this.connectSsh(); - channel = await this.forwardOut(srcAddr, srcPort, dstAddr, dstPort); - } else { - throw err; - } - } - - log.info( - mongoLogId(1_001_000_262), - this.logCtx, - 'Opened SSH channel and accepting socks5 request', - { - ...logMetadata, - } - ); - - socket = accept(true); - this.connections.add(socket); - - socket.on('error', (err: ErrorWithOrigin) => { - log.error( - mongoLogId(1_001_000_263), - this.logCtx, - 'Error on Socks5 stream socket', - { - ...logMetadata, - error: (err as Error).stack, - } - ); - err.origin = err.origin ?? 'connection'; - this.emit('forwardingError', err); - }); - - socket.once('close', () => { - log.info( - mongoLogId(1_001_000_264), - this.logCtx, - 'Socks5 stream socket closed', - { - ...logMetadata, - } - ); - this.connections.delete(socket as Socket); - }); - - socket.pipe(channel).pipe(socket); - } catch (err) { - this.emit('forwardingError', err); - log.error( - mongoLogId(1_001_000_265), - this.logCtx, - 'Error establishing SSH channel for Socks5 request', - { - ...logMetadata, - error: (err as Error).stack, - } - ); - deny(); - if (socket) { - (err as any).origin = 'ssh-client'; - socket.destroy(err as any); - } - } - } -} - -export default SshTunnel; diff --git a/packages/ssh-tunnel/src/socksv5.d.ts b/packages/ssh-tunnel/src/socksv5.d.ts deleted file mode 100644 index 9c9cc3abfba..00000000000 --- a/packages/ssh-tunnel/src/socksv5.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module 'socksv5/lib/server' { - const mod: any; - export = mod; -} -declare module 'socksv5/lib/auth/None' { - const mod: any; - export = mod; -} -declare module 'socksv5/lib/auth/UserPassword' { - const mod: any; - export = mod; -} diff --git a/packages/ssh-tunnel/test/fixtures/rsa b/packages/ssh-tunnel/test/fixtures/rsa deleted file mode 100644 index 14f0ce2c19d..00000000000 --- a/packages/ssh-tunnel/test/fixtures/rsa +++ /dev/null @@ -1,50 +0,0 @@ ------BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn -NhAAAAAwEAAQAAAgEAtQFxHSyEJT1bx80o6j/YrtSyAGcUw0wXkc4dAmyqgvZsyvSCzhob -Zh/cZEdKxIQsZqI3qXnQbh6xCo8QoIc46ETGHG1VRzisSWBH8lUNp2m/24mYZwznGQ+h+H -/1S6iw/MML48huYDSi8IKTdnXob9j2QBbxEl86TVPoepq92dz2sQIKh2WmMaltOaGP+pqe -aQ+Xdq0HwGu4j0ALQbqJAHfB06PxzszvAyuShyNcNOpSfOskLO7FgtoE11qefkNWqPC4+U -gLLGflmCLg5TccFz+Tb7p3yYG9N76QztnqfoI7zKQpBuxmyysnmKYc5E7bfoxvOwpHRMzF -JbjpxDB2wX0ZOynyMBG+0hhB+/gRy++POSx7yWJpcP3A4YTbZ2rQIi8ujIz8O0EAVvH/FI -Neg+P+c3kt39287QxLXzUNz84RTCIVESpMzQ1W3VTrwvKrAXMExMvv58enUv3Qf7FLJGym -1PfzrZEgS6zNlfTjX3z2X8CFd09fDhItRQN1KjXELHASqXQa2M1SfmYHzYKcBvlLUe+jqO -ARV6yanWA0oAiDXTEBVg9dJySnyWnpr8EeviIpth6qmlMflIIsl7rEXaJbGmPkrnyLbwU7 -uxt+WZp15Z+6yVXQ5rmzInHuJP/mxn6e1D369qkYOZDuXyzUd2HsJF0+sotcXmF9JQr7Th -cAAAdg4gZNFuIGTRYAAAAHc3NoLXJzYQAAAgEAtQFxHSyEJT1bx80o6j/YrtSyAGcUw0wX -kc4dAmyqgvZsyvSCzhobZh/cZEdKxIQsZqI3qXnQbh6xCo8QoIc46ETGHG1VRzisSWBH8l -UNp2m/24mYZwznGQ+h+H/1S6iw/MML48huYDSi8IKTdnXob9j2QBbxEl86TVPoepq92dz2 -sQIKh2WmMaltOaGP+pqeaQ+Xdq0HwGu4j0ALQbqJAHfB06PxzszvAyuShyNcNOpSfOskLO -7FgtoE11qefkNWqPC4+UgLLGflmCLg5TccFz+Tb7p3yYG9N76QztnqfoI7zKQpBuxmyysn -mKYc5E7bfoxvOwpHRMzFJbjpxDB2wX0ZOynyMBG+0hhB+/gRy++POSx7yWJpcP3A4YTbZ2 -rQIi8ujIz8O0EAVvH/FINeg+P+c3kt39287QxLXzUNz84RTCIVESpMzQ1W3VTrwvKrAXME -xMvv58enUv3Qf7FLJGym1PfzrZEgS6zNlfTjX3z2X8CFd09fDhItRQN1KjXELHASqXQa2M -1SfmYHzYKcBvlLUe+jqOARV6yanWA0oAiDXTEBVg9dJySnyWnpr8EeviIpth6qmlMflIIs -l7rEXaJbGmPkrnyLbwU7uxt+WZp15Z+6yVXQ5rmzInHuJP/mxn6e1D369qkYOZDuXyzUd2 -HsJF0+sotcXmF9JQr7ThcAAAADAQABAAACADpkSq9UqxSwZKliH+7hxe8wonPKzUHrjDb3 -PRiJIcC56oLWulPuzCP350taTF51HTXG4xoDOCAuOoLjgEOpG8yiUx7cjoQ5XisVqmAc3B -jD3qbeDpI/8VV+W4wlC2bq9p2z9mP3RtQ2ZtIb7aJrix556YbnX8HDgrVrejYDMXfU9qhH -tknTmveuZpQO8Lmxo6TU6NHaJAQJPDLKQFdl68iA2cCCxQEnz3tAVTbPS3Gungm6eaMdLA -54ctNeYn7tDknVznZsrV4X7lNT/SU00BUX52JFz1rsRbRz/5cbabCCJvRviOS44rhsJYvz -GqL0ZY6/kyqCuFcTkA4JRzCJSeW6ZMK18VuUXsy+1UrYcwBO1J+xgCEPA7fvLk+2d9Eg4y -Iyb9bUI7CuiouGH5h9jZq71V8A6N2zCZ7Z9j2SdPZqsyyXy9KzBtBK8jKYKvP94DMezKoh -B8c1QhG5rkY8hH1H/oOVCsTWCmoidhQ19HcshsaF7Z/vQ2s+jZyLXu2Cq4EiBbe61sFEC6 -PdCFecnZemDfxLYbnYtVizncHy1j/pGfk0brcmA4c+CKMlIv4luB2MMeI0m4C5CHlgVJUF -d083i9ZgRL3vhD8xXRTMHPPE7U1ixAp2XHna4dvsedEpnENOSirsgejRm0CCcc77pk2upp -4saYpoggJ/suFuGYDhAAABAQCePGeFIlkYkb69wNtYf6agJxvVVf586IqBlmWIKSTgf5JB -+QvP/9n23/OPxmtBlaJPeyfgfMGEDIhIx/8e0L8zP51v0ypxcV9zeZEtC1rJMQT4u2Ae4H -nhRNGVRWXTsUTINs72kkd699w6JVKMUa9D5nKdEN4GZuWQNLN+SVyJFb1hi88+swJLv3dE -P2W9jBiQHXTgCcFdc1ZhxqvUtVZh9C9xvGIRE/z0zo3a36Z4CNMEr2ybiAkekXyM6Rvkj2 -MR36gHUN6gq5drd5f7pu6jEYDe18V0xu08aCU7KOjphmfof1WvGVBjNgPDYzUkOB1OHgJH -VFAPFj4YMnBPJavrAAABAQDk+B4NiZ/DfQIHi/ldDsJTJncpp85rkdlhcaQIqHJb8czLe3 -dPN/yH+T6aKGHJuZKJlQ7s02/3Tt5nXsrvgMJU+DLO7vFM2ix67ov9xI7xcZvHxP3DwscP -T+5jNoHmMpywFY36CjyD12IRU5skcBvDhfFJ8na6ugyO7mlEentuAB31dQiflW0ftGwuAD -fmz2IjkHCnP6d4UgPjcqXE3E4hLUa/Q6FPXNF1ljUhJeP7vvHyrbBEfC3kMrKf/cv9r2kC -vM/qpbFraFvzHRS6aZm0rxXLqTBVtXI2jEwvZsY+3nc/lEDpx2uF2tQ/honkQIVqRaD81O -1GZw+N+ol+fLIvAAABAQDKX8ZCc+1mX3DrnBoG1CTVSLyDIgaM6B7/gA0dLSyakwd2tait -A5yJkkVCB5XR9Qy/55pIZsGcMC8A40kRn3J2fM9H9J5XVry9SktB643jlH18j5w+bIaKdW -LdAU2OIeFoMn8PXkRQG/Bm2ZTDOC9k5cZ78tsIgei28FtucHC3bmDlVKP+8Z8tsPYxAQ/r -BgLf4aXDuoTHPty47M9TkBVFiubUEFcX4JhPiIUUHW64SzKYpHXynroRMLWznRa9GiE4ok -/ZOr+A8hMTU8fPB7jgRC4ztZsaOerxMrfEMNixzopajvTLgmch7mXavMtDnz0NqQWUe81u -EvAE+B64tDCZAAAAKnNlcmdleS5wZXR1c2hrb3ZAU2VyZ2V5cy1NYWNCb29rLVByby5sb2 -NhbA== ------END OPENSSH PRIVATE KEY----- diff --git a/packages/ssh-tunnel/test/fixtures/rsa.pub b/packages/ssh-tunnel/test/fixtures/rsa.pub deleted file mode 100644 index 942e0f945e7..00000000000 --- a/packages/ssh-tunnel/test/fixtures/rsa.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1AXEdLIQlPVvHzSjqP9iu1LIAZxTDTBeRzh0CbKqC9mzK9ILOGhtmH9xkR0rEhCxmojepedBuHrEKjxCghzjoRMYcbVVHOKxJYEfyVQ2nab/biZhnDOcZD6H4f/VLqLD8wwvjyG5gNKLwgpN2dehv2PZAFvESXzpNU+h6mr3Z3PaxAgqHZaYxqW05oY/6mp5pD5d2rQfAa7iPQAtBuokAd8HTo/HOzO8DK5KHI1w06lJ86yQs7sWC2gTXWp5+Q1ao8Lj5SAssZ+WYIuDlNxwXP5NvunfJgb03vpDO2ep+gjvMpCkG7GbLKyeYphzkTtt+jG87CkdEzMUluOnEMHbBfRk7KfIwEb7SGEH7+BHL7485LHvJYmlw/cDhhNtnatAiLy6MjPw7QQBW8f8Ug16D4/5zeS3f3bztDEtfNQ3PzhFMIhURKkzNDVbdVOvC8qsBcwTEy+/nx6dS/dB/sUskbKbU9/OtkSBLrM2V9ONffPZfwIV3T18OEi1FA3UqNcQscBKpdBrYzVJ+ZgfNgpwG+UtR76Oo4BFXrJqdYDSgCINdMQFWD10nJKfJaemvwR6+Iim2HqqaUx+UgiyXusRdolsaY+SufItvBTu7G35ZmnXln7rJVdDmubMice4k/+bGfp7UPfr2qRg5kO5fLNR3YewkXT6yi1xeYX0lCvtOFw== sergey.petushkov@Sergeys-MacBook-Pro.local diff --git a/packages/ssh-tunnel/tsconfig-lint.json b/packages/ssh-tunnel/tsconfig-lint.json deleted file mode 100644 index 6bdef84f322..00000000000 --- a/packages/ssh-tunnel/tsconfig-lint.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/ssh-tunnel/tsconfig.json b/packages/ssh-tunnel/tsconfig.json deleted file mode 100644 index ecd0a14474a..00000000000 --- a/packages/ssh-tunnel/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@mongodb-js/tsconfig-compass/tsconfig.common.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src/**/*"], - "exclude": ["./src/**/*.spec.*"] -} diff --git a/scripts/check-peer-deps.js b/scripts/check-peer-deps.js index f526173063a..b2764f4a427 100644 --- a/scripts/check-peer-deps.js +++ b/scripts/check-peer-deps.js @@ -148,9 +148,13 @@ async function getImportsForPackage(pkgJson, cwd = process.cwd()) { } return await collectAllAbsoluteImports( - entryPoints.map((entry) => { - return path.resolve(cwd, entry); - }) + entryPoints + .filter((entry) => { + return !/(test|spec).tsx?$/.test(entry); + }) + .map((entry) => { + return path.resolve(cwd, entry); + }) ); } diff --git a/scripts/package.json b/scripts/package.json index 3b238eb900d..755db91d185 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -14,7 +14,7 @@ "email": "compass@mongodb.com" }, "homepage": "https://github.com/mongodb-js/compass", - "version": "0.16.17", + "version": "0.16.18", "repository": { "type": "git", "url": "https://github.com/mongodb-js/compass.git" @@ -30,7 +30,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.1.4", + "@mongodb-js/eslint-config-compass": "^1.1.5", "@mongodb-js/prettier-config-compass": "^1.0.2", "depcheck": "^1.4.1", "eslint": "^7.25.0", @@ -40,7 +40,7 @@ "@babel/core": "^7.24.3", "@mongodb-js/monorepo-tools": "^1.1.1", "commander": "^11.0.0", - "electron": "^29.4.5", + "electron": "^30.4.0", "jsdom": "^21.1.0", "make-fetch-happen": "^8.0.14", "pacote": "^11.3.5",