diff --git a/package-lock.json b/package-lock.json index f25675ffda8..1b854eaf83b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43695,6 +43695,254 @@ "mocha": "^10.2.0" } }, + "packages/bson-transpilers/node_modules/@mongodb-js/eslint-config-compass": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/eslint-config-compass/-/eslint-config-compass-1.4.5.tgz", + "integrity": "sha512-5KLev9+SSp3ytlL5GA0oY/YamyrMGtPS63rgLYgJkvWTfyCN1obBzvNcbAAz4UntX7mU2/iWZ8GJnvR9jHKwEA==", + "dev": true, + "license": "SSPL", + "dependencies": { + "@babel/core": "^7.24.3", + "@babel/eslint-parser": "^7.14.3", + "@mongodb-js/eslint-config-devtools": "^0.9.9", + "@mongodb-js/eslint-plugin-compass": "^1.2.13", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-chai-friendly": "^1.1.0", + "eslint-plugin-filename-rules": "^1.2.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0" + }, + "bin": { + "eslint-compass": "bin/eslint.js" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.39.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/parser": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "packages/bson-transpilers/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "packages/bson-transpilers/node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -43703,6 +43951,39 @@ "sprintf-js": "~1.0.2" } }, + "packages/bson-transpilers/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/bson-transpilers/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/bson-transpilers/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "packages/bson-transpilers/node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -43715,6 +43996,22 @@ "js-yaml": "bin/js-yaml.js" } }, + "packages/bson-transpilers/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/collection-model": { "name": "mongodb-collection-model", "version": "5.31.0", @@ -68411,6 +68708,147 @@ "mocha": "^10.2.0" }, "dependencies": { + "@mongodb-js/eslint-config-compass": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@mongodb-js/eslint-config-compass/-/eslint-config-compass-1.4.5.tgz", + "integrity": "sha512-5KLev9+SSp3ytlL5GA0oY/YamyrMGtPS63rgLYgJkvWTfyCN1obBzvNcbAAz4UntX7mU2/iWZ8GJnvR9jHKwEA==", + "dev": true, + "requires": { + "@babel/core": "^7.24.3", + "@babel/eslint-parser": "^7.14.3", + "@mongodb-js/eslint-config-devtools": "^0.9.9", + "@mongodb-js/eslint-plugin-compass": "^1.2.13", + "@typescript-eslint/eslint-plugin": "^8.38.0", + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-chai-friendly": "^1.1.0", + "eslint-plugin-filename-rules": "^1.2.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-mocha": "^8.0.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "dependencies": { + "@typescript-eslint/type-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + } + }, + "@typescript-eslint/utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "requires": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" + } + }, + "@typescript-eslint/tsconfig-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "dev": true, + "requires": {} + }, + "@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "dev": true, + "requires": { + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.39.1", + "eslint-visitor-keys": "^4.2.1" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -68419,6 +68857,27 @@ "sprintf-js": "~1.0.2" } }, + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + }, + "ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -68427,6 +68886,15 @@ "argparse": "^1.0.7", "esprima": "^4.0.0" } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } } } }, diff --git a/packages/atlas-service/src/atlas-service.spec.ts b/packages/atlas-service/src/atlas-service.spec.ts index b63d7a9a1d6..166f226bf58 100644 --- a/packages/atlas-service/src/atlas-service.spec.ts +++ b/packages/atlas-service/src/atlas-service.spec.ts @@ -7,7 +7,7 @@ import { createNoopLogger } from '@mongodb-js/compass-logging/provider'; import { CompassAtlasAuthService } from './compass-atlas-auth-service'; const ATLAS_CONFIG = { - wsBaseUrl: 'ws://example.com', + ccsBaseUrl: 'ws://example.com', cloudBaseUrl: 'ws://example.com/cloud', atlasApiBaseUrl: 'http://example.com/api', atlasLogin: { diff --git a/packages/atlas-service/src/atlas-service.ts b/packages/atlas-service/src/atlas-service.ts index c7e5bbd7203..8a90283693e 100644 --- a/packages/atlas-service/src/atlas-service.ts +++ b/packages/atlas-service/src/atlas-service.ts @@ -76,7 +76,7 @@ export class AtlasService { return this.cloudEndpoint(path); } driverProxyEndpoint(path?: string): string { - return `${this.config.wsBaseUrl}${normalizePath(path)}`; + return `${this.config.ccsBaseUrl}${normalizePath(path)}`; } async fetch(url: RequestInfo | URL, init?: RequestInit): Promise { throwIfNetworkTrafficDisabled(this.preferences); diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index 5f9bec8c48f..350ae78ca8f 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -48,7 +48,7 @@ describe('CompassAuthServiceMain', function () { }; const defaultConfig = { - wsBaseUrl: 'ws://example.com', + ccsBaseUrl: 'ws://example.com', cloudBaseUrl: 'ws://example.com/cloud', atlasApiBaseUrl: 'http://example.com/api', atlasLogin: { diff --git a/packages/atlas-service/src/secret-store.ts b/packages/atlas-service/src/secret-store.ts index a303151250f..a75ffd2ba36 100644 --- a/packages/atlas-service/src/secret-store.ts +++ b/packages/atlas-service/src/secret-store.ts @@ -7,8 +7,7 @@ export class SecretStore { private readonly userData: FileUserData; private readonly fileName = 'AtlasPluginState'; constructor(basePath?: string) { - this.userData = new FileUserData(AtlasPluginStateSchema, { - subdir: 'AtlasState', + this.userData = new FileUserData(AtlasPluginStateSchema, 'AtlasState', { basePath, }); } diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index 48abca07cd4..ef17a3e10ef 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -96,7 +96,7 @@ export type AtlasServiceConfig = { /** * MongoDB Driver WebSocket proxy base url */ - wsBaseUrl: string; + ccsBaseUrl: string; /** * Cloud UI backend base url */ @@ -131,7 +131,7 @@ export type AtlasServiceConfig = { */ const config = { 'atlas-local': { - wsBaseUrl: 'ws://localhost:61001/ws', + ccsBaseUrl: 'ws://localhost:61001/ws', cloudBaseUrl: '', atlasApiBaseUrl: 'http://localhost:8080/api/private', atlasLogin: { @@ -141,7 +141,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'atlas-dev': { - wsBaseUrl: '', + ccsBaseUrl: '', cloudBaseUrl: '', atlasApiBaseUrl: 'https://cloud-dev.mongodb.com/api/private', atlasLogin: { @@ -151,7 +151,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'atlas-qa': { - wsBaseUrl: '', + ccsBaseUrl: '', cloudBaseUrl: '', atlasApiBaseUrl: 'https://cloud-qa.mongodb.com/api/private', atlasLogin: { @@ -161,7 +161,7 @@ const config = { authPortalUrl: 'https://account-qa.mongodb.com/account/login', }, atlas: { - wsBaseUrl: '', + ccsBaseUrl: '', cloudBaseUrl: '', atlasApiBaseUrl: 'https://cloud.mongodb.com/api/private', atlasLogin: { @@ -171,7 +171,7 @@ const config = { authPortalUrl: 'https://account.mongodb.com/account/login', }, 'web-sandbox-atlas-local': { - wsBaseUrl: '/ccs', + ccsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'http://localhost:8080/api/private', atlasLogin: { @@ -181,7 +181,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'web-sandbox-atlas-dev': { - wsBaseUrl: '/ccs', + ccsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'https://cloud-dev.mongodb.com/api/private', atlasLogin: { @@ -191,7 +191,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'web-sandbox-atlas-qa': { - wsBaseUrl: '/ccs', + ccsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'https://cloud-dev.mongodb.com/api/private', atlasLogin: { @@ -201,7 +201,7 @@ const config = { authPortalUrl: 'https://account-dev.mongodb.com/account/login', }, 'web-sandbox-atlas': { - wsBaseUrl: '/ccs', + ccsBaseUrl: '/ccs', cloudBaseUrl: '/cloud-mongodb-com', atlasApiBaseUrl: 'https://cloud.mongodb.com/api/private', atlasLogin: { diff --git a/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx b/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx index fa7a62f77e9..f820943228f 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx +++ b/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx @@ -12,10 +12,13 @@ class DataModelStorageElectron implements DataModelStorage { typeof MongoDBDataModelDescriptionSchema >; constructor(basePath?: string) { - this.userData = new FileUserData(MongoDBDataModelDescriptionSchema, { - subdir: 'DataModelDescriptions', - basePath, - }); + this.userData = new FileUserData( + MongoDBDataModelDescriptionSchema, + 'DataModelDescriptions', + { + basePath, + } + ); } save(description: MongoDBDataModelDescription) { return this.userData.write(description.id, description); diff --git a/packages/compass-preferences-model/src/preferences-persistent-storage.ts b/packages/compass-preferences-model/src/preferences-persistent-storage.ts index 2a66dee37d5..6a29259d4ce 100644 --- a/packages/compass-preferences-model/src/preferences-persistent-storage.ts +++ b/packages/compass-preferences-model/src/preferences-persistent-storage.ts @@ -25,10 +25,13 @@ export class PersistentStorage implements PreferencesStorage { private safeStorage?: PreferencesSafeStorage; constructor(basePath?: string, safeStorage?: PreferencesSafeStorage) { - this.userData = new FileUserData(getPreferencesValidator(), { - subdir: 'AppPreferences', - basePath, - }); + this.userData = new FileUserData( + getPreferencesValidator(), + 'AppPreferences', + { + basePath, + } + ); this.safeStorage = safeStorage; } diff --git a/packages/compass-preferences-model/src/user-storage.ts b/packages/compass-preferences-model/src/user-storage.ts index 941c3f25065..292e5fb9ca0 100644 --- a/packages/compass-preferences-model/src/user-storage.ts +++ b/packages/compass-preferences-model/src/user-storage.ts @@ -26,8 +26,7 @@ export interface UserStorage { export class UserStorageImpl implements UserStorage { private readonly userData: FileUserData; constructor(basePath?: string) { - this.userData = new FileUserData(UserSchema, { - subdir: 'Users', + this.userData = new FileUserData(UserSchema, 'Users', { basePath, }); } diff --git a/packages/compass-shell/src/modules/history-storage.ts b/packages/compass-shell/src/modules/history-storage.ts index 5f1cab73a04..a7a28875a06 100644 --- a/packages/compass-shell/src/modules/history-storage.ts +++ b/packages/compass-shell/src/modules/history-storage.ts @@ -6,9 +6,8 @@ export class HistoryStorage { userData; constructor(basePath?: string) { - this.userData = new FileUserData(z.string().array(), { - // Todo: https://jira.mongodb.org/browse/COMPASS-7080 - subdir: getAppName() ?? '', + // TODO: https://jira.mongodb.org/browse/COMPASS-7080 + this.userData = new FileUserData(z.string().array(), getAppName() ?? '', { basePath, }); } diff --git a/packages/compass-user-data/src/index.ts b/packages/compass-user-data/src/index.ts index 14a3b49677c..8d8ae1df65e 100644 --- a/packages/compass-user-data/src/index.ts +++ b/packages/compass-user-data/src/index.ts @@ -1,3 +1,3 @@ export type { ReadAllResult } from './user-data'; -export { IUserData, FileUserData } from './user-data'; +export { type IUserData, FileUserData, AtlasUserData } from './user-data'; export { z } from 'zod'; diff --git a/packages/compass-user-data/src/user-data.spec.ts b/packages/compass-user-data/src/user-data.spec.ts index 88c785a2dc9..c60143f10eb 100644 --- a/packages/compass-user-data/src/user-data.spec.ts +++ b/packages/compass-user-data/src/user-data.spec.ts @@ -2,8 +2,13 @@ import fs from 'fs/promises'; import os from 'os'; import path from 'path'; import { expect } from 'chai'; -import { FileUserData, type FileUserDataOptions } from './user-data'; +import { + FileUserData, + AtlasUserData, + type FileUserDataOptions, +} from './user-data'; import { z, type ZodError } from 'zod'; +import sinon from 'sinon'; type ValidatorOptions = { allowUnknownProps?: boolean; @@ -28,7 +33,7 @@ const getTestSchema = ( }; const defaultValues = () => getTestSchema().parse({}); -const subdir = 'test-dir'; +const dataType = 'RecentQueries'; describe('user-data', function () { let tmpDir: string; @@ -47,29 +52,19 @@ describe('user-data', function () { > = {}, validatorOpts: ValidatorOptions = {} ) => { - return new FileUserData(getTestSchema(validatorOpts), { - subdir, + return new FileUserData(getTestSchema(validatorOpts), dataType, { basePath: tmpDir, ...userDataOpts, }); }; const writeFileToStorage = async (filepath: string, contents: string) => { - const absolutePath = path.join(tmpDir, subdir, filepath); + const absolutePath = path.join(tmpDir, dataType, filepath); await fs.mkdir(path.dirname(absolutePath), { recursive: true }); await fs.writeFile(absolutePath, contents, 'utf-8'); }; context('UserData.readAll', function () { - it('does not throw if the subdir does not exist and returns an empty list', async function () { - const userData = getUserData({ - subdir: 'something/non-existant', - }); - const result = await userData.readAll(); - expect(result.data).to.have.lengthOf(0); - expect(result.errors).to.have.lengthOf(0); - }); - it('reads all files from the folder with defaults', async function () { await Promise.all( [ @@ -79,7 +74,6 @@ describe('user-data', function () { ); const result = await getUserData().readAll(); - // sort result.data.sort((first, second) => first.name.localeCompare(second.name) ); @@ -244,7 +238,7 @@ describe('user-data', function () { }); }); - it('does not strip off unknown props that are unknow to validator when specified', async function () { + it('does not strip off unknown props that are unknown to validator when specified', async function () { await writeFileToStorage( 'data.json', JSON.stringify({ @@ -271,14 +265,6 @@ describe('user-data', function () { }); context('UserData.write', function () { - it('does not throw if the subdir does not exist', async function () { - const userData = getUserData({ - subdir: 'something/non-existant', - }); - const isWritten = await userData.write('data', { w: 1 }); - expect(isWritten).to.be.true; - }); - it('writes file to the storage with content', async function () { const userData = getUserData(); await userData.write('data', { name: 'VSCode' }); @@ -289,19 +275,11 @@ describe('user-data', function () { }); context('UserData.delete', function () { - it('does not throw if the subdir does not exist', async function () { - const userData = getUserData({ - subdir: 'something/non-existant', - }); - const isDeleted = await userData.delete('data.json'); - expect(isDeleted).to.be.false; - }); - it('deletes a file', async function () { const userData = getUserData(); const fileId = 'data'; - const absolutePath = path.join(tmpDir, subdir, `${fileId}.json`); + const absolutePath = path.join(tmpDir, dataType, `${fileId}.json`); await userData.write(fileId, { name: 'Compass' }); @@ -335,7 +313,7 @@ describe('user-data', function () { await userData.write('serialized', data); - const absolutePath = path.join(tmpDir, subdir, 'serialized.json'); + const absolutePath = path.join(tmpDir, dataType, 'serialized.json'); const writtenData = JSON.parse( (await fs.readFile(absolutePath)).toString() @@ -363,3 +341,541 @@ describe('user-data', function () { }); }); }); + +describe('AtlasUserData', function () { + let sandbox: sinon.SinonSandbox; + let authenticatedFetchStub: sinon.SinonStub; + let getResourceUrlStub: sinon.SinonStub; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + authenticatedFetchStub = sandbox.stub(); + getResourceUrlStub = sandbox.stub(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + const getAtlasUserData = ( + validatorOpts: ValidatorOptions = {}, + orgId = 'test-org', + projectId = 'test-proj', + type: + | 'RecentQueries' + | 'FavoriteQueries' + | 'SavedPipelines' = 'FavoriteQueries' + ) => { + return new AtlasUserData(getTestSchema(validatorOpts), type, { + orgId, + projectId, + getResourceUrl: getResourceUrlStub, + authenticatedFetch: authenticatedFetchStub, + }); + }; + + const mockResponse = (data: unknown, ok = true, status = 200) => { + return { + ok, + status, + statusText: status === 200 ? 'OK' : 'Error', + json: () => Promise.resolve(data), + }; + }; + + context('AtlasUserData.write', function () { + it('writes data successfully', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.write('test-id', { name: 'VSCode' }); + + expect(result).to.be.true; + expect(authenticatedFetchStub).to.have.been.calledOnce; + + const [url, options] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + expect(options.method).to.equal('POST'); + expect(options.headers['Content-Type']).to.equal('application/json'); + + const body = JSON.parse(options.body as string); + expect(body.id).to.equal('test-id'); + expect(body.projectId).to.equal('test-proj'); + expect(body.data).to.be.a('string'); + expect(JSON.parse(body.data as string)).to.deep.equal({ name: 'VSCode' }); + expect(body.createdAt).to.be.a('string'); + expect(new Date(body.createdAt as string)).to.be.instanceOf(Date); + }); + + it('returns false when authenticatedFetch throws an error', async function () { + authenticatedFetchStub.rejects( + new Error('HTTP 500: Internal Server Error') + ); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + + const result = await userData.write('test-id', { name: 'VSCode' }); + expect(result).to.be.false; + }); + + it('validator removes unknown props', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + + const result = await userData.write('test-id', { + name: 'VSCode', + randomProp: 'should fail', + }); + + expect(result).to.be.true; + }); + + it('uses custom serializer when provided', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = new AtlasUserData(getTestSchema(), 'FavoriteQueries', { + orgId: 'test-org', + projectId: 'test-proj', + getResourceUrl: getResourceUrlStub, + authenticatedFetch: authenticatedFetchStub, + serialize: (data) => `custom:${JSON.stringify(data)}`, + }); + + await userData.write('test-id', { name: 'Custom' }); + + const [, options] = authenticatedFetchStub.firstCall.args; + const body = JSON.parse(options.body as string); + expect(body.data).to.equal('custom:{"name":"Custom"}'); + }); + }); + + context('AtlasUserData.delete', function () { + it('deletes data successfully', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + + const userData = getAtlasUserData(); + const result = await userData.delete('test-id'); + + expect(result).to.be.true; + expect(authenticatedFetchStub).to.have.been.calledOnce; + + const [url, options] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + expect(options.method).to.equal('DELETE'); + }); + + it('returns false when authenticatedFetch throws an error', async function () { + authenticatedFetchStub.rejects(new Error('HTTP 404: Not Found')); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + + const result = await userData.delete('test-id'); + expect(result).to.be.false; + }); + }); + + context('AtlasUserData.readAll', function () { + it('reads all data successfully with defaults', async function () { + const responseData = [ + { data: JSON.stringify({ name: 'VSCode' }) }, + { data: JSON.stringify({ name: 'Mongosh' }) }, + ]; + authenticatedFetchStub.resolves(mockResponse(responseData)); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(2); + expect(result.errors).to.have.lengthOf(0); + + // Sort for consistent testing + result.data.sort((first, second) => + first.name.localeCompare(second.name) + ); + + expect(result.data).to.deep.equal([ + { + ...defaultValues(), + name: 'Mongosh', + }, + { + ...defaultValues(), + name: 'VSCode', + }, + ]); + + expect(authenticatedFetchStub).to.have.been.calledOnce; + const [url, options] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + expect(options.method).to.equal('GET'); + }); + + it('handles empty response', async function () { + authenticatedFetchStub.resolves(mockResponse([])); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(0); + expect(result.errors).to.have.lengthOf(0); + }); + + it('handles non-array response', async function () { + authenticatedFetchStub.resolves(mockResponse({ notAnArray: true })); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(0); + expect(result.errors).to.have.lengthOf(1); + }); + + it('handles errors gracefully', async function () { + authenticatedFetchStub.rejects(new Error('Unknown error')); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(0); + expect(result.errors).to.have.lengthOf(1); + expect(result.errors[0].message).to.equal('Unknown error'); + }); + + it('handles authenticatedFetch errors gracefully', async function () { + authenticatedFetchStub.rejects( + new Error('HTTP 500: Internal Server Error') + ); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(0); + expect(result.errors).to.have.lengthOf(1); + expect(result.errors[0].message).to.contain( + 'HTTP 500: Internal Server Error' + ); + }); + + it('uses custom deserializer when provided', async function () { + const responseData = [{ data: 'custom:{"name":"Custom"}' }]; + authenticatedFetchStub.resolves(mockResponse(responseData)); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = new AtlasUserData(getTestSchema(), 'FavoriteQueries', { + orgId: 'test-org', + projectId: 'test-proj', + getResourceUrl: getResourceUrlStub, + authenticatedFetch: authenticatedFetchStub, + deserialize: (data) => { + if (data.startsWith('custom:')) { + return JSON.parse(data.slice(7)); + } + return JSON.parse(data); + }, + }); + + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(1); + expect(result.data[0]).to.deep.equal({ + ...defaultValues(), + name: 'Custom', + }); + expect(result.errors).to.have.lengthOf(0); + }); + + it('strips unknown props by default', async function () { + const responseData = [ + { + data: JSON.stringify({ + name: 'VSCode', + unknownProp: 'should be stripped', + }), + }, + ]; + authenticatedFetchStub.resolves(mockResponse(responseData)); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ); + + const userData = getAtlasUserData(); + const result = await userData.readAll(); + + expect(result.data).to.have.lengthOf(1); + expect(result.data[0]).to.deep.equal({ + ...defaultValues(), + name: 'VSCode', + }); + expect(result.data[0]).to.not.have.property('unknownProp'); + expect(result.errors).to.have.lengthOf(0); + }); + }); + + context('AtlasUserData.updateAttributes', function () { + it('updates data successfully', async function () { + const getResponse = { + data: JSON.stringify({ name: 'Original Name', hasDarkMode: true }), + }; + const putResponse = {}; + + authenticatedFetchStub + .onFirstCall() + .resolves(mockResponse(getResponse)) + .onSecondCall() + .resolves(mockResponse(putResponse)); + + getResourceUrlStub + .onFirstCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ) + .onSecondCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + + const userData = getAtlasUserData(); + const result = await userData.updateAttributes('test-id', { + name: 'Updated Name', + hasDarkMode: false, + }); + + expect(result).equals(true); + + expect(authenticatedFetchStub).to.have.been.calledTwice; + + const [getUrl, getOptions] = authenticatedFetchStub.firstCall.args; + expect(getUrl).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + expect(getOptions.method).to.equal('GET'); + + const [putUrl, putOptions] = authenticatedFetchStub.secondCall.args; + expect(putUrl).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + expect(putOptions.method).to.equal('PUT'); + expect(putOptions.headers['Content-Type']).to.equal('application/json'); + }); + + it('throws error when authenticatedFetch throws an error', async function () { + const getResponse = { + data: JSON.stringify({ name: 'Original Name', hasDarkMode: true }), + }; + + authenticatedFetchStub + .onFirstCall() + .resolves(mockResponse(getResponse)) + .onSecondCall() + .rejects(new Error('HTTP 400: Bad Request')); + + getResourceUrlStub + .onFirstCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ) + .onSecondCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + + const userData = getAtlasUserData(); + + try { + await userData.updateAttributes('test-id', { + name: 'Updated', + }); + expect.fail('Expected method to throw an error'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.equal('HTTP 400: Bad Request'); + } + }); + + it('uses custom serializer for request body', async function () { + const getResponse = { + data: JSON.stringify({ name: 'Original Name', hasDarkMode: true }), + }; + const putResponse = {}; + + authenticatedFetchStub + .onFirstCall() + .resolves(mockResponse(getResponse)) + .onSecondCall() + .resolves(mockResponse(putResponse)); + + getResourceUrlStub + .onFirstCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj' + ) + .onSecondCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/test-org/test-proj/test-id' + ); + + const userData = new AtlasUserData(getTestSchema(), 'FavoriteQueries', { + orgId: 'test-org', + projectId: 'test-proj', + getResourceUrl: getResourceUrlStub, + authenticatedFetch: authenticatedFetchStub, + serialize: (data) => `custom:${JSON.stringify(data)}`, + }); + + await userData.updateAttributes('test-id', { name: 'Updated' }); + + const [, putOptions] = authenticatedFetchStub.secondCall.args; + expect(putOptions.body as string).to.equal( + 'custom:{"name":"Updated","hasDarkMode":true,"hasWebSupport":false}' + ); + }); + }); + + context('AtlasUserData urls', function () { + it('constructs URL correctly for write operation', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/custom-org/custom-proj' + ); + + const userData = getAtlasUserData({}, 'custom-org', 'custom-proj'); + await userData.write('test-id', { name: 'Test' }); + + const [url] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/custom-org/custom-proj' + ); + }); + + it('constructs URL correctly for delete operation', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456/item789' + ); + + const userData = getAtlasUserData({}, 'org123', 'proj456'); + await userData.delete('item789'); + + const [url] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456/item789' + ); + }); + + it('constructs URL correctly for read operation', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org456/proj123' + ); + + const userData = getAtlasUserData({}, 'org456', 'proj123'); + + await userData.readAll(); + + const [url] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org456/proj123' + ); + }); + + it('constructs URL correctly for update operation', async function () { + const getResponse = { + data: JSON.stringify({ name: 'Original', hasDarkMode: true }), + }; + const putResponse = {}; + + authenticatedFetchStub + .onFirstCall() + .resolves(mockResponse(getResponse)) + .onSecondCall() + .resolves(mockResponse(putResponse)); + + getResourceUrlStub + .onFirstCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456' + ) + .onSecondCall() + .resolves( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456/item789' + ); + + const userData = getAtlasUserData({}, 'org123', 'proj456'); + await userData.updateAttributes('item789', { name: 'Updated' }); + + expect(authenticatedFetchStub).to.have.been.calledTwice; + + const [getUrl] = authenticatedFetchStub.firstCall.args; + expect(getUrl).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456' + ); + + const [putUrl] = authenticatedFetchStub.secondCall.args; + expect(putUrl).to.equal( + 'cluster-connection.cloud.mongodb.com/FavoriteQueries/org123/proj456/item789' + ); + }); + + it('constructs URL correctly for different types', async function () { + authenticatedFetchStub.resolves(mockResponse({})); + getResourceUrlStub.resolves( + 'cluster-connection.cloud.mongodb.com/RecentQueries/org123/proj456' + ); + + const userData = getAtlasUserData( + {}, + 'org123', + 'proj456', + 'RecentQueries' + ); + await userData.write('item789', { name: 'Recent Item' }); + + const [url] = authenticatedFetchStub.firstCall.args; + expect(url).to.equal( + 'cluster-connection.cloud.mongodb.com/RecentQueries/org123/proj456' + ); + }); + }); +}); diff --git a/packages/compass-user-data/src/user-data.ts b/packages/compass-user-data/src/user-data.ts index 1216f151e79..5db85695f60 100644 --- a/packages/compass-user-data/src/user-data.ts +++ b/packages/compass-user-data/src/user-data.ts @@ -10,15 +10,23 @@ const { log, mongoLogId } = createLogger('COMPASS-USER-STORAGE'); type SerializeContent = (content: I) => string; type DeserializeContent = (content: string) => unknown; +type GetResourceUrl = (path?: string) => Promise; +type AuthenticatedFetch = ( + url: RequestInfo | URL, + options?: RequestInit +) => Promise; export type FileUserDataOptions = { - subdir: string; basePath?: string; serialize?: SerializeContent; deserialize?: DeserializeContent; }; export type AtlasUserDataOptions = { + orgId: string; + projectId: string; + getResourceUrl: GetResourceUrl; + authenticatedFetch: AuthenticatedFetch; serialize?: SerializeContent; deserialize?: DeserializeContent; }; @@ -34,11 +42,12 @@ export interface ReadAllResult { export abstract class IUserData { protected readonly validator: T; + protected readonly dataType: string; protected readonly serialize: SerializeContent>; protected readonly deserialize: DeserializeContent; - constructor( validator: T, + dataType: string, { serialize = (content: z.input) => JSON.stringify(content, null, 2), deserialize = JSON.parse, @@ -48,6 +57,7 @@ export abstract class IUserData { } = {} ) { this.validator = validator; + this.dataType = dataType; this.serialize = serialize; this.deserialize = deserialize; } @@ -58,25 +68,19 @@ export abstract class IUserData { abstract updateAttributes( id: string, data: Partial> - ): Promise>; + ): Promise; } export class FileUserData extends IUserData { - private readonly subdir: string; private readonly basePath?: string; protected readonly semaphore = new Semaphore(100); constructor( validator: T, - { - subdir, - basePath, - serialize, - deserialize, - }: FileUserDataOptions> + dataType: string, + { basePath, serialize, deserialize }: FileUserDataOptions> ) { - super(validator, { serialize, deserialize }); - this.subdir = subdir; + super(validator, dataType, { serialize, deserialize }); this.basePath = basePath; } @@ -87,7 +91,7 @@ export class FileUserData extends IUserData { private async getEnsuredBasePath(): Promise { const basepath = this.basePath ? this.basePath : getStoragePath(); - const root = path.join(basepath, this.subdir); + const root = path.join(basepath, this.dataType); await fs.mkdir(root, { recursive: true }); @@ -252,11 +256,198 @@ export class FileUserData extends IUserData { async updateAttributes( id: string, data: Partial> - ): Promise> { - await this.write(id, { - ...((await this.readOne(id)) ?? {}), - ...data, - }); - return await this.readOne(id); + ): Promise { + try { + await this.write(id, { + ...((await this.readOne(id)) ?? {}), + ...data, + }); + return true; + } catch { + return false; + } + } +} + +// TODO: update endpoints to reflect the merged api endpoints https://jira.mongodb.org/browse/CLOUDP-329716 +export class AtlasUserData extends IUserData { + private readonly authenticatedFetch; + private readonly getResourceUrl; + private orgId: string; + private projectId: string; + constructor( + validator: T, + dataType: string, + { + orgId, + projectId, + getResourceUrl, + authenticatedFetch, + serialize, + deserialize, + }: AtlasUserDataOptions> + ) { + super(validator, dataType, { serialize, deserialize }); + this.authenticatedFetch = authenticatedFetch; + this.getResourceUrl = getResourceUrl; + this.orgId = orgId; + this.projectId = projectId; + } + + async write(id: string, content: z.input): Promise { + const url = await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}` + ); + + try { + this.validator.parse(content); + + await this.authenticatedFetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: id, + data: this.serialize(content), + createdAt: new Date(), + projectId: this.projectId, + }), + }); + + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_366), + 'Atlas Backend', + 'Error writing data', + { + url, + error: (error as Error).message, + } + ); + return false; + } + } + + async delete(id: string): Promise { + const url = await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ); + + try { + await this.authenticatedFetch(url, { + method: 'DELETE', + }); + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_367), + 'Atlas Backend', + 'Error deleting data', + { + url, + error: (error as Error).message, + } + ); + return false; + } + } + + async readAll(): Promise> { + const result: ReadAllResult = { + data: [], + errors: [], + }; + try { + const response = await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}` + ), + { + method: 'GET', + } + ); + const json = await response.json(); + for (const item of json) { + try { + const parsedData = this.deserialize(item.data as string); + result.data.push(this.validator.parse(parsedData) as z.output); + } catch (error) { + result.errors.push(error as Error); + } + } + return result; + } catch (error) { + result.errors.push(error as Error); + return result; + } + } + + async updateAttributes( + id: string, + data: Partial> + ): Promise { + try { + const prevData = await this.readOne(id); + const newData: z.input = { + ...prevData, + ...data, + }; + + await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: this.serialize(newData), + } + ); + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_368), + 'Atlas Backend', + 'Error updating data', + { + url: await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + error: (error as Error).message, + } + ); + throw error; + } + } + + // TODO: change this depending on whether or not updateAttributes can provide all current data + async readOne(id: string): Promise> { + const url = await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ); + + try { + const getResponse = await this.authenticatedFetch(url, { + method: 'GET', + }); + const json = await getResponse.json(); + const data = this.validator.parse(this.deserialize(json.data as string)); + return data; + } catch (error) { + log.error( + mongoLogId(1_001_000_369), + 'Atlas Backend', + 'Error reading data', + { + url, + error: (error as Error).message, + } + ); + throw error; + } } } diff --git a/packages/connection-storage/src/compass-main-connection-storage.ts b/packages/connection-storage/src/compass-main-connection-storage.ts index 78f5380b735..19c9f468292 100644 --- a/packages/connection-storage/src/compass-main-connection-storage.ts +++ b/packages/connection-storage/src/compass-main-connection-storage.ts @@ -95,8 +95,7 @@ class CompassMainConnectionStorage implements ConnectionStorage { private readonly ipcMain: ConnectionStorageIPCMain, basePath?: string ) { - this.userData = new FileUserData(ConnectionSchema, { - subdir: 'Connections', + this.userData = new FileUserData(ConnectionSchema, 'Connections', { basePath, }); this.ipcMain.createHandle( diff --git a/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts b/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts index a1ae70727ef..26329ab6cf3 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts @@ -87,14 +87,12 @@ describe('CompassPipelineStorage', function () { expect((e as any).code).to.equal('ENOENT'); } - const pipeline = await pipelineStorage.createOrUpdate(data.id, data); + const result = await pipelineStorage.createOrUpdate(data.id, data); // Verify the file exists await fs.access(await getEnsuredFilePath(tmpDir, data.id)); - expect(pipeline.id).to.equal(data.id); - expect(pipeline.name).to.equal(data.name); - expect(pipeline.pipelineText).to.equal(data.pipelineText); + expect(result).to.be.true; }); it('createOrUpdate - updates a pipeline if it exists', async function () { @@ -108,14 +106,12 @@ describe('CompassPipelineStorage', function () { await createPipeline(tmpDir, data); await fs.access(await getEnsuredFilePath(tmpDir, data.id)); - const pipeline = await pipelineStorage.createOrUpdate(data.id, { + const result = await pipelineStorage.createOrUpdate(data.id, { ...data, name: 'modified listings', }); - expect(pipeline.id).to.equal(data.id); - expect(pipeline.name).to.equal('modified listings'); - expect(pipeline.pipelineText).to.equal(data.pipelineText); + expect(result).to.be.true; }); it('updateAttributes - updates a pipeline if it exists', async function () { @@ -136,17 +132,13 @@ describe('CompassPipelineStorage', function () { expect(restOfAggregation).to.deep.equal(data); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { lastModified, pipelineText, ...updatedAggregation } = - await pipelineStorage.updateAttributes(data.id, { - name: 'updated', - namespace: 'airbnb.users', - }); - - expect(updatedAggregation, 'returns updated pipeline').to.deep.equal({ - ...data, + const result = await pipelineStorage.updateAttributes(data.id, { name: 'updated', + namespace: 'airbnb.users', }); + expect(result).to.be.true; + { const aggregations = await pipelineStorage.loadAll(); expect(aggregations).to.have.length(1); diff --git a/packages/my-queries-storage/src/compass-pipeline-storage.ts b/packages/my-queries-storage/src/compass-pipeline-storage.ts index 069707ae1e7..d9484262d89 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.ts @@ -6,8 +6,7 @@ import type { PipelineStorage } from './pipeline-storage'; export class CompassPipelineStorage implements PipelineStorage { private readonly userData: FileUserData; constructor(basePath?: string) { - this.userData = new FileUserData(PipelineSchema, { - subdir: 'SavedPipelines', + this.userData = new FileUserData(PipelineSchema, 'SavedPipelines', { basePath, }); } @@ -42,24 +41,32 @@ export class CompassPipelineStorage implements PipelineStorage { : this.create(attributes)); } - private async create(data: Omit) { - await this.userData.write(data.id, { - ...data, - lastModified: Date.now(), - }); - return await this.loadOne(data.id); + async create(data: Omit): Promise { + try { + await this.userData.write(data.id, { + ...data, + lastModified: Date.now(), + }); + return true; + } catch { + return false; + } } async updateAttributes( id: string, attributes: Partial - ): Promise { - await this.userData.write(id, { - ...(await this.loadOne(id)), - ...attributes, - lastModified: Date.now(), - }); - return await this.loadOne(id); + ): Promise { + try { + await this.userData.write(id, { + ...(await this.userData.readOne(id)), + ...attributes, + lastModified: Date.now(), + }); + return true; + } catch { + return false; + } } async delete(id: string) { diff --git a/packages/my-queries-storage/src/compass-query-storage.ts b/packages/my-queries-storage/src/compass-query-storage.ts index c133ee81c53..dfcfbca36c5 100644 --- a/packages/my-queries-storage/src/compass-query-storage.ts +++ b/packages/my-queries-storage/src/compass-query-storage.ts @@ -16,8 +16,7 @@ export abstract class CompassQueryStorage { protected readonly options: QueryStorageOptions ) { // TODO: logic for whether we're in compass web or compass desktop - this.userData = new FileUserData(schemaValidator, { - subdir: folder, + this.userData = new FileUserData(schemaValidator, folder, { basePath: options.basepath, serialize: (content) => EJSON.stringify(content, undefined, 2), deserialize: (content: string) => EJSON.parse(content), @@ -49,7 +48,7 @@ export abstract class CompassQueryStorage { async updateAttributes( id: string, data: Partial> - ): Promise> { + ): Promise { return await this.userData.updateAttributes(id, data); } diff --git a/packages/my-queries-storage/src/pipeline-storage.ts b/packages/my-queries-storage/src/pipeline-storage.ts index faca39feb35..ae10ed65ea7 100644 --- a/packages/my-queries-storage/src/pipeline-storage.ts +++ b/packages/my-queries-storage/src/pipeline-storage.ts @@ -8,10 +8,11 @@ export interface PipelineStorage { createOrUpdate( id: string, attributes: Omit - ): Promise; + ): Promise; + create(attributes: Omit): Promise; updateAttributes( id: string, attributes: Partial - ): Promise; + ): Promise; delete(id: string): Promise; } diff --git a/packages/my-queries-storage/src/query-storage.ts b/packages/my-queries-storage/src/query-storage.ts index a932e0a46d5..9ee445a646c 100644 --- a/packages/my-queries-storage/src/query-storage.ts +++ b/packages/my-queries-storage/src/query-storage.ts @@ -6,7 +6,7 @@ import type { interface QueryStorage { loadAll(namespace?: string): Promise[]>; - updateAttributes(id: string, data: Partial>): Promise>; + updateAttributes(id: string, data: Partial>): Promise; delete(id: string): Promise; }