diff --git a/package-lock.json b/package-lock.json index 112aed677fb..bfdaa5c6ea4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43748,12 +43748,260 @@ "js-yaml": "^3.13.1" }, "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.4.5", "chai": "^4.3.4", "depcheck": "^1.4.1", "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", @@ -43762,6 +44010,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", @@ -43774,6 +44055,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.30.2", @@ -68322,7 +68619,7 @@ "bson-transpilers": { "version": "file:packages/bson-transpilers", "requires": { - "@mongodb-js/eslint-config-compass": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.4.5", "antlr4": "4.7.2", "bson": "^6.10.4", "chai": "^4.3.4", @@ -68331,6 +68628,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", @@ -68339,6 +68777,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", @@ -68347,6 +68806,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/bson-transpilers/package.json b/packages/bson-transpilers/package.json index b0a076a99e4..2a6b9ffa882 100644 --- a/packages/bson-transpilers/package.json +++ b/packages/bson-transpilers/package.json @@ -32,7 +32,7 @@ }, "license": "SSPL", "devDependencies": { - "@mongodb-js/eslint-config-compass": "^1.4.4", + "@mongodb-js/eslint-config-compass": "^1.4.5", "chai": "^4.3.4", "depcheck": "^1.4.1", "mocha": "^10.2.0" 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..6bef5e47670 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, + 'DataModelDescription', + { + 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 fbaed7f7b27..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, }); } @@ -79,8 +78,4 @@ export class UserStorageImpl implements UserStorage { await this.userData.write(user.id, user); return this.getUser(user.id); } - - private getFileName(id: string) { - return `${id}.json`; - } } diff --git a/packages/compass-saved-aggregations-queries/src/index.spec.tsx b/packages/compass-saved-aggregations-queries/src/index.spec.tsx index eb1f7d1686d..b7cff3427ca 100644 --- a/packages/compass-saved-aggregations-queries/src/index.spec.tsx +++ b/packages/compass-saved-aggregations-queries/src/index.spec.tsx @@ -227,9 +227,11 @@ describe('AggregationsAndQueriesAndUpdatemanyList', function () { queryStorageLoadAllStub = sandbox .stub(queryStorage, 'loadAll') .resolves(queries.map((item) => item.query)); - sandbox - .stub(pipelineStorage, 'loadAll') - .resolves(pipelines.map((item) => item.aggregation)); + sandbox.stub(pipelineStorage, 'loadAll').resolves( + pipelines.map((item) => { + return { ...item.aggregation, lastModified: new Date() }; + }) + ); renderPlugin(); 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 b216fd77f99..8d8ae1df65e 100644 --- a/packages/compass-user-data/src/index.ts +++ b/packages/compass-user-data/src/index.ts @@ -1,3 +1,3 @@ -export type { Stats, ReadAllResult, ReadAllWithStatsResult } from './user-data'; -export { IUserData, FileUserData } from './user-data'; +export type { ReadAllResult } 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 155a020092a..a25d13393b4 100644 --- a/packages/compass-user-data/src/user-data.spec.ts +++ b/packages/compass-user-data/src/user-data.spec.ts @@ -1,10 +1,14 @@ import fs from 'fs/promises'; -import { Stats } from 'fs'; 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; @@ -29,7 +33,7 @@ const getTestSchema = ( }; const defaultValues = () => getTestSchema().parse({}); -const subdir = 'test-dir'; +const dataType = 'RecentQueries'; describe('user-data', function () { let tmpDir: string; @@ -48,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( [ @@ -80,7 +74,6 @@ describe('user-data', function () { ); const result = await getUserData().readAll(); - // sort result.data.sort((first, second) => first.name.localeCompare(second.name) ); @@ -125,39 +118,6 @@ describe('user-data', function () { expect(result.data).to.have.lengthOf(0); expect(result.errors).to.have.lengthOf(2); }); - - it('returns file stats', async function () { - await Promise.all( - [ - ['data1.json', JSON.stringify({ name: 'VSCode' })], - ['data2.json', JSON.stringify({ name: 'Mongosh' })], - ].map(([filepath, data]) => writeFileToStorage(filepath, data)) - ); - - const { data } = await getUserData().readAllWithStats({ - ignoreErrors: true, - }); - - { - const vscodeData = data.find((x) => x[0].name === 'VSCode'); - expect(vscodeData?.[0]).to.deep.equal({ - name: 'VSCode', - hasDarkMode: true, - hasWebSupport: false, - }); - expect(vscodeData?.[1]).to.be.instanceOf(Stats); - } - - { - const mongoshData = data.find((x) => x[0].name === 'Mongosh'); - expect(mongoshData?.[0]).to.deep.equal({ - name: 'Mongosh', - hasDarkMode: true, - hasWebSupport: false, - }); - expect(mongoshData?.[1]).to.be.instanceOf(Stats); - } - }); }); context('UserData.readOne', function () { @@ -278,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({ @@ -302,38 +262,9 @@ describe('user-data', function () { company: 'MongoDB', }); }); - - it('return file stats', async function () { - await writeFileToStorage( - 'data.json', - JSON.stringify({ - name: 'Mongosh', - company: 'MongoDB', - }) - ); - - const [data, stats] = await getUserData().readOneWithStats('data', { - ignoreErrors: false, - }); - - expect(data).to.deep.equal({ - name: 'Mongosh', - hasDarkMode: true, - hasWebSupport: false, - }); - expect(stats).to.be.instanceOf(Stats); - }); }); 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' }); @@ -344,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' }); @@ -390,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() @@ -418,3 +341,545 @@ 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, + getResourceUrlStub, + 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 response is not ok', async function () { + authenticatedFetchStub.resolves(mockResponse({}, false, 500)); + 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', + 'test-org', + 'test-proj', + getResourceUrlStub, + 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 response is not ok', async function () { + authenticatedFetchStub.resolves(mockResponse({}, false, 404)); + 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 non-ok response gracefully', async function () { + authenticatedFetchStub.resolves(mockResponse({}, false, 500)); + 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('Failed to get data: 500'); + }); + + 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', + 'test-org', + 'test-proj', + getResourceUrlStub, + 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' + ) + .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' + ); + 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('returns false when response is not ok', async function () { + const getResponse = { + data: JSON.stringify({ name: 'Original Name', hasDarkMode: true }), + }; + + authenticatedFetchStub + .onFirstCall() + .resolves(mockResponse(getResponse)) + .onSecondCall() + .resolves(mockResponse({}, false, 400)); + + 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 = getAtlasUserData(); + + const result = await userData.updateAttributes('test-id', { + name: 'Updated', + }); + expect(result).equals(false); + }); + + 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', + 'test-org', + 'test-proj', + getResourceUrlStub, + 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 ec4cad6868f..11de8a1230c 100644 --- a/packages/compass-user-data/src/user-data.ts +++ b/packages/compass-user-data/src/user-data.ts @@ -10,14 +10,16 @@ const { log, mongoLogId } = createLogger('COMPASS-USER-STORAGE'); type SerializeContent = (content: I) => string; type DeserializeContent = (content: string) => unknown; -type GetFileName = (id: string) => string; +type GetResourceUrl = (path?: string) => Promise; +type AuthenticatedFetch = ( + url: RequestInfo | URL, + options?: RequestInit +) => Promise; export type FileUserDataOptions = { - subdir: string; basePath?: string; serialize?: SerializeContent; deserialize?: DeserializeContent; - getFileName?: GetFileName; }; export type AtlasUserDataOptions = { @@ -29,52 +31,19 @@ type ReadOptions = { ignoreErrors: boolean; }; -// Copied from the Node.js fs module. -export interface Stats { - isFile(): boolean; - isDirectory(): boolean; - isBlockDevice(): boolean; - isCharacterDevice(): boolean; - isSymbolicLink(): boolean; - isFIFO(): boolean; - isSocket(): boolean; - dev: number; - ino: number; - mode: number; - nlink: number; - uid: number; - gid: number; - rdev: number; - size: number; - blksize: number; - blocks: number; - atimeMs: number; - mtimeMs: number; - ctimeMs: number; - birthtimeMs: number; - atime: Date; - mtime: Date; - ctime: Date; - birthtime: Date; -} - export interface ReadAllResult { data: z.output[]; errors: Error[]; } -export interface ReadAllWithStatsResult { - data: [z.output, Stats][]; - errors: Error[]; -} - 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, @@ -84,6 +53,7 @@ export abstract class IUserData { } = {} ) { this.validator = validator; + this.dataType = dataType; this.serialize = serialize; this.deserialize = deserialize; } @@ -94,35 +64,30 @@ 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; - private readonly getFileName: GetFileName; protected readonly semaphore = new Semaphore(100); constructor( validator: T, - { - subdir, - basePath, - serialize, - deserialize, - getFileName = (id) => `${id}.json`, - }: FileUserDataOptions> + dataType: string, + { basePath, serialize, deserialize }: FileUserDataOptions> ) { - super(validator, { serialize, deserialize }); - this.subdir = subdir; + super(validator, dataType, { serialize, deserialize }); this.basePath = basePath; - this.getFileName = getFileName; + } + + private getFileName(id: string) { + return `${id}.json`; } 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 }); @@ -148,21 +113,15 @@ export class FileUserData extends IUserData { return path.resolve(root, pathRelativeToRoot); } - private async readAndParseFileWithStats( + private async readAndParseFile( absolutePath: string, options: ReadOptions - ): Promise<[z.output, Stats] | undefined> { + ): Promise | undefined> { let data: string; - let stats: Stats; - let handle: fs.FileHandle | undefined = undefined; let release: (() => void) | undefined = undefined; try { release = await this.semaphore.waitForRelease(); - handle = await fs.open(absolutePath, 'r'); - [stats, data] = await Promise.all([ - handle.stat(), - handle.readFile('utf-8'), - ]); + data = await fs.readFile(absolutePath, 'utf-8'); } catch (error) { log.error(mongoLogId(1_001_000_234), 'Filesystem', 'Error reading file', { path: absolutePath, @@ -173,13 +132,12 @@ export class FileUserData extends IUserData { } throw error; } finally { - await handle?.close(); release?.(); } try { const content = this.deserialize(data); - return [this.validator.parse(content), stats]; + return this.validator.parse(content); } catch (error) { log.error(mongoLogId(1_001_000_235), 'Filesystem', 'Error parsing data', { path: absolutePath, @@ -235,50 +193,52 @@ export class FileUserData extends IUserData { } } - async readAllWithStats( + async readAll( options: ReadOptions = { ignoreErrors: true, } - ): Promise> { - const absolutePath = await this.getFileAbsolutePath(); - const filePathList = await fs.readdir(absolutePath); - - const data = await Promise.allSettled( - filePathList.map((x) => - this.readAndParseFileWithStats(path.join(absolutePath, x), options) - ) - ); - - const result: ReadAllWithStatsResult = { + ): Promise> { + const result: ReadAllResult = { data: [], errors: [], }; - - for (const item of data) { - if (item.status === 'fulfilled' && item.value) { - result.data.push(item.value); + try { + const absolutePath = await this.getFileAbsolutePath(); + const filePathList = await fs.readdir(absolutePath); + for (const settled of await Promise.allSettled( + filePathList.map((x) => { + return this.readAndParseFile(path.join(absolutePath, x), options); + }) + )) { + if (settled.status === 'fulfilled' && settled.value) { + result.data.push(settled.value); + } + if (settled.status === 'rejected') { + result.errors.push(settled.reason); + } } - if (item.status === 'rejected') { - result.errors.push(item.reason); + return result; + } catch (err) { + if (options.ignoreErrors) { + return result; } + throw err; } - - return result; } - async readOneWithStats( + async readOne( id: string, options?: { ignoreErrors: false } - ): Promise<[z.output, Stats]>; - async readOneWithStats( + ): Promise>; + async readOne( id: string, options?: { ignoreErrors: true } - ): Promise<[z.output, Stats] | undefined>; - async readOneWithStats( + ): Promise | undefined>; + async readOne( id: string, options?: ReadOptions - ): Promise<[z.output, Stats] | undefined>; - async readOneWithStats( + ): Promise | undefined>; + async readOne( id: string, options: ReadOptions = { ignoreErrors: true, @@ -286,50 +246,236 @@ export class FileUserData extends IUserData { ) { const filepath = this.getFileName(id); const absolutePath = await this.getFileAbsolutePath(filepath); - return await this.readAndParseFileWithStats(absolutePath, options); + return await this.readAndParseFile(absolutePath, options); } - async readAll( - options: ReadOptions = { - ignoreErrors: true, + async updateAttributes( + id: string, + data: Partial> + ): Promise { + try { + await this.write(id, { + ...((await this.readOne(id)) ?? {}), + ...data, + }); + return true; + } catch { + return false; } - ): Promise> { - const result = await this.readAllWithStats(options); - return { - data: result.data.map(([data]) => data), - errors: result.errors, - }; } +} - async readOne( - id: string, - options?: { ignoreErrors: false } - ): Promise>; - async readOne( - id: string, - options?: { ignoreErrors: true } - ): Promise | undefined>; - async readOne( - id: string, - options?: ReadOptions - ): Promise | undefined>; - async readOne( - id: string, - options: ReadOptions = { - ignoreErrors: true, - } +// 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: string, + projectId: string, + getResourceUrl: GetResourceUrl, + authenticatedFetch: AuthenticatedFetch, + { serialize, deserialize }: AtlasUserDataOptions> ) { - return (await this.readOneWithStats(id, options))?.[0]; + 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 { + try { + this.validator.parse(content); + + const response = await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}` + ), + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + id: id, + data: this.serialize(content), + createdAt: new Date(), + projectId: this.projectId, + }), + } + ); + + if (!response.ok) { + throw new Error( + `Failed to post data: ${response.status} ${response.statusText}` + ); + } + + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_362), + 'Atlas Backend', + 'Error writing data', + { + url: await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}` + ), + error: (error as Error).message, + } + ); + return false; + } + } + + async delete(id: string): Promise { + try { + const response = await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + { + method: 'DELETE', + } + ); + if (!response.ok) { + throw new Error( + `Failed to delete data: ${response.status} ${response.statusText}` + ); + } + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_363), + 'Atlas Backend', + 'Error deleting data', + { + url: await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + 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', + } + ); + if (!response.ok) { + throw new Error( + `Failed to get data: ${response.status} ${response.statusText}` + ); + } + 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> { - await this.write(id, { - ...((await this.readOne(id)) ?? {}), - ...data, - }); - return await this.readOne(id); + ): Promise { + try { + const prevData = await this.readOne(id); + const newData: z.input = { + ...prevData, + ...data, + }; + + const response = await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: this.serialize(newData), + } + ); + if (!response.ok) { + throw new Error( + `Failed to update data: ${response.status} ${response.statusText}` + ); + } + return true; + } catch (error) { + log.error( + mongoLogId(1_001_000_364), + 'Atlas Backend', + 'Error updating data', + { + url: await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + error: (error as Error).message, + } + ); + return false; + } + } + + // TODO: change this depending on whether or not updateAttributes can provide all current data + async readOne(id: string): Promise> { + try { + const getResponse = await this.authenticatedFetch( + await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + { + method: 'GET', + } + ); + if (!getResponse.ok) { + throw new Error( + `Failed to fetch data: ${getResponse.status} ${getResponse.statusText}` + ); + } + 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_365), + 'Atlas Backend', + 'Error reading data', + { + url: await this.getResourceUrl( + `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + ), + error: (error as Error).message, + } + ); + return null; + } } } 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 766fc0166a0..8c42eeb722b 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.ts @@ -1,4 +1,3 @@ -import type { Stats } from '@mongodb-js/compass-user-data'; import { FileUserData } from '@mongodb-js/compass-user-data'; import { PipelineSchema } from './pipeline-storage-schema'; import type { SavedPipeline } from './pipeline-storage-schema'; @@ -7,27 +6,15 @@ 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, }); } - private mergeStats(pipeline: SavedPipeline, stats: Stats): SavedPipeline { - return { - ...pipeline, - lastModified: new Date(stats.ctimeMs), - }; - } - async loadAll(): Promise { try { - const { data } = await this.userData.readAllWithStats({ - ignoreErrors: false, - }); - return data.map(([item, stats]) => { - return this.mergeStats(item, stats); - }); + const { data } = await this.userData.readAll(); + return data; } catch { return []; } @@ -40,40 +27,42 @@ export class CompassPipelineStorage implements PipelineStorage { return this.loadAll().then((pipelines) => pipelines.filter(predicate)); } - private async loadOne(id: string): Promise { - const [item, stats] = await this.userData.readOneWithStats(id); - return this.mergeStats(item, stats); - } - - async createOrUpdate(id: string, attributes: SavedPipeline) { - const pipelineExists = Boolean( - await this.userData.readOne(id, { - ignoreErrors: true, - }) - ); + async createOrUpdate( + id: string, + attributes: Omit + ) { + const pipelineExists = Boolean(await this.userData.readOne(id)); return await (pipelineExists ? this.updateAttributes(id, attributes) : this.create(attributes)); } - private async create(data: SavedPipeline) { - 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-schema.ts b/packages/my-queries-storage/src/pipeline-storage-schema.ts index 35bfbe53ada..9173c4eea24 100644 --- a/packages/my-queries-storage/src/pipeline-storage-schema.ts +++ b/packages/my-queries-storage/src/pipeline-storage-schema.ts @@ -62,8 +62,8 @@ export const PipelineSchema = z.preprocess( pipelineText: z.string(), lastModified: z .number() - .transform((x) => new Date(x)) - .optional(), + .default(0) + .transform((x) => new Date(x)), }) ); diff --git a/packages/my-queries-storage/src/pipeline-storage.ts b/packages/my-queries-storage/src/pipeline-storage.ts index 7924645e6fe..ae10ed65ea7 100644 --- a/packages/my-queries-storage/src/pipeline-storage.ts +++ b/packages/my-queries-storage/src/pipeline-storage.ts @@ -5,10 +5,14 @@ export interface PipelineStorage { loadMany( predicate: (arg0: SavedPipeline) => boolean ): Promise; - createOrUpdate(id: string, attributes: SavedPipeline): Promise; + createOrUpdate( + id: string, + attributes: Omit + ): 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; }